Skip to content

Commit 8a8e6f7

Browse files
paoloricciutibenmccannRich-Harris
authored
fix: avoid marking subtree as dynamic for inlined attributes (#14269)
* fix: avoid marking subtree as dynamic for inlined attributes * fix: i'm a silly goose 🪿 * chore: refactor `is_inlinable_expression` to accept the attribute * feat: inline dom expression too * fix: special case literals with `"` in it and fix standalone case * chore: simpler check first Co-authored-by: Ben McCann <[email protected]> * typo * add more stuff to snapshot test * simplify/speedup by doing the work once, during analysis * simplify * simplify - no reason these cases should prevent inlining * return template * name is incorrect * name is incorrect * fix escaping * no longer necessary * remove obsolete description * better concatenation * fix test * do the work at runtime * fix another thing * tidy * tidy up * simplify * simplify * fix * note to self * another * simplify * more accurate name * simplify * simplify * explain what is happening * tidy up * simplify * better inlining * update test * colocate some code * better inlining * use attribute metadata * Update packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js Co-authored-by: Ben McCann <[email protected]> * Apply suggestions from code review --------- Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 6a7146b commit 8a8e6f7

File tree

23 files changed

+313
-227
lines changed

23 files changed

+313
-227
lines changed

.changeset/spotty-sheep-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: better inlining of static attributes

packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
22
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */
33
/** @import { Context } from '../types' */
4-
import { is_capture_event, is_delegated } from '../../../../utils.js';
4+
import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js';
55
import {
66
get_attribute_chunks,
77
get_attribute_expression,
@@ -16,14 +16,23 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
1616
export function Attribute(node, context) {
1717
context.next();
1818

19+
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
20+
1921
// special case
2022
if (node.name === 'value') {
21-
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
2223
if (parent.type === 'RegularElement' && parent.name === 'option') {
2324
mark_subtree_dynamic(context.path);
2425
}
2526
}
2627

28+
if (node.name.startsWith('on')) {
29+
mark_subtree_dynamic(context.path);
30+
}
31+
32+
if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) {
33+
node.metadata.expression.can_inline = false;
34+
}
35+
2736
if (node.value !== true) {
2837
for (const chunk of get_attribute_chunks(node.value)) {
2938
if (chunk.type !== 'ExpressionTag') continue;
@@ -37,6 +46,7 @@ export function Attribute(node, context) {
3746

3847
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
3948
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
49+
node.metadata.expression.can_inline &&= chunk.metadata.expression.can_inline;
4050
}
4151

4252
if (is_event_attribute(node)) {

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export function CallExpression(node, context) {
178178
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
179179
context.state.expression.has_call = true;
180180
context.state.expression.has_state = true;
181+
context.state.expression.can_inline = false;
181182
}
182183
}
183184
}

packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
/** @import { Context } from '../types' */
33
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
44
import * as e from '../../../errors.js';
5-
import { mark_subtree_dynamic } from './shared/fragment.js';
65

76
/**
87
* @param {AST.ExpressionTag} node
@@ -15,9 +14,5 @@ export function ExpressionTag(node, context) {
1514
}
1615
}
1716

18-
// TODO ideally we wouldn't do this here, we'd just do it on encountering
19-
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
20-
mark_subtree_dynamic(context.path);
21-
2217
context.next({ ...context.state, expression: node.metadata.expression });
2318
}

packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/** @import { Expression, Identifier } from 'estree' */
2-
/** @import { EachBlock } from '#compiler' */
32
/** @import { Context } from '../types' */
43
import is_reference from 'is-reference';
54
import { should_proxy } from '../../3-transform/client/utils.js';
@@ -20,8 +19,6 @@ export function Identifier(node, context) {
2019
return;
2120
}
2221

23-
mark_subtree_dynamic(context.path);
24-
2522
// If we are using arguments outside of a function, then throw an error
2623
if (
2724
node.name === 'arguments' &&
@@ -87,6 +84,12 @@ export function Identifier(node, context) {
8784
}
8885
}
8986

87+
// no binding means global, and we can't inline e.g. `<span>{location}</span>`
88+
// because it could change between component renders. if there _is_ a
89+
// binding and it is outside module scope, the expression cannot
90+
// be inlined (TODO allow inlining in more cases - e.g. primitive consts)
91+
let can_inline = !!binding && !binding.scope.parent && binding.kind === 'normal';
92+
9093
if (binding) {
9194
if (context.state.expression) {
9295
context.state.expression.dependencies.add(binding);
@@ -122,4 +125,17 @@ export function Identifier(node, context) {
122125
w.reactive_declaration_module_script_dependency(node);
123126
}
124127
}
128+
129+
if (!can_inline && context.state.expression) {
130+
context.state.expression.can_inline = false;
131+
}
132+
133+
/**
134+
* if the identifier is part of an expression tag of an attribute we want to check if it's inlinable
135+
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
136+
* directly making the whole thing actually static.
137+
*/
138+
if (!can_inline || !context.path.find((node) => node.type === 'Attribute')) {
139+
mark_subtree_dynamic(context.path);
140+
}
125141
}

packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function MemberExpression(node, context) {
1919

2020
if (context.state.expression && !is_pure(node, context)) {
2121
context.state.expression.has_state = true;
22+
context.state.expression.can_inline = false;
2223
}
2324

2425
if (!is_safe_identifier(node, context.state.scope)) {

packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function TaggedTemplateExpression(node, context) {
1010
if (context.state.expression && !is_pure(node.tag, context)) {
1111
context.state.expression.has_call = true;
1212
context.state.expression.has_state = true;
13+
context.state.expression.can_inline = false;
1314
}
1415

1516
if (node.tag.type === 'Identifier') {

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

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
2-
/** @import { AST, Binding, SvelteNode } from '#compiler' */
2+
/** @import { Binding, SvelteNode } from '#compiler' */
33
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
44
/** @import { Analysis } from '../../types.js' */
55
/** @import { Scope } from '../../scope.js' */
6-
import * as b from '../../../utils/builders.js';
7-
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
86
import {
9-
PROPS_IS_LAZY_INITIAL,
7+
PROPS_IS_BINDABLE,
108
PROPS_IS_IMMUTABLE,
9+
PROPS_IS_LAZY_INITIAL,
1110
PROPS_IS_RUNES,
12-
PROPS_IS_UPDATED,
13-
PROPS_IS_BINDABLE
11+
PROPS_IS_UPDATED
1412
} from '../../../../constants.js';
1513
import { dev } from '../../../state.js';
14+
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
15+
import * as b from '../../../utils/builders.js';
1616
import { get_value } from './visitors/shared/declarations.js';
1717

1818
/**
@@ -311,43 +311,3 @@ 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-
}
329-
330-
/**
331-
* @param {(AST.Text | AST.ExpressionTag) | (AST.Text | AST.ExpressionTag)[]} node_or_nodes
332-
* @param {import('./types.js').ComponentClientTransformState} state
333-
*/
334-
export function is_inlinable_expression(node_or_nodes, state) {
335-
let nodes = Array.isArray(node_or_nodes) ? node_or_nodes : [node_or_nodes];
336-
let has_expression_tag = false;
337-
for (let value of nodes) {
338-
if (value.type === 'ExpressionTag') {
339-
if (value.expression.type === 'Identifier') {
340-
const binding = state.scope
341-
.owner(value.expression.name)
342-
?.declarations.get(value.expression.name);
343-
if (!can_inline_variable(binding)) {
344-
return false;
345-
}
346-
} else {
347-
return false;
348-
}
349-
has_expression_tag = true;
350-
}
351-
}
352-
return has_expression_tag;
353-
}

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, Identifier, Statement } from 'estree' */
1+
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
22
/** @import { AST, Namespace } from '#compiler' */
33
/** @import { SourceLocation } from '#shared' */
44
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -141,14 +141,14 @@ export function Fragment(node, context) {
141141
const id = b.id(context.state.scope.generate('fragment'));
142142

143143
const use_space_template =
144-
trimmed.some((node) => node.type === 'ExpressionTag') &&
145-
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
144+
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
145+
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
146146

147147
if (use_space_template) {
148148
// special case — we can use `$.text` instead of creating a unique template
149149
const id = b.id(context.state.scope.generate('text'));
150150

151-
process_children(trimmed, () => id, false, {
151+
process_children(trimmed, () => id, null, {
152152
...context,
153153
state
154154
});
@@ -158,12 +158,12 @@ export function Fragment(node, context) {
158158
} else {
159159
if (is_standalone) {
160160
// no need to create a template, we can just use the existing block's anchor
161-
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
161+
process_children(trimmed, () => b.id('$$anchor'), null, { ...context, state });
162162
} else {
163163
/** @type {(is_text: boolean) => Expression} */
164164
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
165165

166-
process_children(trimmed, expression, false, { ...context, state });
166+
process_children(trimmed, expression, null, { ...context, state });
167167

168168
let flags = TEMPLATE_FRAGMENT;
169169

@@ -212,12 +212,34 @@ function join_template(items) {
212212
let quasi = b.quasi('');
213213
const template = b.template([quasi], []);
214214

215+
/**
216+
* @param {Expression} expression
217+
*/
218+
function push(expression) {
219+
if (expression.type === 'TemplateLiteral') {
220+
for (let i = 0; i < expression.expressions.length; i += 1) {
221+
const q = expression.quasis[i];
222+
const e = expression.expressions[i];
223+
224+
quasi.value.cooked += /** @type {string} */ (q.value.cooked);
225+
push(e);
226+
}
227+
228+
const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
229+
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
230+
} else if (expression.type === 'Literal') {
231+
/** @type {string} */ (quasi.value.cooked) += expression.value;
232+
} else {
233+
template.expressions.push(expression);
234+
template.quasis.push((quasi = b.quasi('')));
235+
}
236+
}
237+
215238
for (const item of items) {
216239
if (typeof item === 'string') {
217240
quasi.value.cooked += item;
218241
} else {
219-
template.expressions.push(item);
220-
template.quasis.push((quasi = b.quasi('')));
242+
push(item);
221243
}
222244
}
223245

0 commit comments

Comments
 (0)