Skip to content

Commit 1126ef3

Browse files
feat: out of order rendering (#17038)
* WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * revert * note to self * unused * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * deprecate * update tests * lint * lint * WIP * WIP * fix * WIP * unused * deopt to ensure state is ready * fix * DRY * reduce diff * reduce diff * reduce diff * handle blocked attributes * WIP * pre-transform * tidy up * fix * WIP * WIP * fix: handle `<svelte:head>` rendered asynchronously * fix tests * fix * delay resolve * Revert "fix" This reverts commit 2e56cd7. * add error * simplify/fix hydration restoration * fix * use $state.eager mechanism for $effect.pending - way simpler and more robust * disable these warnings for now, too many false positives * fix * changeset was already merged * changeset * oops * lint * docs + tidy * prettier * robustify: logic inside memoizer and outside could get out of sync, introducing bugs. use equality comparison instead * oops * uncomment * use finally * use is_async --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
1 parent 90a8a03 commit 1126ef3

File tree

69 files changed

+1097
-352
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1097
-352
lines changed

.changeset/bitter-rings-help.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: out-of-order rendering

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { walk } from 'zimmerframe';
66
import { parse } from '../1-parse/acorn.js';
77
import * as e from '../../errors.js';
88
import * as w from '../../warnings.js';
9-
import { extract_identifiers, has_await_expression } from '../../utils/ast.js';
9+
import {
10+
extract_identifiers,
11+
has_await_expression,
12+
object,
13+
unwrap_pattern
14+
} from '../../utils/ast.js';
1015
import * as b from '#compiler/builders';
1116
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
1217
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@@ -543,7 +548,13 @@ export function analyze_component(root, source, options) {
543548
snippet_renderers: new Map(),
544549
snippets: new Set(),
545550
async_deriveds: new Set(),
546-
pickled_awaits: new Set()
551+
pickled_awaits: new Set(),
552+
instance_body: {
553+
sync: [],
554+
async: [],
555+
declarations: [],
556+
hoisted: []
557+
}
547558
};
548559

549560
if (!runes) {
@@ -676,6 +687,194 @@ export function analyze_component(root, source, options) {
676687
}
677688
}
678689

690+
/**
691+
* @param {ESTree.Node} expression
692+
* @param {Scope} scope
693+
* @param {Set<Binding>} touched
694+
* @param {Set<ESTree.Node>} seen
695+
*/
696+
const touch = (expression, scope, touched, seen = new Set()) => {
697+
if (seen.has(expression)) return;
698+
seen.add(expression);
699+
700+
walk(
701+
expression,
702+
{ scope },
703+
{
704+
ImportDeclaration(node) {},
705+
Identifier(node, context) {
706+
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
707+
if (is_reference(node, parent)) {
708+
const binding = context.state.scope.get(node.name);
709+
if (binding) {
710+
touched.add(binding);
711+
712+
for (const assignment of binding.assignments) {
713+
touch(assignment.value, assignment.scope, touched, seen);
714+
}
715+
}
716+
}
717+
}
718+
}
719+
);
720+
};
721+
722+
/**
723+
* @param {ESTree.Node} node
724+
* @param {Set<ESTree.Node>} seen
725+
* @param {Set<Binding>} reads
726+
* @param {Set<Binding>} writes
727+
*/
728+
const trace_references = (node, reads, writes, seen = new Set()) => {
729+
if (seen.has(node)) return;
730+
seen.add(node);
731+
732+
/**
733+
* @param {ESTree.Pattern} node
734+
* @param {Scope} scope
735+
*/
736+
function update(node, scope) {
737+
for (const pattern of unwrap_pattern(node)) {
738+
const node = object(pattern);
739+
if (!node) return;
740+
741+
const binding = scope.get(node.name);
742+
if (!binding) return;
743+
744+
writes.add(binding);
745+
}
746+
}
747+
748+
walk(
749+
node,
750+
{ scope: instance.scope },
751+
{
752+
_(node, context) {
753+
const scope = scopes.get(node);
754+
if (scope) {
755+
context.next({ scope });
756+
} else {
757+
context.next();
758+
}
759+
},
760+
AssignmentExpression(node, context) {
761+
update(node.left, context.state.scope);
762+
},
763+
UpdateExpression(node, context) {
764+
update(
765+
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
766+
context.state.scope
767+
);
768+
},
769+
CallExpression(node, context) {
770+
// for now, assume everything touched by the callee ends up mutating the object
771+
// TODO optimise this better
772+
773+
// special case — no need to peek inside effects as they only run once async work has completed
774+
const rune = get_rune(node, context.state.scope);
775+
if (rune === '$effect') return;
776+
777+
/** @type {Set<Binding>} */
778+
const touched = new Set();
779+
touch(node, context.state.scope, touched);
780+
781+
for (const b of touched) {
782+
writes.add(b);
783+
}
784+
},
785+
// don't look inside functions until they are called
786+
ArrowFunctionExpression(_, context) {},
787+
FunctionDeclaration(_, context) {},
788+
FunctionExpression(_, context) {},
789+
Identifier(node, context) {
790+
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
791+
if (is_reference(node, parent)) {
792+
const binding = context.state.scope.get(node.name);
793+
if (binding) {
794+
reads.add(binding);
795+
}
796+
}
797+
}
798+
}
799+
);
800+
};
801+
802+
let awaited = false;
803+
804+
// TODO this should probably be attached to the scope?
805+
var promises = b.id('$$promises');
806+
807+
/**
808+
* @param {ESTree.Identifier} id
809+
* @param {ESTree.Expression} blocker
810+
*/
811+
function push_declaration(id, blocker) {
812+
analysis.instance_body.declarations.push(id);
813+
814+
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
815+
binding.blocker = blocker;
816+
}
817+
818+
for (let node of instance.ast.body) {
819+
if (node.type === 'ImportDeclaration') {
820+
analysis.instance_body.hoisted.push(node);
821+
continue;
822+
}
823+
824+
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
825+
// these can't exist inside `<script>` but TypeScript doesn't know that
826+
continue;
827+
}
828+
829+
if (node.type === 'ExportNamedDeclaration') {
830+
if (node.declaration) {
831+
node = node.declaration;
832+
} else {
833+
continue;
834+
}
835+
}
836+
837+
const has_await = has_await_expression(node);
838+
awaited ||= has_await;
839+
840+
if (awaited && node.type !== 'FunctionDeclaration') {
841+
/** @type {Set<Binding>} */
842+
const reads = new Set(); // TODO we're not actually using this yet
843+
844+
/** @type {Set<Binding>} */
845+
const writes = new Set();
846+
847+
trace_references(node, reads, writes);
848+
849+
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
850+
851+
for (const binding of writes) {
852+
binding.blocker = blocker;
853+
}
854+
855+
if (node.type === 'VariableDeclaration') {
856+
for (const declarator of node.declarations) {
857+
for (const id of extract_identifiers(declarator.id)) {
858+
push_declaration(id, blocker);
859+
}
860+
861+
// one declarator per declaration, makes things simpler
862+
analysis.instance_body.async.push({
863+
node: declarator,
864+
has_await
865+
});
866+
}
867+
} else if (node.type === 'ClassDeclaration') {
868+
push_declaration(node.id, blocker);
869+
analysis.instance_body.async.push({ node, has_await });
870+
} else {
871+
analysis.instance_body.async.push({ node, has_await });
872+
}
873+
} else {
874+
analysis.instance_body.sync.push(node);
875+
}
876+
}
877+
679878
if (analysis.runes) {
680879
const props_refs = module.scope.references.get('$$props');
681880
if (props_refs) {

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@ import * as e from '../../../errors.js';
1010
export function AwaitExpression(node, context) {
1111
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
1212

13-
// preserve context for
14-
// a) top-level await and
15-
// b) awaits that precede other expressions in template or `$derived(...)`
13+
// preserve context for awaits that precede other expressions in template or `$derived(...)`
1614
if (
17-
tla ||
18-
(is_reactive_expression(
15+
is_reactive_expression(
1916
context.path,
2017
context.state.derived_function_depth === context.state.function_depth
2118
) &&
22-
!is_last_evaluated_expression(context.path, node))
19+
!is_last_evaluated_expression(context.path, node)
2320
) {
2421
context.state.analysis.pickled_awaits.add(node);
2522
}
@@ -145,6 +142,9 @@ function is_last_evaluated_expression(path, node) {
145142
if (node !== parent.expressions.at(-1)) return false;
146143
break;
147144

145+
case 'VariableDeclarator':
146+
return true;
147+
148148
default:
149149
return false;
150150
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export function BindDirective(node, context) {
172172
}
173173

174174
const binding = context.state.scope.get(left.name);
175+
node.metadata.binding = binding;
175176

176177
if (assignee.type === 'Identifier') {
177178
// reassignment

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,13 @@ export function SnippetBlock(node, context) {
8181
function can_hoist_snippet(scope, scopes, visited = new Set()) {
8282
for (const [reference] of scope.references) {
8383
const binding = scope.get(reference);
84+
if (!binding) continue;
8485

85-
if (!binding || binding.scope.function_depth === 0) {
86+
if (binding.blocker) {
87+
return false;
88+
}
89+
90+
if (binding.scope.function_depth === 0) {
8691
continue;
8792
}
8893

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

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
3333
import { HtmlTag } from './visitors/HtmlTag.js';
3434
import { Identifier } from './visitors/Identifier.js';
3535
import { IfBlock } from './visitors/IfBlock.js';
36-
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
3736
import { KeyBlock } from './visitors/KeyBlock.js';
3837
import { LabeledStatement } from './visitors/LabeledStatement.js';
3938
import { LetDirective } from './visitors/LetDirective.js';
@@ -111,7 +110,6 @@ const visitors = {
111110
HtmlTag,
112111
Identifier,
113112
IfBlock,
114-
ImportDeclaration,
115113
KeyBlock,
116114
LabeledStatement,
117115
LetDirective,
@@ -153,7 +151,7 @@ export function client_component(analysis, options) {
153151
scope: analysis.module.scope,
154152
scopes: analysis.module.scopes,
155153
is_instance: false,
156-
hoisted: [b.import_all('$', 'svelte/internal/client')],
154+
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
157155
node: /** @type {any} */ (null), // populated by the root node
158156
legacy_reactive_imports: [],
159157
legacy_reactive_statements: new Map(),
@@ -370,41 +368,22 @@ export function client_component(analysis, options) {
370368
analysis.reactive_statements.size > 0 ||
371369
component_returned_object.length > 0;
372370

373-
if (analysis.instance.has_await) {
374-
if (should_inject_context && component_returned_object.length > 0) {
375-
component_block.body.push(b.var('$$exports'));
376-
}
377-
const body = b.block([
378-
...store_setup,
379-
...state.instance_level_snippets,
380-
.../** @type {ESTree.Statement[]} */ (instance.body),
381-
...(should_inject_context && component_returned_object.length > 0
382-
? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))]
383-
: []),
384-
b.if(b.call('$.aborted'), b.return()),
385-
.../** @type {ESTree.Statement[]} */ (template.body)
386-
]);
387-
388-
component_block.body.push(
389-
b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true)))
390-
);
391-
} else {
392-
component_block.body.push(
393-
...state.instance_level_snippets,
394-
.../** @type {ESTree.Statement[]} */ (instance.body)
395-
);
396-
if (should_inject_context && component_returned_object.length > 0) {
397-
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
398-
}
399-
component_block.body.unshift(...store_setup);
371+
component_block.body.push(
372+
...state.instance_level_snippets,
373+
.../** @type {ESTree.Statement[]} */ (instance.body)
374+
);
400375

401-
if (!analysis.runes && analysis.needs_context) {
402-
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
403-
}
376+
if (should_inject_context && component_returned_object.length > 0) {
377+
component_block.body.push(b.var('$$exports', b.object(component_returned_object)));
378+
}
379+
component_block.body.unshift(...store_setup);
404380

405-
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
381+
if (!analysis.runes && analysis.needs_context) {
382+
component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)));
406383
}
407384

385+
component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body));
386+
408387
if (analysis.needs_mutation_validation) {
409388
component_block.body.unshift(
410389
b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ export interface ClientTransformState extends TransformState {
2121
*/
2222
readonly in_constructor: boolean;
2323

24-
/** `true` if we're transforming the contents of `<script>` */
25-
readonly is_instance: boolean;
26-
2724
readonly transform: Record<
2825
string,
2926
{

0 commit comments

Comments
 (0)