Skip to content

Commit 7fd2d86

Browse files
fix: parallelize async @consts in the template (#17165)
* fix: parallelize async `@const`s in the template This fixes #17075 by solving the TODO of #17038 to add out of order rendering for async `@const` declarations in the template. It's implemented by a new field on the component state which is set as soon as we come across an async const. All async const declarations and those after it will be added to that field, and the existing blockers mechanism is then used to line up the async work correctly. After processing a fragment a `run` command is created from the collected consts. * fix * tweak --------- Co-authored-by: Rich Harris <[email protected]>
1 parent b9c7e45 commit 7fd2d86

File tree

23 files changed

+191
-117
lines changed

23 files changed

+191
-117
lines changed

.changeset/social-taxis-tell.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: ensure async `@const` in boundary hydrates correctly

.changeset/stale-items-know.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: parallelize async `@const`s in the template

packages/svelte/src/compiler/phases/1-parse/utils/create.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ export function create_fragment(transparent = false) {
1010
nodes: [],
1111
metadata: {
1212
transparent,
13-
dynamic: false,
14-
has_await: false
13+
dynamic: false
1514
}
1615
};
1716
}

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ export function AwaitExpression(node, context) {
2626
if (context.state.expression) {
2727
context.state.expression.has_await = true;
2828

29-
if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
30-
context.state.fragment.metadata.has_await = true;
31-
}
32-
3329
suspend = true;
3430
}
3531

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export interface ComponentClientTransformState extends ClientTransformState {
5151
readonly after_update: Statement[];
5252
/** Transformed `{@const }` declarations */
5353
readonly consts: Statement[];
54+
/** Transformed async `{@const }` declarations (if any) and those coming after them */
55+
async_consts?: {
56+
id: Identifier;
57+
thunks: Expression[];
58+
};
5459
/** Transformed `let:` directives */
5560
readonly let_directives: Statement[];
5661
/** Memoized expressions */

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

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export function ConstTag(node, context) {
2424
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
2525
}
2626

27-
context.state.consts.push(b.const(declaration.id, expression));
28-
2927
context.state.transform[declaration.id.name] = { read: get_value };
3028

31-
// we need to eagerly evaluate the expression in order to hit any
32-
// 'Cannot access x before initialization' errors
33-
if (dev) {
34-
context.state.consts.push(b.stmt(b.call('$.get', declaration.id)));
35-
}
29+
add_const_declaration(
30+
context.state,
31+
declaration.id,
32+
expression,
33+
node.metadata.expression.has_await,
34+
context.state.scope.get_bindings(declaration)
35+
);
3636
} else {
3737
const identifiers = extract_identifiers(declaration.id);
3838
const tmp = b.id(context.state.scope.generate('computed_const'));
@@ -69,13 +69,13 @@ export function ConstTag(node, context) {
6969
expression = b.call('$.tag', expression, b.literal('[@const]'));
7070
}
7171

72-
context.state.consts.push(b.const(tmp, expression));
73-
74-
// we need to eagerly evaluate the expression in order to hit any
75-
// 'Cannot access x before initialization' errors
76-
if (dev) {
77-
context.state.consts.push(b.stmt(b.call('$.get', tmp)));
78-
}
72+
add_const_declaration(
73+
context.state,
74+
tmp,
75+
expression,
76+
node.metadata.expression.has_await,
77+
context.state.scope.get_bindings(declaration)
78+
);
7979

8080
for (const node of identifiers) {
8181
context.state.transform[node.name] = {
@@ -84,3 +84,39 @@ export function ConstTag(node, context) {
8484
}
8585
}
8686
}
87+
88+
/**
89+
* @param {ComponentContext['state']} state
90+
* @param {import('estree').Identifier} id
91+
* @param {import('estree').Expression} expression
92+
* @param {boolean} has_await
93+
* @param {import('#compiler').Binding[]} bindings
94+
*/
95+
function add_const_declaration(state, id, expression, has_await, bindings) {
96+
// we need to eagerly evaluate the expression in order to hit any
97+
// 'Cannot access x before initialization' errors
98+
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
99+
100+
if (has_await || state.async_consts) {
101+
const run = (state.async_consts ??= {
102+
id: b.id(state.scope.generate('promises')),
103+
thunks: []
104+
});
105+
106+
state.consts.push(b.let(id));
107+
108+
const assignment = b.assignment('=', id, expression);
109+
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
110+
111+
run.thunks.push(b.thunk(body, has_await));
112+
113+
const blocker = b.member(run.id, b.literal(run.thunks.length - 1), true);
114+
115+
for (const binding of bindings) {
116+
binding.blocker = blocker;
117+
}
118+
} else {
119+
state.consts.push(b.const(id, expression));
120+
state.consts.push(...after);
121+
}
122+
}

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

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ export function Fragment(node, context) {
4848
const is_single_child_not_needing_template =
4949
trimmed.length === 1 &&
5050
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
51-
const has_await = context.state.init !== null && (node.metadata.has_await || false);
52-
5351
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
5452

5553
/** @type {Statement[]} */
@@ -72,7 +70,8 @@ export function Fragment(node, context) {
7270
metadata: {
7371
namespace,
7472
bound_contenteditable: context.state.metadata.bound_contenteditable
75-
}
73+
},
74+
async_consts: undefined
7675
};
7776

7877
for (const node of hoisted) {
@@ -153,8 +152,8 @@ export function Fragment(node, context) {
153152

154153
body.push(...state.let_directives, ...state.consts);
155154

156-
if (has_await) {
157-
body.push(b.if(b.call('$.aborted'), b.return()));
155+
if (state.async_consts && state.async_consts.thunks.length > 0) {
156+
body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));
158157
}
159158

160159
if (is_text_first) {
@@ -177,13 +176,5 @@ export function Fragment(node, context) {
177176
body.push(close);
178177
}
179178

180-
if (has_await) {
181-
return b.block([
182-
b.stmt(
183-
b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
184-
)
185-
]);
186-
} else {
187-
return b.block(body);
188-
}
179+
return b.block(body);
189180
}

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export function SnippetBlock(node, context) {
1414
// TODO hoist where possible
1515
/** @type {(Identifier | AssignmentPattern)[]} */
1616
const args = [b.id('$$anchor')];
17-
const has_await = node.body.metadata.has_await || false;
18-
1917
/** @type {BlockStatement} */
2018
let body;
2119

@@ -78,12 +76,8 @@ export function SnippetBlock(node, context) {
7876

7977
// in dev we use a FunctionExpression (not arrow function) so we can use `arguments`
8078
let snippet = dev
81-
? b.call(
82-
'$.wrap_snippet',
83-
b.id(context.state.analysis.name),
84-
b.function(null, args, body, has_await)
85-
)
86-
: b.arrow(args, body, has_await);
79+
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
80+
: b.arrow(args, body);
8781

8882
const declaration = b.const(node.expression, snippet);
8983

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export function SvelteBoundary(node, context) {
4848
if (child.type === 'ConstTag') {
4949
has_const = true;
5050
if (!context.state.options.experimental.async) {
51-
context.visit(child, { ...context.state, consts: const_tags });
51+
context.visit(child, {
52+
...context.state,
53+
consts: const_tags,
54+
scope: context.state.scopes.get(node.fragment) ?? context.state.scope
55+
});
5256
}
5357
}
5458
}
@@ -101,7 +105,13 @@ export function SvelteBoundary(node, context) {
101105
nodes.push(child);
102106
}
103107

104-
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
108+
const block = /** @type {BlockStatement} */ (
109+
context.visit(
110+
{ ...node.fragment, nodes },
111+
// Since we're creating a new fragment the reference in scopes can't match, so we gotta attach the right scope manually
112+
{ ...context.state, scope: context.state.scopes.get(node.fragment) ?? context.state.scope }
113+
)
114+
);
105115

106116
if (!context.state.options.experimental.async) {
107117
block.body.unshift(...const_tags);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) {
105105
is_element &&
106106
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
107107
// TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally)
108-
!(node.body.metadata.has_await || node.metadata.expression.is_async())
108+
!node.metadata.expression.is_async()
109109
) {
110110
node.metadata.is_controlled = true;
111111
} else {

0 commit comments

Comments
 (0)