Skip to content

Commit b8d1513

Browse files
fix: stable attachments (#15961)
* fix: stable attachments * WIP * WIP * WIP * WIP * WIP * WIP * fix * unused * unused * unused * changeset * ugh * remove nonsensical comment, this is unused in spread * rename * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js Co-authored-by: Simon H <[email protected]> --------- Co-authored-by: Simon H <[email protected]>
1 parent 81a34aa commit b8d1513

File tree

11 files changed

+173
-109
lines changed

11 files changed

+173
-109
lines changed

.changeset/soft-jobs-sip.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+
fix: avoid rerunning attachments when unrelated spread attributes change

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

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { build_getter } from '../utils.js';
1717
import {
1818
get_attribute_name,
1919
build_attribute_value,
20-
build_set_attributes,
20+
build_attribute_effect,
2121
build_set_class,
2222
build_set_style
2323
} from './shared/element.js';
@@ -201,37 +201,7 @@ export function RegularElement(node, context) {
201201
const node_id = context.state.node;
202202

203203
if (has_spread) {
204-
const attributes_id = b.id(context.state.scope.generate('attributes'));
205-
206-
build_set_attributes(
207-
attributes,
208-
class_directives,
209-
style_directives,
210-
context,
211-
node,
212-
node_id,
213-
attributes_id
214-
);
215-
216-
// If value binding exists, that one takes care of calling $.init_select
217-
if (node.name === 'select' && !bindings.has('value')) {
218-
context.state.init.push(
219-
b.stmt(b.call('$.init_select', node_id, b.thunk(b.member(attributes_id, 'value'))))
220-
);
221-
222-
context.state.update.push(
223-
b.if(
224-
b.binary('in', b.literal('value'), attributes_id),
225-
b.block([
226-
// This ensures a one-way street to the DOM in case it's <select {value}>
227-
// and not <select bind:value>. We need it in addition to $.init_select
228-
// because the select value is not reflected as an attribute, so the
229-
// mutation observer wouldn't notice.
230-
b.stmt(b.call('$.select_option', node_id, b.member(attributes_id, 'value')))
231-
])
232-
)
233-
);
234-
}
204+
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
235205
} else {
236206
/** If true, needs `__value` for inputs */
237207
const needs_special_value_handling =
@@ -290,7 +260,8 @@ export function RegularElement(node, context) {
290260
const { value, has_state } = build_attribute_value(
291261
attribute.value,
292262
context,
293-
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
263+
(value, metadata) =>
264+
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
294265
);
295266

296267
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@@ -482,10 +453,11 @@ function setup_select_synchronization(value_binding, context) {
482453

483454
/**
484455
* @param {AST.ClassDirective[]} class_directives
456+
* @param {Expression[]} expressions
485457
* @param {ComponentContext} context
486458
* @return {ObjectExpression | Identifier}
487459
*/
488-
export function build_class_directives_object(class_directives, context) {
460+
export function build_class_directives_object(class_directives, expressions, context) {
489461
let properties = [];
490462
let has_call_or_state = false;
491463

@@ -497,15 +469,16 @@ export function build_class_directives_object(class_directives, context) {
497469

498470
const directives = b.object(properties);
499471

500-
return has_call_or_state ? get_expression_id(context.state, directives) : directives;
472+
return has_call_or_state ? get_expression_id(expressions, directives) : directives;
501473
}
502474

503475
/**
504476
* @param {AST.StyleDirective[]} style_directives
477+
* @param {Expression[]} expressions
505478
* @param {ComponentContext} context
506479
* @return {ObjectExpression | ArrayExpression}}
507480
*/
508-
export function build_style_directives_object(style_directives, context) {
481+
export function build_style_directives_object(style_directives, expressions, context) {
509482
let normal_properties = [];
510483
let important_properties = [];
511484

@@ -514,7 +487,7 @@ export function build_style_directives_object(style_directives, context) {
514487
directive.value === true
515488
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
516489
: build_attribute_value(directive.value, context, (value, metadata) =>
517-
metadata.has_call ? get_expression_id(context.state, value) : value
490+
metadata.has_call ? get_expression_id(expressions, value) : value
518491
).value;
519492
const property = b.init(directive.name, expression);
520493

@@ -653,7 +626,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
653626
? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately
654627
is_select_with_value
655628
? memoize_expression(state, value)
656-
: get_expression_id(state, value)
629+
: get_expression_id(state.expressions, value)
657630
: value
658631
);
659632

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { dev, locator } from '../../../../state.js';
55
import { is_text_attribute } from '../../../../utils/ast.js';
66
import * as b from '#compiler/builders';
77
import { determine_namespace_for_children } from '../../utils.js';
8-
import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
8+
import {
9+
build_attribute_value,
10+
build_attribute_effect,
11+
build_set_class
12+
} from './shared/element.js';
913
import { build_render_statement } from './shared/utils.js';
1014

1115
/**
@@ -80,18 +84,15 @@ export function SvelteElement(node, context) {
8084
) {
8185
build_set_class(node, element_id, attributes[0], class_directives, inner_context, false);
8286
} else if (attributes.length) {
83-
const attributes_id = b.id(context.state.scope.generate('attributes'));
84-
8587
// Always use spread because we don't know whether the element is a custom element or not,
8688
// therefore we need to do the "how to set an attribute" logic at runtime.
87-
build_set_attributes(
89+
build_attribute_effect(
8890
attributes,
8991
class_directives,
9092
style_directives,
9193
inner_context,
9294
node,
93-
element_id,
94-
attributes_id
95+
element_id
9596
);
9697
}
9798

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

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,30 @@ import { build_template_chunk, get_expression_id } from './utils.js';
1616
* @param {ComponentContext} context
1717
* @param {AST.RegularElement | AST.SvelteElement} element
1818
* @param {Identifier} element_id
19-
* @param {Identifier} attributes_id
2019
*/
21-
export function build_set_attributes(
20+
export function build_attribute_effect(
2221
attributes,
2322
class_directives,
2423
style_directives,
2524
context,
2625
element,
27-
element_id,
28-
attributes_id
26+
element_id
2927
) {
30-
let is_dynamic = false;
31-
3228
/** @type {ObjectExpression['properties']} */
3329
const values = [];
3430

31+
/** @type {Expression[]} */
32+
const expressions = [];
33+
34+
/** @param {Expression} value */
35+
function memoize(value) {
36+
return b.id(`$${expressions.push(value) - 1}`);
37+
}
38+
3539
for (const attribute of attributes) {
3640
if (attribute.type === 'Attribute') {
37-
const { value, has_state } = build_attribute_value(
38-
attribute.value,
39-
context,
40-
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
41+
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
42+
metadata.has_call ? memoize(value) : value
4143
);
4244

4345
if (
@@ -51,16 +53,11 @@ export function build_set_attributes(
5153
} else {
5254
values.push(b.init(attribute.name, value));
5355
}
54-
55-
is_dynamic ||= has_state;
5656
} else {
57-
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
58-
is_dynamic = true;
59-
6057
let value = /** @type {Expression} */ (context.visit(attribute));
6158

6259
if (attribute.metadata.expression.has_call) {
63-
value = get_expression_id(context.state, value);
60+
value = memoize(value);
6461
}
6562

6663
values.push(b.spread(value));
@@ -72,44 +69,38 @@ export function build_set_attributes(
7269
b.prop(
7370
'init',
7471
b.array([b.id('$.CLASS')]),
75-
build_class_directives_object(class_directives, context)
72+
build_class_directives_object(class_directives, expressions, context)
7673
)
7774
);
78-
79-
is_dynamic ||=
80-
class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
8175
}
8276

8377
if (style_directives.length) {
8478
values.push(
8579
b.prop(
8680
'init',
8781
b.array([b.id('$.STYLE')]),
88-
build_style_directives_object(style_directives, context)
82+
build_style_directives_object(style_directives, expressions, context)
8983
)
9084
);
91-
92-
is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state);
9385
}
9486

95-
const call = b.call(
96-
'$.set_attributes',
97-
element_id,
98-
is_dynamic ? attributes_id : b.null,
99-
b.object(values),
100-
element.metadata.scoped &&
101-
context.state.analysis.css.hash !== '' &&
102-
b.literal(context.state.analysis.css.hash),
103-
is_ignored(element, 'hydration_attribute_changed') && b.true
87+
context.state.init.push(
88+
b.stmt(
89+
b.call(
90+
'$.attribute_effect',
91+
element_id,
92+
b.arrow(
93+
expressions.map((_, i) => b.id(`$${i}`)),
94+
b.object(values)
95+
),
96+
expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))),
97+
element.metadata.scoped &&
98+
context.state.analysis.css.hash !== '' &&
99+
b.literal(context.state.analysis.css.hash),
100+
is_ignored(element, 'hydration_attribute_changed') && b.true
101+
)
102+
)
104103
);
105-
106-
if (is_dynamic) {
107-
context.state.init.push(b.let(attributes_id));
108-
const update = b.stmt(b.assignment('=', attributes_id, call));
109-
context.state.update.push(update);
110-
} else {
111-
context.state.init.push(b.stmt(call));
112-
}
113104
}
114105

115106
/**
@@ -167,7 +158,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
167158
value = b.call('$.clsx', value);
168159
}
169160

170-
return metadata.has_call ? get_expression_id(context.state, value) : value;
161+
return metadata.has_call ? get_expression_id(context.state.expressions, value) : value;
171162
});
172163

173164
/** @type {Identifier | undefined} */
@@ -180,7 +171,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
180171
let next;
181172

182173
if (class_directives.length) {
183-
next = build_class_directives_object(class_directives, context);
174+
next = build_class_directives_object(class_directives, context.state.expressions, context);
184175
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
185176

186177
if (has_state) {
@@ -235,7 +226,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
235226
*/
236227
export function build_set_style(node_id, attribute, style_directives, context) {
237228
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
238-
metadata.has_call ? get_expression_id(context.state, value) : value
229+
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
239230
);
240231

241232
/** @type {Identifier | undefined} */
@@ -248,7 +239,7 @@ export function build_set_style(node_id, attribute, style_directives, context) {
248239
let next;
249240

250241
if (style_directives.length) {
251-
next = build_style_directives_object(style_directives, context);
242+
next = build_style_directives_object(style_directives, context.state.expressions, context);
252243
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
253244

254245
if (has_state) {

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ export function memoize_expression(state, value) {
2121
}
2222

2323
/**
24-
*
25-
* @param {ComponentClientTransformState} state
24+
* Pushes `value` into `expressions` and returns a new id
25+
* @param {Expression[]} expressions
2626
* @param {Expression} value
2727
*/
28-
export function get_expression_id(state, value) {
29-
return b.id(`$${state.expressions.push(value) - 1}`);
28+
export function get_expression_id(expressions, value) {
29+
return b.id(`$${expressions.push(value) - 1}`);
3030
}
3131

3232
/**
@@ -40,7 +40,8 @@ export function build_template_chunk(
4040
values,
4141
visit,
4242
state,
43-
memoize = (value, metadata) => (metadata.has_call ? get_expression_id(state, value) : value)
43+
memoize = (value, metadata) =>
44+
metadata.has_call ? get_expression_id(state.expressions, value) : value
4445
) {
4546
/** @type {Expression[]} */
4647
const expressions = [];

0 commit comments

Comments
 (0)