Skip to content

Commit dffeef1

Browse files
authored
perf: inline module variables into template (#13075)
1 parent a6df4eb commit dffeef1

File tree

19 files changed

+158
-26
lines changed

19 files changed

+158
-26
lines changed

.changeset/eighty-dragons-search.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
perf: inline module variables into template

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ export interface ComponentClientTransformState extends ClientTransformState {
5454
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
5555
readonly after_update: Statement[];
5656
/** The HTML template string */
57-
readonly template: string[];
57+
readonly template: {
58+
push_quasi: (q: string) => void;
59+
push_expression: (e: Expression) => void;
60+
};
5861
readonly locations: SourceLocation[];
5962
readonly metadata: {
6063
namespace: Namespace;

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,18 @@ export function create_derived_block_argument(node, context) {
311311
export function create_derived(state, arg) {
312312
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
313313
}
314+
315+
/**
316+
* Whether a variable can be referenced directly from template string.
317+
* @param {import('#compiler').Binding | undefined} binding
318+
* @returns {boolean}
319+
*/
320+
export function can_inline_variable(binding) {
321+
return (
322+
!!binding &&
323+
// in a `<script module>` block
324+
!binding.scope.parent &&
325+
// to prevent the need for escaping
326+
binding.initial?.type === 'Literal'
327+
);
328+
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { create_derived_block_argument } from '../utils.js';
99
* @param {ComponentContext} context
1010
*/
1111
export function AwaitBlock(node, context) {
12-
context.state.template.push('<!>');
12+
context.state.template.push_quasi('<!>');
1313

1414
// Visit {#await <expression>} first to ensure that scopes are in the correct order
1515
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));

packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
*/
88
export function Comment(node, context) {
99
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true
10-
context.state.template.push(`<!--${node.data}-->`);
10+
context.state.template.push_quasi(`<!--${node.data}-->`);
1111
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function EachBlock(node, context) {
3232
);
3333

3434
if (!each_node_meta.is_controlled) {
35-
context.state.template.push('<!>');
35+
context.state.template.push_quasi('<!>');
3636
}
3737

3838
if (each_node_meta.array_name !== null) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,34 @@ export function Fragment(node, context) {
5757
/** @type {Statement | undefined} */
5858
let close = undefined;
5959

60+
/** @type {string[]} */
61+
const quasi = [];
62+
/** @type {Expression[]} */
63+
const expressions = [];
64+
6065
/** @type {ComponentClientTransformState} */
6166
const state = {
6267
...context.state,
6368
before_init: [],
6469
init: [],
6570
update: [],
6671
after_update: [],
67-
template: [],
72+
template: {
73+
push_quasi: (/** @type {string} */ quasi_to_add) => {
74+
if (quasi.length === 0) {
75+
quasi.push(quasi_to_add);
76+
return;
77+
}
78+
quasi[quasi.length - 1] = quasi[quasi.length - 1].concat(quasi_to_add);
79+
},
80+
push_expression: (/** @type {Expression} */ expression_to_add) => {
81+
if (quasi.length === 0) {
82+
quasi.push('');
83+
}
84+
expressions.push(expression_to_add);
85+
quasi.push('');
86+
}
87+
},
6888
locations: [],
6989
transform: { ...context.state.transform },
7090
metadata: {
@@ -115,7 +135,12 @@ export function Fragment(node, context) {
115135
});
116136

117137
/** @type {Expression[]} */
118-
const args = [b.template([b.quasi(state.template.join(''), true)], [])];
138+
const args = [
139+
b.template(
140+
quasi.map((q) => b.quasi(q, true)),
141+
expressions
142+
)
143+
];
119144

120145
if (state.metadata.context.template_needs_import_node) {
121146
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
@@ -170,12 +195,15 @@ export function Fragment(node, context) {
170195
flags |= TEMPLATE_USE_IMPORT_NODE;
171196
}
172197

173-
if (state.template.length === 1 && state.template[0] === '<!>') {
198+
if (quasi.length === 1 && quasi[0] === '<!>') {
174199
// special case — we can use `$.comment` instead of creating a unique template
175200
body.push(b.var(id, b.call('$.comment')));
176201
} else {
177202
add_template(template_name, [
178-
b.template([b.quasi(state.template.join(''), true)], []),
203+
b.template(
204+
quasi.map((q) => b.quasi(q, true)),
205+
expressions
206+
),
179207
b.literal(flags)
180208
]);
181209

packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js';
99
* @param {ComponentContext} context
1010
*/
1111
export function HtmlTag(node, context) {
12-
context.state.template.push('<!>');
12+
context.state.template.push_quasi('<!>');
1313

1414
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
1515
context.state.init.push(

packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js';
88
* @param {ComponentContext} context
99
*/
1010
export function IfBlock(node, context) {
11-
context.state.template.push('<!>');
11+
context.state.template.push_quasi('<!>');
1212

1313
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
1414

packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js';
88
* @param {ComponentContext} context
99
*/
1010
export function KeyBlock(node, context) {
11-
context.state.template.push('<!>');
11+
context.state.template.push_quasi('<!>');
1212

1313
const key = /** @type {Expression} */ (context.visit(node.expression));
1414
const body = /** @type {Expression} */ (context.visit(node.fragment));

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import * as b from '../../../../utils/builders.js';
2020
import { is_custom_element_node } from '../../../nodes.js';
2121
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
22-
import { build_getter } from '../utils.js';
22+
import { build_getter, can_inline_variable } from '../utils.js';
2323
import {
2424
get_attribute_name,
2525
build_attribute_value,
@@ -54,7 +54,7 @@ export function RegularElement(node, context) {
5454
}
5555

5656
if (node.name === 'noscript') {
57-
context.state.template.push('<noscript></noscript>');
57+
context.state.template.push_quasi('<noscript></noscript>');
5858
return;
5959
}
6060

@@ -68,7 +68,7 @@ export function RegularElement(node, context) {
6868
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
6969
};
7070

71-
context.state.template.push(`<${node.name}`);
71+
context.state.template.push_quasi(`<${node.name}`);
7272

7373
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
7474
const attributes = [];
@@ -242,7 +242,7 @@ export function RegularElement(node, context) {
242242
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
243243

244244
if (name !== 'class' || value) {
245-
context.state.template.push(
245+
context.state.template.push_quasi(
246246
` ${attribute.name}${
247247
is_boolean_attribute(name) && value === true
248248
? ''
@@ -279,7 +279,7 @@ export function RegularElement(node, context) {
279279
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
280280
}
281281

282-
context.state.template.push('>');
282+
context.state.template.push_quasi('>');
283283

284284
/** @type {SourceLocation[]} */
285285
const child_locations = [];
@@ -384,7 +384,7 @@ export function RegularElement(node, context) {
384384
}
385385

386386
if (!is_void(node.name)) {
387-
context.state.template.push(`</${node.name}>`);
387+
context.state.template.push_quasi(`</${node.name}>`);
388388
}
389389
}
390390

@@ -472,7 +472,7 @@ function build_element_spread_attributes(
472472
value.type === 'Literal' &&
473473
context.state.metadata.namespace === 'html'
474474
) {
475-
context.state.template.push(` is="${escape_html(value.value, true)}"`);
475+
context.state.template.push_quasi(` is="${escape_html(value.value, true)}"`);
476476
continue;
477477
}
478478

@@ -614,6 +614,13 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
614614
);
615615
}
616616

617+
const inlinable_expression =
618+
attribute.value === true
619+
? false // not an expression
620+
: is_inlinable_expression(
621+
Array.isArray(attribute.value) ? attribute.value : [attribute.value],
622+
context.state
623+
);
617624
if (attribute.metadata.expression.has_state) {
618625
if (has_call) {
619626
state.init.push(build_update(update));
@@ -622,11 +629,41 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
622629
}
623630
return true;
624631
} else {
625-
state.init.push(update);
632+
if (inlinable_expression) {
633+
context.state.template.push_quasi(` ${name}="`);
634+
context.state.template.push_expression(value);
635+
context.state.template.push_quasi('"');
636+
} else {
637+
state.init.push(update);
638+
}
626639
return false;
627640
}
628641
}
629642

643+
/**
644+
* @param {(AST.Text | AST.ExpressionTag)[]} nodes
645+
* @param {import('../types.js').ComponentClientTransformState} state
646+
*/
647+
function is_inlinable_expression(nodes, state) {
648+
let has_expression_tag = false;
649+
for (let value of nodes) {
650+
if (value.type === 'ExpressionTag') {
651+
if (value.expression.type === 'Identifier') {
652+
const binding = state.scope
653+
.owner(value.expression.name)
654+
?.declarations.get(value.expression.name);
655+
if (!can_inline_variable(binding)) {
656+
return false;
657+
}
658+
} else {
659+
return false;
660+
}
661+
has_expression_tag = true;
662+
}
663+
}
664+
return has_expression_tag;
665+
}
666+
630667
/**
631668
* Like `build_element_attribute_update_assignment` but without any special attribute treatment.
632669
* @param {Identifier} node_id

packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js';
99
* @param {ComponentContext} context
1010
*/
1111
export function RenderTag(node, context) {
12-
context.state.template.push('<!>');
12+
context.state.template.push_quasi('<!>');
1313
const callee = unwrap_optional(node.expression).callee;
1414
const raw_args = unwrap_optional(node.expression).arguments;
1515

packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { build_attribute_value } from './shared/element.js';
1010
*/
1111
export function SlotElement(node, context) {
1212
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
13-
context.state.template.push('<!>');
13+
context.state.template.push_quasi('<!>');
1414

1515
/** @type {Property[]} */
1616
const props = [];

packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { build_render_statement, build_update } from './shared/utils.js';
2121
* @param {ComponentContext} context
2222
*/
2323
export function SvelteElement(node, context) {
24-
context.state.template.push(`<!>`);
24+
context.state.template.push_quasi(`<!>`);
2525

2626
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
2727
const attributes = [];

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ export function build_component(node, component_name, context, anchor = context.
357357
}
358358

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

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
6262
function flush_sequence(sequence) {
6363
if (sequence.every((node) => node.type === 'Text')) {
6464
skipped += 1;
65-
state.template.push(sequence.map((node) => node.raw).join(''));
65+
state.template.push_quasi(sequence.map((node) => node.raw).join(''));
6666
return;
6767
}
6868

69-
state.template.push(' ');
69+
state.template.push_quasi(' ');
7070

7171
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
7272

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import "svelte/internal/disclose-version";
2+
import * as $ from "svelte/internal/client";
3+
4+
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
5+
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
6+
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
7+
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
8+
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>`);
9+
10+
export default function Inline_module_vars($$anchor) {
11+
var picture = root();
12+
var source = $.child(picture);
13+
var source_1 = $.sibling(source, 2);
14+
var source_2 = $.sibling(source_1, 2);
15+
var img = $.sibling(source_2, 2);
16+
17+
$.reset(picture);
18+
$.append($$anchor, picture);
19+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as $ from "svelte/internal/server";
2+
3+
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
4+
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
5+
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
6+
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
7+
8+
export default function Inline_module_vars($$payload) {
9+
$$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>`;
10+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<svelte:options runes={true} />
2+
3+
<script module>
4+
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
5+
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
6+
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
7+
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
8+
</script>
9+
10+
<picture>
11+
<source srcset={__DECLARED_ASSET_0__} type="image/avif" />
12+
<source srcset={__DECLARED_ASSET_1__} type="image/webp" />
13+
<source srcset={__DECLARED_ASSET_2__} type="image/png" />
14+
<img src={__DECLARED_ASSET_3__} alt="production test" width=1440 height=1440 />
15+
</picture>

0 commit comments

Comments
 (0)