Skip to content

Commit b39331e

Browse files
committed
perf: inline module variables into template
1 parent a24ea0a commit b39331e

File tree

18 files changed

+148
-63
lines changed

18 files changed

+148
-63
lines changed

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+
quasi: string[];
59+
expressions: Expression[];
60+
};
5861
readonly locations: SourceLocation[];
5962
readonly metadata: {
6063
namespace: Namespace;

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,38 @@ 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+
* @param {import('./types.js').ComponentClientTransformState} state
317+
* @param {string} quasi_to_add
318+
*/
319+
export function push_template_quasi(state, quasi_to_add) {
320+
const { quasi } = state.template;
321+
if (quasi.length === 0) {
322+
quasi.push(quasi_to_add);
323+
return;
324+
}
325+
quasi[quasi.length - 1] = quasi[quasi.length - 1].concat(quasi_to_add);
326+
}
327+
328+
/**
329+
* @param {import('./types.js').ComponentClientTransformState} state
330+
* @param {import('estree').Expression} expression_to_add
331+
*/
332+
export function push_template_expression(state, expression_to_add) {
333+
const { expressions, quasi } = state.template;
334+
if (quasi.length === 0) {
335+
quasi.push('');
336+
}
337+
expressions.push(expression_to_add);
338+
quasi.push('');
339+
}
340+
341+
/**
342+
* Whether a variable can be referenced directly from template string.
343+
* @param {import('#compiler').Binding | undefined} binding
344+
* @returns {boolean}
345+
*/
346+
export function can_inline_variable(binding) {
347+
return !!binding && !binding.scope.parent;
348+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
/** @import { AwaitBlock } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '../../../../utils/builders.js';
5-
import { create_derived_block_argument } from '../utils.js';
5+
import { create_derived_block_argument, push_template_quasi } from '../utils.js';
66

77
/**
88
* @param {AwaitBlock} node
99
* @param {ComponentContext} context
1010
*/
1111
export function AwaitBlock(node, context) {
12-
context.state.template.push('<!>');
12+
push_template_quasi(context.state, '<!>');
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)));
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/** @import { Comment } from '#compiler' */
22
/** @import { ComponentContext } from '../types' */
33

4+
import { push_template_quasi } from '../utils.js';
5+
46
/**
57
* @param {Comment} node
68
* @param {ComponentContext} context
79
*/
810
export function Comment(node, context) {
911
// 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}-->`);
12+
push_template_quasi(context.state, `<!--${node.data}-->`);
1113
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { dev } from '../../../../state.js';
1313
import { extract_paths, object } from '../../../../utils/ast.js';
1414
import * as b from '../../../../utils/builders.js';
15-
import { build_getter } from '../utils.js';
15+
import { build_getter, push_template_quasi } from '../utils.js';
1616
import { get_value } from './shared/declarations.js';
1717

1818
/**
@@ -32,7 +32,7 @@ export function EachBlock(node, context) {
3232
);
3333

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

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

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ export function Fragment(node, context) {
6464
init: [],
6565
update: [],
6666
after_update: [],
67-
template: [],
67+
template: {
68+
quasi: [],
69+
expressions: []
70+
},
6871
locations: [],
6972
transform: { ...context.state.transform },
7073
metadata: {
@@ -115,7 +118,12 @@ export function Fragment(node, context) {
115118
});
116119

117120
/** @type {Expression[]} */
118-
const args = [b.template([b.quasi(state.template.join(''), true)], [])];
121+
const args = [
122+
b.template(
123+
state.template.quasi.map((q) => b.quasi(q, true)),
124+
state.template.expressions
125+
)
126+
];
119127

120128
if (state.metadata.context.template_needs_import_node) {
121129
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
@@ -170,12 +178,15 @@ export function Fragment(node, context) {
170178
flags |= TEMPLATE_USE_IMPORT_NODE;
171179
}
172180

173-
if (state.template.length === 1 && state.template[0] === '<!>') {
181+
if (state.template.quasi.length === 1 && state.template.quasi[0] === '<!>') {
174182
// special case — we can use `$.comment` instead of creating a unique template
175183
body.push(b.var(id, b.call('$.comment')));
176184
} else {
177185
add_template(template_name, [
178-
b.template([b.quasi(state.template.join(''), true)], []),
186+
b.template(
187+
state.template.quasi.map((q) => b.quasi(q, true)),
188+
state.template.expressions
189+
),
179190
b.literal(flags)
180191
]);
181192

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
/** @import { ComponentContext } from '../types' */
44
import { is_ignored } from '../../../../state.js';
55
import * as b from '../../../../utils/builders.js';
6+
import { push_template_quasi } from '../utils.js';
67

78
/**
89
* @param {HtmlTag} node
910
* @param {ComponentContext} context
1011
*/
1112
export function HtmlTag(node, context) {
12-
context.state.template.push('<!>');
13+
push_template_quasi(context.state, '<!>');
1314

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

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
/** @import { IfBlock } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '../../../../utils/builders.js';
5+
import { push_template_quasi } from '../utils.js';
56

67
/**
78
* @param {IfBlock} node
89
* @param {ComponentContext} context
910
*/
1011
export function IfBlock(node, context) {
11-
context.state.template.push('<!>');
12+
push_template_quasi(context.state, '<!>');
1213

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

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
/** @import { KeyBlock } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '../../../../utils/builders.js';
5+
import { push_template_quasi } from '../utils.js';
56

67
/**
78
* @param {KeyBlock} node
89
* @param {ComponentContext} context
910
*/
1011
export function KeyBlock(node, context) {
11-
context.state.template.push('<!>');
12+
push_template_quasi(context.state, '<!>');
1213

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

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

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ 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 {
23+
build_getter,
24+
can_inline_variable,
25+
push_template_expression,
26+
push_template_quasi
27+
} from '../utils.js';
2328
import {
2429
get_attribute_name,
2530
build_attribute_value,
@@ -53,7 +58,7 @@ export function RegularElement(node, context) {
5358
}
5459

5560
if (node.name === 'noscript') {
56-
context.state.template.push('<noscript></noscript>');
61+
push_template_quasi(context.state, '<noscript></noscript>');
5762
return;
5863
}
5964

@@ -67,7 +72,7 @@ export function RegularElement(node, context) {
6772
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
6873
};
6974

70-
context.state.template.push(`<${node.name}`);
75+
push_template_quasi(context.state, `<${node.name}`);
7176

7277
/** @type {Array<Attribute | SpreadAttribute>} */
7378
const attributes = [];
@@ -241,7 +246,8 @@ export function RegularElement(node, context) {
241246
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
242247

243248
if (name !== 'class' || value) {
244-
context.state.template.push(
249+
push_template_quasi(
250+
context.state,
245251
` ${attribute.name}${
246252
is_boolean_attribute(name) && value === true
247253
? ''
@@ -278,7 +284,7 @@ export function RegularElement(node, context) {
278284
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
279285
}
280286

281-
context.state.template.push('>');
287+
push_template_quasi(context.state, '>');
282288

283289
/** @type {SourceLocation[]} */
284290
const child_locations = [];
@@ -377,7 +383,7 @@ export function RegularElement(node, context) {
377383
}
378384

379385
if (!is_void(node.name)) {
380-
context.state.template.push(`</${node.name}>`);
386+
push_template_quasi(context.state, `</${node.name}>`);
381387
}
382388
}
383389

@@ -465,7 +471,7 @@ function build_element_spread_attributes(
465471
value.type === 'Literal' &&
466472
context.state.metadata.namespace === 'html'
467473
) {
468-
context.state.template.push(` is="${escape_html(value.value, true)}"`);
474+
push_template_quasi(context.state, ` is="${escape_html(value.value, true)}"`);
469475
continue;
470476
}
471477

@@ -607,6 +613,13 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
607613
);
608614
}
609615

616+
const { has_expression_tag, can_inline } =
617+
attribute.value === true
618+
? { has_expression_tag: false, can_inline: true }
619+
: can_inline_all_nodes(
620+
Array.isArray(attribute.value) ? attribute.value : [attribute.value],
621+
context.state
622+
);
610623
if (attribute.metadata.expression.has_state) {
611624
if (has_call) {
612625
state.init.push(build_update(update));
@@ -615,11 +628,40 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
615628
}
616629
return true;
617630
} else {
618-
state.init.push(update);
631+
if (has_expression_tag && can_inline) {
632+
push_template_quasi(context.state, ` ${name}="`);
633+
push_template_expression(context.state, value);
634+
push_template_quasi(context.state, '"');
635+
} else {
636+
state.init.push(update);
637+
}
619638
return false;
620639
}
621640
}
622641

642+
/**
643+
* @param {(import('#compiler').Text | import('#compiler').ExpressionTag)[]} nodes
644+
* @param {import('../types.js').ComponentClientTransformState} state
645+
*/
646+
function can_inline_all_nodes(nodes, state) {
647+
let can_inline = true;
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+
can_inline &&= can_inline_variable(binding);
656+
} else {
657+
can_inline = false;
658+
}
659+
has_expression_tag = true;
660+
}
661+
}
662+
return { can_inline, has_expression_tag };
663+
}
664+
623665
/**
624666
* Like `build_element_attribute_update_assignment` but without any special attribute treatment.
625667
* @param {Identifier} node_id

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
/** @import { ComponentContext } from '../types' */
44
import { unwrap_optional } from '../../../../utils/ast.js';
55
import * as b from '../../../../utils/builders.js';
6+
import { push_template_quasi } from '../utils.js';
67

78
/**
89
* @param {RenderTag} node
910
* @param {ComponentContext} context
1011
*/
1112
export function RenderTag(node, context) {
12-
context.state.template.push('<!>');
13+
push_template_quasi(context.state, '<!>');
1314
const callee = unwrap_optional(node.expression).callee;
1415
const raw_args = unwrap_optional(node.expression).arguments;
1516

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/** @import { SlotElement } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '../../../../utils/builders.js';
5+
import { push_template_quasi } from '../utils.js';
56
import { build_attribute_value } from './shared/element.js';
67

78
/**
@@ -10,7 +11,7 @@ import { build_attribute_value } from './shared/element.js';
1011
*/
1112
export function SlotElement(node, context) {
1213
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
13-
context.state.template.push('<!>');
14+
push_template_quasi(context.state, '<!>');
1415

1516
/** @type {Property[]} */
1617
const props = [];

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '../../../../utils/ast.js';
1212
import * as b from '../../../../utils/builders.js';
1313
import { determine_namespace_for_children } from '../../utils.js';
14+
import { push_template_quasi } from '../utils.js';
1415
import {
1516
build_attribute_value,
1617
build_class_directives,
@@ -23,7 +24,7 @@ import { build_render_statement, build_update } from './shared/utils.js';
2324
* @param {ComponentContext} context
2425
*/
2526
export function SvelteElement(node, context) {
26-
context.state.template.push(`<!>`);
27+
push_template_quasi(context.state, `<!>`);
2728

2829
/** @type {Array<Attribute | SpreadAttribute>} */
2930
const attributes = [];

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { dev, is_ignored } from '../../../../../state.js';
55
import { get_attribute_chunks } from '../../../../../utils/ast.js';
66
import * as b from '../../../../../utils/builders.js';
7-
import { create_derived } from '../../utils.js';
7+
import { create_derived, push_template_quasi } from '../../utils.js';
88
import { build_bind_this, validate_binding } from '../shared/utils.js';
99
import { build_attribute_value } from '../shared/element.js';
1010
import { build_event_handler } from './events.js';
@@ -357,7 +357,8 @@ 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+
push_template_quasi(
361+
context.state,
361362
context.state.metadata.namespace === 'svg'
362363
? '<g><!></g>'
363364
: '<div style="display: contents"><!></div>'
@@ -369,7 +370,7 @@ export function build_component(node, component_name, context, anchor = context.
369370
b.stmt(b.call('$.reset', anchor))
370371
);
371372
} else {
372-
context.state.template.push('<!>');
373+
push_template_quasi(context.state, '<!>');
373374
statements.push(b.stmt(fn(anchor)));
374375
}
375376

0 commit comments

Comments
 (0)