Skip to content

Commit 6a3e293

Browse files
trueadmdummdidummRich-Harris
authored
fix: wait a microtask for await blocks to reduce UI churn (#11989)
* fix: wait a microtask for await blocks to reduce UI churn * fix: wait a microtask for await blocks to reduce UI churn * fix: wait a microtask for await blocks to reduce UI churn * fix bug * Make then blocks reactive * add test * update test * update test * Update packages/svelte/src/internal/client/dom/blocks/await.js Co-authored-by: Simon H <[email protected]> * Add support for catch block * slightly more specific naming * if we use the reserved $$ prefix we dont need to mess around with scope.generate * omit args for then/catch if unnecessary * neaten up some old code * shrink code * simplify test * add failing test * preserve pending blocks * update test * fix comment typo * tidy up --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent e9e7d8b commit 6a3e293

File tree

15 files changed

+320
-125
lines changed

15 files changed

+320
-125
lines changed

.changeset/gentle-eagles-walk.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: wait a microtask for await blocks to reduce UI churn

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as b from '../../../utils/builders.js';
22
import {
3+
extract_identifiers,
34
extract_paths,
45
is_expression_async,
56
is_simple_expression,
@@ -684,3 +685,44 @@ export function with_loc(target, source) {
684685
}
685686
return target;
686687
}
688+
689+
/**
690+
* @param {import("estree").Pattern} node
691+
* @param {import("zimmerframe").Context<import("#compiler").SvelteNode, import("./types").ComponentClientTransformState>} context
692+
* @returns {{ id: import("estree").Pattern, declarations: null | import("estree").Statement[] }}
693+
*/
694+
export function create_derived_block_argument(node, context) {
695+
if (node.type === 'Identifier') {
696+
return { id: node, declarations: null };
697+
}
698+
699+
const pattern = /** @type {import('estree').Pattern} */ (context.visit(node));
700+
const identifiers = extract_identifiers(node);
701+
702+
const id = b.id('$$source');
703+
const value = b.id('$$value');
704+
705+
const block = b.block([
706+
b.var(pattern, b.call('$.get', id)),
707+
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
708+
]);
709+
710+
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
711+
712+
for (const id of identifiers) {
713+
declarations.push(
714+
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
715+
);
716+
}
717+
718+
return { id, declarations };
719+
}
720+
721+
/**
722+
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
723+
* @param {import('./types.js').ComponentClientTransformState} state
724+
* @param {import('estree').Expression} arg
725+
*/
726+
export function create_derived(state, arg) {
727+
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
728+
}

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

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
function_visitor,
2222
get_assignment_value,
2323
serialize_get_binding,
24-
serialize_set_binding
24+
serialize_set_binding,
25+
create_derived,
26+
create_derived_block_argument
2527
} from '../utils.js';
2628
import {
2729
AttributeAliases,
@@ -646,15 +648,6 @@ function collect_parent_each_blocks(context) {
646648
);
647649
}
648650

649-
/**
650-
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
651-
* @param {import('../types.js').ComponentClientTransformState} state
652-
* @param {import('estree').Expression} arg
653-
*/
654-
function create_derived(state, arg) {
655-
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
656-
}
657-
658651
/**
659652
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
660653
* @param {string} component_name
@@ -2594,6 +2587,45 @@ export const template_visitors = {
25942587
AwaitBlock(node, context) {
25952588
context.state.template.push('<!>');
25962589

2590+
let then_block;
2591+
let catch_block;
2592+
2593+
if (node.then) {
2594+
/** @type {import('estree').Pattern[]} */
2595+
const args = [b.id('$$anchor')];
2596+
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.then));
2597+
2598+
if (node.value) {
2599+
const argument = create_derived_block_argument(node.value, context);
2600+
2601+
args.push(argument.id);
2602+
2603+
if (argument.declarations !== null) {
2604+
block.body.unshift(...argument.declarations);
2605+
}
2606+
}
2607+
2608+
then_block = b.arrow(args, block);
2609+
}
2610+
2611+
if (node.catch) {
2612+
/** @type {import('estree').Pattern[]} */
2613+
const args = [b.id('$$anchor')];
2614+
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.catch));
2615+
2616+
if (node.error) {
2617+
const argument = create_derived_block_argument(node.error, context);
2618+
2619+
args.push(argument.id);
2620+
2621+
if (argument.declarations !== null) {
2622+
block.body.unshift(...argument.declarations);
2623+
}
2624+
}
2625+
2626+
catch_block = b.arrow(args, block);
2627+
}
2628+
25972629
context.state.init.push(
25982630
b.stmt(
25992631
b.call(
@@ -2606,28 +2638,8 @@ export const template_visitors = {
26062638
/** @type {import('estree').BlockStatement} */ (context.visit(node.pending))
26072639
)
26082640
: b.literal(null),
2609-
node.then
2610-
? b.arrow(
2611-
node.value
2612-
? [
2613-
b.id('$$anchor'),
2614-
/** @type {import('estree').Pattern} */ (context.visit(node.value))
2615-
]
2616-
: [b.id('$$anchor')],
2617-
/** @type {import('estree').BlockStatement} */ (context.visit(node.then))
2618-
)
2619-
: b.literal(null),
2620-
node.catch
2621-
? b.arrow(
2622-
node.error
2623-
? [
2624-
b.id('$$anchor'),
2625-
/** @type {import('estree').Pattern} */ (context.visit(node.error))
2626-
]
2627-
: [b.id('$$anchor')],
2628-
/** @type {import('estree').BlockStatement} */ (context.visit(node.catch))
2629-
)
2630-
: b.literal(null)
2641+
then_block,
2642+
catch_block
26312643
)
26322644
)
26332645
);

packages/svelte/src/compiler/phases/scope.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
613613
scopes.set(node.value, value_scope);
614614
context.visit(node.value, { scope: value_scope });
615615
for (const id of extract_identifiers(node.value)) {
616-
then_scope.declare(id, 'normal', 'const');
616+
then_scope.declare(id, 'derived', 'const');
617617
value_scope.declare(id, 'normal', 'const');
618618
}
619619
}
@@ -627,7 +627,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
627627
scopes.set(node.error, error_scope);
628628
context.visit(node.error, { scope: error_scope });
629629
for (const id of extract_identifiers(node.error)) {
630-
catch_scope.declare(id, 'normal', 'const');
630+
catch_scope.declare(id, 'derived', 'const');
631631
error_scope.declare(id, 'normal', 'const');
632632
}
633633
}

packages/svelte/src/internal/client/dom/blocks/await.js

Lines changed: 85 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,111 +7,135 @@ import {
77
set_current_reaction,
88
set_dev_current_component_function
99
} from '../../runtime.js';
10-
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
11-
import { INERT } from '../../constants.js';
10+
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
1211
import { DEV } from 'esm-env';
12+
import { queue_micro_task } from '../task.js';
13+
import { hydrating } from '../hydration.js';
14+
import { set, source } from '../../reactivity/sources.js';
15+
16+
const PENDING = 0;
17+
const THEN = 1;
18+
const CATCH = 2;
1319

1420
/**
1521
* @template V
1622
* @param {Comment} anchor
1723
* @param {(() => Promise<V>)} get_input
1824
* @param {null | ((anchor: Node) => void)} pending_fn
19-
* @param {null | ((anchor: Node, value: V) => void)} then_fn
25+
* @param {null | ((anchor: Node, value: import('#client').Source<V>) => void)} then_fn
2026
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
2127
* @returns {void}
2228
*/
2329
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
24-
const component_context = current_component_context;
25-
/** @type {any} */
26-
let component_function;
27-
if (DEV) {
28-
component_function = component_context?.function ?? null;
29-
}
30+
var component_context = current_component_context;
3031

3132
/** @type {any} */
32-
let input;
33+
var component_function = DEV ? component_context?.function : null;
34+
35+
/** @type {V | Promise<V>} */
36+
var input;
3337

3438
/** @type {import('#client').Effect | null} */
35-
let pending_effect;
39+
var pending_effect;
3640

3741
/** @type {import('#client').Effect | null} */
38-
let then_effect;
42+
var then_effect;
3943

4044
/** @type {import('#client').Effect | null} */
41-
let catch_effect;
45+
var catch_effect;
46+
47+
var input_source = source(/** @type {V} */ (undefined));
48+
var error_source = source(undefined);
49+
var resolved = false;
4250

4351
/**
44-
* @param {(anchor: Comment, value: any) => void} fn
45-
* @param {any} value
52+
* @param {PENDING | THEN | CATCH} state
53+
* @param {boolean} restore
4654
*/
47-
function create_effect(fn, value) {
48-
set_current_effect(effect);
49-
set_current_reaction(effect); // TODO do we need both?
50-
set_current_component_context(component_context);
51-
if (DEV) {
52-
set_dev_current_component_function(component_function);
55+
function update(state, restore) {
56+
resolved = true;
57+
58+
if (restore) {
59+
set_current_effect(effect);
60+
set_current_reaction(effect); // TODO do we need both?
61+
set_current_component_context(component_context);
62+
if (DEV) set_dev_current_component_function(component_function);
63+
}
64+
65+
if (state === PENDING && pending_fn) {
66+
if (pending_effect) resume_effect(pending_effect);
67+
else pending_effect = branch(() => pending_fn(anchor));
68+
}
69+
70+
if (state === THEN && then_fn) {
71+
if (then_effect) resume_effect(then_effect);
72+
else then_effect = branch(() => then_fn(anchor, input_source));
73+
}
74+
75+
if (state === CATCH && catch_fn) {
76+
if (catch_effect) resume_effect(catch_effect);
77+
else catch_effect = branch(() => catch_fn(anchor, error_source));
78+
}
79+
80+
if (state !== PENDING && pending_effect) {
81+
pause_effect(pending_effect, () => (pending_effect = null));
5382
}
54-
var e = branch(() => fn(anchor, value));
55-
if (DEV) {
56-
set_dev_current_component_function(null);
83+
84+
if (state !== THEN && then_effect) {
85+
pause_effect(then_effect, () => (then_effect = null));
86+
}
87+
88+
if (state !== CATCH && catch_effect) {
89+
pause_effect(catch_effect, () => (catch_effect = null));
5790
}
58-
set_current_component_context(null);
59-
set_current_reaction(null);
60-
set_current_effect(null);
6191

62-
// without this, the DOM does not update until two ticks after the promise,
63-
// resolves which is unexpected behaviour (and somewhat irksome to test)
64-
flush_sync();
92+
if (restore) {
93+
if (DEV) set_dev_current_component_function(null);
94+
set_current_component_context(null);
95+
set_current_reaction(null);
96+
set_current_effect(null);
6597

66-
return e;
98+
// without this, the DOM does not update until two ticks after the promise
99+
// resolves, which is unexpected behaviour (and somewhat irksome to test)
100+
flush_sync();
101+
}
67102
}
68103

69-
const effect = block(() => {
104+
var effect = block(() => {
70105
if (input === (input = get_input())) return;
71106

72107
if (is_promise(input)) {
73-
const promise = /** @type {Promise<any>} */ (input);
108+
var promise = input;
74109

75-
if (pending_fn) {
76-
if (pending_effect && (pending_effect.f & INERT) === 0) {
77-
destroy_effect(pending_effect);
78-
}
79-
80-
pending_effect = branch(() => pending_fn(anchor));
81-
}
82-
83-
if (then_effect) pause_effect(then_effect);
84-
if (catch_effect) pause_effect(catch_effect);
110+
resolved = false;
85111

86112
promise.then(
87113
(value) => {
88114
if (promise !== input) return;
89-
if (pending_effect) pause_effect(pending_effect);
90-
91-
if (then_fn) {
92-
then_effect = create_effect(then_fn, value);
93-
}
115+
set(input_source, value);
116+
update(THEN, true);
94117
},
95118
(error) => {
96119
if (promise !== input) return;
97-
if (pending_effect) pause_effect(pending_effect);
98-
99-
if (catch_fn) {
100-
catch_effect = create_effect(catch_fn, error);
101-
}
120+
set(error_source, error);
121+
update(CATCH, true);
102122
}
103123
);
104-
} else {
105-
if (pending_effect) pause_effect(pending_effect);
106-
if (catch_effect) pause_effect(catch_effect);
107124

108-
if (then_fn) {
109-
if (then_effect) {
110-
destroy_effect(then_effect);
125+
if (hydrating) {
126+
if (pending_fn) {
127+
pending_effect = branch(() => pending_fn(anchor));
111128
}
112-
113-
then_effect = branch(() => then_fn(anchor, input));
129+
} else {
130+
// Wait a microtask before checking if we should show the pending state as
131+
// the promise might have resolved by the next microtask.
132+
queue_micro_task(() => {
133+
if (!resolved) update(PENDING, true);
134+
});
114135
}
136+
} else {
137+
set(input_source, input);
138+
update(THEN, false);
115139
}
116140

117141
// Inert effects are proactively detached from the effect tree. Returning a noop

packages/svelte/tests/runtime-legacy/samples/await-then-destruct-computed-props/_config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default test({
2222
prop3: { prop7: 'seven' },
2323
prop4: { prop10: 'ten' }
2424
}));
25+
await Promise.resolve();
2526
assert.htmlEqual(
2627
target.innerHTML,
2728
`

packages/svelte/tests/runtime-legacy/samples/await-then-no-expression/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@
2020
<p>the promise is pending</p>
2121
{:then}
2222
<p>the promise is resolved</p>
23-
{/await}
23+
{/await}

0 commit comments

Comments
 (0)