Skip to content

Commit 700029b

Browse files
authored
fix: more efficient template effect grouping (#15050)
* WIP * drive-by * group attribute updates * fix * more * unused * more * WIP * unused * simplify * simplify * simplify * more * unused * unused * more * tweak * update how class/style directives are handled * more * more * simplify * changeset
1 parent a9d1f46 commit 700029b

File tree

24 files changed

+227
-199
lines changed

24 files changed

+227
-199
lines changed

.changeset/cyan-games-cheat.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: more efficient template effect grouping

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

-3
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,6 @@ export function Attribute(node, context) {
6161
) {
6262
continue;
6363
}
64-
65-
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
66-
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
6764
}
6865

6966
if (is_event_attribute(node)) {

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function visit_function(node, context) {
1515

1616
context.next({
1717
...context.state,
18-
function_depth: context.state.function_depth + 1
18+
function_depth: context.state.function_depth + 1,
19+
expression: null
1920
});
2021
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ export function client_component(analysis, options) {
169169
module_level_snippets: [],
170170

171171
// these are set inside the `Fragment` visitor, and cannot be used until then
172-
before_init: /** @type {any} */ (null),
173172
init: /** @type {any} */ (null),
174173
update: /** @type {any} */ (null),
174+
expressions: /** @type {any} */ (null),
175175
after_update: /** @type {any} */ (null),
176176
template: /** @type {any} */ (null),
177177
locations: /** @type {any} */ (null)

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ export interface ComponentClientTransformState extends ClientTransformState {
4646
readonly events: Set<string>;
4747
readonly is_instance: boolean;
4848

49-
/** Stuff that happens before the render effect(s) */
50-
readonly before_init: Statement[];
5149
/** Stuff that happens before the render effect(s) */
5250
readonly init: Statement[];
5351
/** Stuff that happens inside the render effect */
5452
readonly update: Statement[];
5553
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
5654
readonly after_update: Statement[];
55+
/** Expressions used inside the render effect */
56+
readonly expressions: Expression[];
5757
/** The HTML template string */
5858
readonly template: Array<string | Expression>;
5959
readonly locations: SourceLocation[];

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

+7-14
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ export function Fragment(node, context) {
6161
/** @type {ComponentClientTransformState} */
6262
const state = {
6363
...context.state,
64-
before_init: [],
6564
init: [],
6665
update: [],
66+
expressions: [],
6767
after_update: [],
6868
template: [],
6969
locations: [],
@@ -124,18 +124,13 @@ export function Fragment(node, context) {
124124

125125
add_template(template_name, args);
126126

127-
body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
127+
body.push(b.var(id, b.call(template_name)));
128128
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
129129
} else if (is_single_child_not_needing_template) {
130130
context.visit(trimmed[0], state);
131-
body.push(...state.before_init, ...state.init);
132131
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
133132
const id = b.id(context.state.scope.generate('text'));
134-
body.push(
135-
b.var(id, b.call('$.text', b.literal(trimmed[0].data))),
136-
...state.before_init,
137-
...state.init
138-
);
133+
body.push(b.var(id, b.call('$.text', b.literal(trimmed[0].data))));
139134
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
140135
} else if (trimmed.length > 0) {
141136
const id = b.id(context.state.scope.generate('fragment'));
@@ -153,7 +148,7 @@ export function Fragment(node, context) {
153148
state
154149
});
155150

156-
body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init);
151+
body.push(b.var(id, b.call('$.text')));
157152
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
158153
} else {
159154
if (is_standalone) {
@@ -182,15 +177,13 @@ export function Fragment(node, context) {
182177

183178
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
184179
}
185-
186-
body.push(...state.before_init, ...state.init);
187180
}
188-
} else {
189-
body.push(...state.before_init, ...state.init);
190181
}
191182

183+
body.push(...state.init);
184+
192185
if (state.update.length > 0) {
193-
body.push(build_render_statement(state.update));
186+
body.push(build_render_statement(state));
194187
}
195188

196189
body.push(...state.after_update);

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

+20-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
1+
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { SourceLocation } from '#shared' */
44
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@@ -16,7 +16,7 @@ import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'
1616
import * as b from '../../../../utils/builders.js';
1717
import { is_custom_element_node } from '../../../nodes.js';
1818
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
19-
import { build_getter, create_derived } from '../utils.js';
19+
import { build_getter } from '../utils.js';
2020
import {
2121
get_attribute_name,
2222
build_attribute_value,
@@ -28,8 +28,8 @@ import { process_children } from './shared/fragment.js';
2828
import {
2929
build_render_statement,
3030
build_template_chunk,
31-
build_update,
32-
build_update_assignment
31+
build_update_assignment,
32+
get_expression_id
3333
} from './shared/utils.js';
3434
import { visit_event_attribute } from './shared/events.js';
3535

@@ -409,7 +409,7 @@ export function RegularElement(node, context) {
409409
b.block([
410410
...child_state.init,
411411
...element_state.init,
412-
child_state.update.length > 0 ? build_render_statement(child_state.update) : b.empty,
412+
child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,
413413
...child_state.after_update,
414414
...element_state.after_update
415415
])
@@ -536,7 +536,10 @@ function build_element_attribute_update_assignment(
536536
const name = get_attribute_name(element, attribute);
537537
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
538538
const is_mathml = context.state.metadata.namespace === 'mathml';
539-
let { has_call, value } = build_attribute_value(attribute.value, context);
539+
540+
let { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
541+
get_expression_id(state, value)
542+
);
540543

541544
if (name === 'autofocus') {
542545
state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
@@ -557,15 +560,6 @@ function build_element_attribute_update_assignment(
557560
value = b.call('$.clsx', value);
558561
}
559562

560-
if (attribute.metadata.expression.has_state && has_call) {
561-
// ensure we're not creating a separate template effect for this so that
562-
// potential class directives are added to the same effect and therefore always apply
563-
const id = b.id(state.scope.generate('class_derived'));
564-
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
565-
value = b.call('$.get', id);
566-
has_call = false;
567-
}
568-
569563
update = b.stmt(
570564
b.call(
571565
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
@@ -605,14 +599,6 @@ function build_element_attribute_update_assignment(
605599
} else if (is_dom_property(name)) {
606600
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
607601
} else {
608-
if (name === 'style' && attribute.metadata.expression.has_state && has_call) {
609-
// ensure we're not creating a separate template effect for this so that
610-
// potential style directives are added to the same effect and therefore always apply
611-
const id = b.id(state.scope.generate('style_derived'));
612-
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
613-
value = b.call('$.get', id);
614-
has_call = false;
615-
}
616602
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
617603
update = b.stmt(
618604
b.call(
@@ -625,12 +611,8 @@ function build_element_attribute_update_assignment(
625611
);
626612
}
627613

628-
if (attribute.metadata.expression.has_state) {
629-
if (has_call) {
630-
state.init.push(build_update(update));
631-
} else {
632-
state.update.push(update);
633-
}
614+
if (has_state) {
615+
state.update.push(update);
634616
return true;
635617
} else {
636618
state.init.push(update);
@@ -648,7 +630,7 @@ function build_element_attribute_update_assignment(
648630
function build_custom_element_attribute_update_assignment(node_id, attribute, context) {
649631
const state = context.state;
650632
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
651-
let { has_call, value } = build_attribute_value(attribute.value, context);
633+
let { value, has_state } = build_attribute_value(attribute.value, context);
652634

653635
// We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes
654636
if (name === 'class' && attribute.metadata.needs_clsx) {
@@ -660,12 +642,10 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
660642

661643
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
662644

663-
if (attribute.metadata.expression.has_state) {
664-
if (has_call) {
665-
state.init.push(build_update(update));
666-
} else {
667-
state.update.push(update);
668-
}
645+
if (has_state) {
646+
// this is different from other updates — it doesn't get grouped,
647+
// because set_custom_element_data may not be idempotent
648+
state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression))));
669649
return true;
670650
} else {
671651
state.init.push(update);
@@ -685,7 +665,9 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
685665
*/
686666
function build_element_special_value_attribute(element, node_id, attribute, context) {
687667
const state = context.state;
688-
const { value } = build_attribute_value(attribute.value, context);
668+
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
669+
get_expression_id(state, value)
670+
);
689671

690672
const inner_assignment = b.assignment(
691673
'=',
@@ -719,7 +701,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
719701
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
720702
}
721703

722-
if (attribute.metadata.expression.has_state) {
704+
if (has_state) {
723705
const id = state.scope.generate(`${node_id.name}_value`);
724706
build_update_assignment(
725707
state,

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '../../../../utils/builders.js';
55
import { build_attribute_value } from './shared/element.js';
6+
import { memoize_expression } from './shared/utils.js';
67

78
/**
89
* @param {AST.SlotElement} node
@@ -29,13 +30,15 @@ export function SlotElement(node, context) {
2930
if (attribute.type === 'SpreadAttribute') {
3031
spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute))));
3132
} else if (attribute.type === 'Attribute') {
32-
const { value } = build_attribute_value(attribute.value, context);
33+
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
34+
memoize_expression(context.state, value)
35+
);
3336

3437
if (attribute.name === 'name') {
3538
name = /** @type {Literal} */ (value);
3639
is_default = false;
3740
} else if (attribute.name !== 'slot') {
38-
if (attribute.metadata.expression.has_state) {
41+
if (has_state) {
3942
props.push(b.get(attribute.name, [b.return(value)]));
4043
} else {
4144
props.push(b.init(attribute.name, value));

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function SvelteBoundary(node, context) {
2323

2424
const expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
2525

26-
if (attribute.metadata.expression.has_state) {
26+
if (chunk.metadata.expression.has_state) {
2727
props.properties.push(b.get(attribute.name, [b.return(expression)]));
2828
} else {
2929
props.properties.push(b.init(attribute.name, expression));

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

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, ObjectExpression, Statement } from 'estree' */
1+
/** @import { BlockStatement, Expression, ExpressionStatement, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
4-
import { dev, is_ignored, locator } from '../../../../state.js';
5-
import {
6-
get_attribute_expression,
7-
is_event_attribute,
8-
is_text_attribute
9-
} from '../../../../utils/ast.js';
4+
import { dev, locator } from '../../../../state.js';
5+
import { is_text_attribute } from '../../../../utils/ast.js';
106
import * as b from '../../../../utils/builders.js';
117
import { determine_namespace_for_children } from '../../utils.js';
128
import {
@@ -15,7 +11,7 @@ import {
1511
build_set_attributes,
1612
build_style_directives
1713
} from './shared/element.js';
18-
import { build_render_statement, build_update } from './shared/utils.js';
14+
import { build_render_statement } from './shared/utils.js';
1915

2016
/**
2117
* @param {AST.SvelteElement} node
@@ -49,9 +45,9 @@ export function SvelteElement(node, context) {
4945
state: {
5046
...context.state,
5147
node: element_id,
52-
before_init: [],
5348
init: [],
5449
update: [],
50+
expressions: [],
5551
after_update: []
5652
}
5753
};
@@ -123,7 +119,7 @@ export function SvelteElement(node, context) {
123119
/** @type {Statement[]} */
124120
const inner = inner_context.state.init;
125121
if (inner_context.state.update.length > 0) {
126-
inner.push(build_render_statement(inner_context.state.update));
122+
inner.push(build_render_statement(inner_context.state));
127123
}
128124
inner.push(...inner_context.state.after_update);
129125
inner.push(

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { dev, is_ignored } from '../../../../../state.js';
55
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
66
import * as b from '../../../../../utils/builders.js';
77
import { create_derived } from '../../utils.js';
8-
import { build_bind_this, validate_binding } from '../shared/utils.js';
8+
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
99
import { build_attribute_value } from '../shared/element.js';
1010
import { build_event_handler } from './events.js';
1111
import { determine_slot } from '../../../../../utils/slot.js';
@@ -132,7 +132,13 @@ export function build_component(node, component_name, context, anchor = context.
132132
} else if (attribute.type === 'Attribute') {
133133
if (attribute.name.startsWith('--')) {
134134
custom_css_props.push(
135-
b.init(attribute.name, build_attribute_value(attribute.value, context).value)
135+
b.init(
136+
attribute.name,
137+
build_attribute_value(attribute.value, context, (value) =>
138+
// TODO put the derived in the local block
139+
memoize_expression(context.state, value)
140+
).value
141+
)
136142
);
137143
continue;
138144
}
@@ -145,9 +151,11 @@ export function build_component(node, component_name, context, anchor = context.
145151
has_children_prop = true;
146152
}
147153

148-
const { value } = build_attribute_value(attribute.value, context);
154+
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
155+
memoize_expression(context.state, value)
156+
);
149157

150-
if (attribute.metadata.expression.has_state) {
158+
if (has_state) {
151159
let arg = value;
152160

153161
// When we have a non-simple computation, anything other than an Identifier or Member expression,

0 commit comments

Comments
 (0)