perf: inline module variables into template (#13075)

pull/13228/head
Ben McCann 4 months ago committed by GitHub
parent a6df4ebfcc
commit dffeef179e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: inline module variables into template

@ -54,7 +54,10 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[]; readonly after_update: Statement[];
/** The HTML template string */ /** The HTML template string */
readonly template: string[]; readonly template: {
push_quasi: (q: string) => void;
push_expression: (e: Expression) => void;
};
readonly locations: SourceLocation[]; readonly locations: SourceLocation[];
readonly metadata: { readonly metadata: {
namespace: Namespace; namespace: Namespace;

@ -311,3 +311,18 @@ export function create_derived_block_argument(node, context) {
export function create_derived(state, arg) { export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
} }
/**
* Whether a variable can be referenced directly from template string.
* @param {import('#compiler').Binding | undefined} binding
* @returns {boolean}
*/
export function can_inline_variable(binding) {
return (
!!binding &&
// in a `<script module>` block
!binding.scope.parent &&
// to prevent the need for escaping
binding.initial?.type === 'Literal'
);
}

@ -9,7 +9,7 @@ import { create_derived_block_argument } from '../utils.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function AwaitBlock(node, context) { export function AwaitBlock(node, context) {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
// Visit {#await <expression>} first to ensure that scopes are in the correct order // Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));

@ -7,5 +7,5 @@
*/ */
export function Comment(node, context) { export function Comment(node, context) {
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true // We'll only get here if comments are not filtered out, which they are unless preserveComments is true
context.state.template.push(`<!--${node.data}-->`); context.state.template.push_quasi(`<!--${node.data}-->`);
} }

@ -32,7 +32,7 @@ export function EachBlock(node, context) {
); );
if (!each_node_meta.is_controlled) { if (!each_node_meta.is_controlled) {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
} }
if (each_node_meta.array_name !== null) { if (each_node_meta.array_name !== null) {

@ -57,6 +57,11 @@ export function Fragment(node, context) {
/** @type {Statement | undefined} */ /** @type {Statement | undefined} */
let close = undefined; let close = undefined;
/** @type {string[]} */
const quasi = [];
/** @type {Expression[]} */
const expressions = [];
/** @type {ComponentClientTransformState} */ /** @type {ComponentClientTransformState} */
const state = { const state = {
...context.state, ...context.state,
@ -64,7 +69,22 @@ export function Fragment(node, context) {
init: [], init: [],
update: [], update: [],
after_update: [], after_update: [],
template: [], template: {
push_quasi: (/** @type {string} */ quasi_to_add) => {
if (quasi.length === 0) {
quasi.push(quasi_to_add);
return;
}
quasi[quasi.length - 1] = quasi[quasi.length - 1].concat(quasi_to_add);
},
push_expression: (/** @type {Expression} */ expression_to_add) => {
if (quasi.length === 0) {
quasi.push('');
}
expressions.push(expression_to_add);
quasi.push('');
}
},
locations: [], locations: [],
transform: { ...context.state.transform }, transform: { ...context.state.transform },
metadata: { metadata: {
@ -115,7 +135,12 @@ export function Fragment(node, context) {
}); });
/** @type {Expression[]} */ /** @type {Expression[]} */
const args = [b.template([b.quasi(state.template.join(''), true)], [])]; const args = [
b.template(
quasi.map((q) => b.quasi(q, true)),
expressions
)
];
if (state.metadata.context.template_needs_import_node) { if (state.metadata.context.template_needs_import_node) {
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE)); args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
@ -170,12 +195,15 @@ export function Fragment(node, context) {
flags |= TEMPLATE_USE_IMPORT_NODE; flags |= TEMPLATE_USE_IMPORT_NODE;
} }
if (state.template.length === 1 && state.template[0] === '<!>') { if (quasi.length === 1 && quasi[0] === '<!>') {
// special case — we can use `$.comment` instead of creating a unique template // special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment'))); body.push(b.var(id, b.call('$.comment')));
} else { } else {
add_template(template_name, [ add_template(template_name, [
b.template([b.quasi(state.template.join(''), true)], []), b.template(
quasi.map((q) => b.quasi(q, true)),
expressions
),
b.literal(flags) b.literal(flags)
]); ]);

@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function HtmlTag(node, context) { export function HtmlTag(node, context) {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
// push into init, so that bindings run afterwards, which might trigger another run and override hydration // push into init, so that bindings run afterwards, which might trigger another run and override hydration
context.state.init.push( context.state.init.push(

@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function IfBlock(node, context) { export function IfBlock(node, context) {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));

@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function KeyBlock(node, context) { export function KeyBlock(node, context) {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
const key = /** @type {Expression} */ (context.visit(node.expression)); const key = /** @type {Expression} */ (context.visit(node.expression));
const body = /** @type {Expression} */ (context.visit(node.fragment)); const body = /** @type {Expression} */ (context.visit(node.fragment));

@ -19,7 +19,7 @@ import {
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { is_custom_element_node } from '../../../nodes.js'; import { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter } from '../utils.js'; import { build_getter, can_inline_variable } from '../utils.js';
import { import {
get_attribute_name, get_attribute_name,
build_attribute_value, build_attribute_value,
@ -54,7 +54,7 @@ export function RegularElement(node, context) {
} }
if (node.name === 'noscript') { if (node.name === 'noscript') {
context.state.template.push('<noscript></noscript>'); context.state.template.push_quasi('<noscript></noscript>');
return; return;
} }
@ -68,7 +68,7 @@ export function RegularElement(node, context) {
namespace: determine_namespace_for_children(node, context.state.metadata.namespace) namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
}; };
context.state.template.push(`<${node.name}`); context.state.template.push_quasi(`<${node.name}`);
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */ /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = []; const attributes = [];
@ -242,7 +242,7 @@ export function RegularElement(node, context) {
const value = is_text_attribute(attribute) ? attribute.value[0].data : true; const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
if (name !== 'class' || value) { if (name !== 'class' || value) {
context.state.template.push( context.state.template.push_quasi(
` ${attribute.name}${ ` ${attribute.name}${
is_boolean_attribute(name) && value === true is_boolean_attribute(name) && value === true
? '' ? ''
@ -279,7 +279,7 @@ export function RegularElement(node, context) {
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id))); context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
} }
context.state.template.push('>'); context.state.template.push_quasi('>');
/** @type {SourceLocation[]} */ /** @type {SourceLocation[]} */
const child_locations = []; const child_locations = [];
@ -384,7 +384,7 @@ export function RegularElement(node, context) {
} }
if (!is_void(node.name)) { if (!is_void(node.name)) {
context.state.template.push(`</${node.name}>`); context.state.template.push_quasi(`</${node.name}>`);
} }
} }
@ -472,7 +472,7 @@ function build_element_spread_attributes(
value.type === 'Literal' && value.type === 'Literal' &&
context.state.metadata.namespace === 'html' context.state.metadata.namespace === 'html'
) { ) {
context.state.template.push(` is="${escape_html(value.value, true)}"`); context.state.template.push_quasi(` is="${escape_html(value.value, true)}"`);
continue; continue;
} }
@ -614,6 +614,13 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
); );
} }
const inlinable_expression =
attribute.value === true
? false // not an expression
: is_inlinable_expression(
Array.isArray(attribute.value) ? attribute.value : [attribute.value],
context.state
);
if (attribute.metadata.expression.has_state) { if (attribute.metadata.expression.has_state) {
if (has_call) { if (has_call) {
state.init.push(build_update(update)); state.init.push(build_update(update));
@ -622,11 +629,41 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
} }
return true; return true;
} else { } else {
state.init.push(update); if (inlinable_expression) {
context.state.template.push_quasi(` ${name}="`);
context.state.template.push_expression(value);
context.state.template.push_quasi('"');
} else {
state.init.push(update);
}
return false; return false;
} }
} }
/**
* @param {(AST.Text | AST.ExpressionTag)[]} nodes
* @param {import('../types.js').ComponentClientTransformState} state
*/
function is_inlinable_expression(nodes, state) {
let has_expression_tag = false;
for (let value of nodes) {
if (value.type === 'ExpressionTag') {
if (value.expression.type === 'Identifier') {
const binding = state.scope
.owner(value.expression.name)
?.declarations.get(value.expression.name);
if (!can_inline_variable(binding)) {
return false;
}
} else {
return false;
}
has_expression_tag = true;
}
}
return has_expression_tag;
}
/** /**
* Like `build_element_attribute_update_assignment` but without any special attribute treatment. * Like `build_element_attribute_update_assignment` but without any special attribute treatment.
* @param {Identifier} node_id * @param {Identifier} node_id

@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function RenderTag(node, context) { export function RenderTag(node, context) {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
const callee = unwrap_optional(node.expression).callee; const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;

@ -10,7 +10,7 @@ import { build_attribute_value } from './shared/element.js';
*/ */
export function SlotElement(node, context) { export function SlotElement(node, context) {
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback); // <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
/** @type {Property[]} */ /** @type {Property[]} */
const props = []; const props = [];

@ -21,7 +21,7 @@ import { build_render_statement, build_update } from './shared/utils.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function SvelteElement(node, context) { export function SvelteElement(node, context) {
context.state.template.push(`<!>`); context.state.template.push_quasi(`<!>`);
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */ /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = []; const attributes = [];

@ -357,7 +357,7 @@ export function build_component(node, component_name, context, anchor = context.
} }
if (Object.keys(custom_css_props).length > 0) { if (Object.keys(custom_css_props).length > 0) {
context.state.template.push( context.state.template.push_quasi(
context.state.metadata.namespace === 'svg' context.state.metadata.namespace === 'svg'
? '<g><!></g>' ? '<g><!></g>'
: '<div style="display: contents"><!></div>' : '<div style="display: contents"><!></div>'
@ -369,7 +369,7 @@ export function build_component(node, component_name, context, anchor = context.
b.stmt(b.call('$.reset', anchor)) b.stmt(b.call('$.reset', anchor))
); );
} else { } else {
context.state.template.push('<!>'); context.state.template.push_quasi('<!>');
statements.push(b.stmt(fn(anchor))); statements.push(b.stmt(fn(anchor)));
} }

@ -62,11 +62,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function flush_sequence(sequence) { function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) { if (sequence.every((node) => node.type === 'Text')) {
skipped += 1; skipped += 1;
state.template.push(sequence.map((node) => node.raw).join('')); state.template.push_quasi(sequence.map((node) => node.raw).join(''));
return; return;
} }
state.template.push(' '); state.template.push_quasi(' ');
const { has_state, has_call, value } = build_template_literal(sequence, visit, state); const { has_state, has_call, value } = build_template_literal(sequence, visit, state);

@ -0,0 +1,19 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
var root = $.template(`<picture><source srcset="${__DECLARED_ASSET_0__}" type="image/avif"> <source srcset="${__DECLARED_ASSET_1__}" type="image/webp"> <source srcset="${__DECLARED_ASSET_2__}" type="image/png"> <img src="${__DECLARED_ASSET_3__}" alt="production test" width="1440" height="1440"></picture>`);
export default function Inline_module_vars($$anchor) {
var picture = root();
var source = $.child(picture);
var source_1 = $.sibling(source, 2);
var source_2 = $.sibling(source_1, 2);
var img = $.sibling(source_2, 2);
$.reset(picture);
$.append($$anchor, picture);
}

@ -0,0 +1,10 @@
import * as $ from "svelte/internal/server";
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
export default function Inline_module_vars($$payload) {
$$payload.out += `<picture><source${$.attr("srcset", __DECLARED_ASSET_0__)} type="image/avif"> <source${$.attr("srcset", __DECLARED_ASSET_1__)} type="image/webp"> <source${$.attr("srcset", __DECLARED_ASSET_2__)} type="image/png"> <img${$.attr("src", __DECLARED_ASSET_3__)} alt="production test" width="1440" height="1440"></picture>`;
}

@ -0,0 +1,15 @@
<svelte:options runes={true} />
<script module>
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
</script>
<picture>
<source srcset={__DECLARED_ASSET_0__} type="image/avif" />
<source srcset={__DECLARED_ASSET_1__} type="image/webp" />
<source srcset={__DECLARED_ASSET_2__} type="image/png" />
<img src={__DECLARED_ASSET_3__} alt="production test" width=1440 height=1440 />
</picture>
Loading…
Cancel
Save