fix: more efficient spread attributes in SSR output (#11660)

* fix: more efficient spread attributes in SSR output

* more tweaks
pull/11669/head
Rich Harris 1 year ago committed by GitHub
parent 62e2647c8a
commit c70533a5a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: more efficient spread attributes in SSR output

@ -24,7 +24,13 @@ import {
import { create_attribute, is_custom_element_node, is_element_node } from '../../nodes.js';
import { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
import { DOMBooleanAttributes, HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
import {
DOMBooleanAttributes,
ELEMENT_IS_NAMESPACED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
HYDRATION_END,
HYDRATION_START
} from '../../../../constants.js';
import { escape_html } from '../../../../escaping.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
import { BLOCK_CLOSE, BLOCK_CLOSE_ELSE } from '../../../../internal/server/hydration.js';
@ -889,42 +895,29 @@ function serialize_element_spread_attributes(
class_directives,
context
) {
/** @type {import('estree').Expression[]} */
const values = [];
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute, context);
const value = serialize_attribute_value(
attribute.value,
context,
WhitespaceInsensitiveAttributes.includes(name)
);
values.push(b.object([b.prop('init', b.literal(name), value)]));
} else {
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
}
}
let classes;
let styles;
let flags = 0;
const lowercase_attributes =
element.metadata.svg ||
element.metadata.mathml ||
(element.type === 'RegularElement' && is_custom_element_node(element))
? b.false
: b.true;
if (class_directives.length > 0 || context.state.analysis.css.hash) {
const properties = class_directives.map((directive) =>
b.init(
directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name)
: /** @type {import('estree').Expression} */ (context.visit(directive.expression))
)
);
const is_html = element.metadata.svg || element.metadata.mathml ? b.false : b.true;
if (context.state.analysis.css.hash) {
properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
}
/** @type {import('estree').Expression[]} */
const args = [
b.array(values),
lowercase_attributes,
is_html,
b.literal(context.state.analysis.css.hash)
];
classes = b.object(properties);
}
if (style_directives.length > 0 || class_directives.length > 0) {
const styles = style_directives.map((directive) =>
if (style_directives.length > 0) {
const properties = style_directives.map((directive) =>
b.init(
directive.name,
directive.value === true
@ -932,25 +925,33 @@ function serialize_element_spread_attributes(
: serialize_attribute_value(directive.value, context, true)
)
);
const expressions = class_directives.map((directive) =>
b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
);
const classes = expressions.length
? b.call(
b.member(
b.call(b.member(b.array(expressions), b.id('filter')), b.id('Boolean')),
b.id('join')
),
b.literal(' ')
)
: b.literal('');
args.push(
b.object([
b.init('styles', styles.length === 0 ? b.literal(null) : b.object(styles)),
b.init('classes', classes)
])
);
styles = b.object(properties);
}
if (element.metadata.svg || element.metadata.mathml) {
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
} else if (is_custom_element_node(element)) {
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
}
const object = b.object(
attributes.map((attribute) => {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute, context);
const value = serialize_attribute_value(
attribute.value,
context,
WhitespaceInsensitiveAttributes.includes(name)
);
return b.prop('init', b.key(name), value);
}
return b.spread(/** @type {import('estree').Expression} */ (context.visit(attribute)));
})
);
const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(t_expression(b.call('$.spread_attributes', ...args)));
}

@ -21,11 +21,11 @@ export function is_element_node(node) {
}
/**
* @param {import('#compiler').RegularElement} node
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @returns {boolean}
*/
export function is_custom_element_node(node) {
return node.name.includes('-');
return node.type === 'RegularElement' && node.name.includes('-');
}
/**

@ -24,6 +24,9 @@ export const HYDRATION_END = ']';
export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered
export const HYDRATION_ERROR = {};
export const ELEMENT_IS_NAMESPACED = 1;
export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1;
export const UNINITIALIZED = Symbol();
/** List of elements that require raw contents and should not have SSR comments put in them */

@ -1,6 +1,12 @@
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import { UNINITIALIZED, DOMBooleanAttributes, RawTextElements } from '../../constants.js';
import {
UNINITIALIZED,
DOMBooleanAttributes,
RawTextElements,
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
ELEMENT_IS_NAMESPACED
} from '../../constants.js';
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
@ -178,66 +184,49 @@ export function css_props(payload, is_html, props, component) {
}
/**
* @param {Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes
* @param {boolean} is_html
* @param {string} class_hash
* @param {{ styles: Record<string, string> | null; classes: string }} [additional]
* @param {Record<string, unknown>} attrs
* @param {Record<string, string>} [classes]
* @param {Record<string, string>} [styles]
* @param {number} [flags]
* @returns {string}
*/
export function spread_attributes(attrs, lowercase_attributes, is_html, class_hash, additional) {
/** @type {Record<string, unknown>} */
const merged_attrs = {};
let key;
for (let i = 0; i < attrs.length; i++) {
const obj = attrs[i];
for (key in obj) {
// omit functions and internal svelte properties
const prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
if (typeof obj[key] !== 'function' && prefix !== '$$') {
merged_attrs[key] = obj[key];
}
}
}
const styles = additional?.styles;
export function spread_attributes(attrs, classes, styles, flags = 0) {
if (styles) {
if ('style' in merged_attrs) {
merged_attrs.style = style_object_to_string(
merge_styles(/** @type {string} */ (merged_attrs.style), styles)
);
} else {
merged_attrs.style = style_object_to_string(styles);
}
attrs.style = attrs.style
? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
: style_object_to_string(styles);
}
if (class_hash) {
if ('class' in merged_attrs) {
merged_attrs.class += ` ${class_hash}`;
} else {
merged_attrs.class = class_hash;
}
}
const classes = additional?.classes;
if (classes) {
if ('class' in merged_attrs) {
merged_attrs.class += ` ${classes}`;
} else {
merged_attrs.class = classes;
const classlist = attrs.class ? [attrs.class] : [];
for (const key in classes) {
if (classes[key]) {
classlist.push(key);
}
}
attrs.class = classlist.join(' ');
}
let attr_str = '';
let name;
for (name in merged_attrs) {
const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0;
const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0;
for (name in attrs) {
// omit functions, internal svelte properties and invalid attribute names
if (typeof attrs[name] === 'function') continue;
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
if (lowercase_attributes) {
if (lowercase) {
name = name.toLowerCase();
}
const is_boolean = is_html && DOMBooleanAttributes.includes(name);
attr_str += attr(name, merged_attrs[name], is_boolean);
attr_str += attr(name, attrs[name], is_boolean);
}
return attr_str;

Loading…
Cancel
Save