Skip to content

fix: wait a microtask for await blocks to reduce UI churn #11989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gentle-eagles-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: wait a microtask for await blocks to reduce UI churn
42 changes: 42 additions & 0 deletions packages/svelte/src/compiler/phases/3-transform/client/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as b from '../../../utils/builders.js';
import {
extract_identifiers,
extract_paths,
is_expression_async,
is_simple_expression,
Expand Down Expand Up @@ -684,3 +685,44 @@ export function with_loc(target, source) {
}
return target;
}

/**
* @param {import("estree").Pattern} node
* @param {import("zimmerframe").Context<import("#compiler").SvelteNode, import("./types").ComponentClientTransformState>} context
* @returns {{ id: import("estree").Pattern, declarations: null | import("estree").Statement[] }}
*/
export function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
return { id: node, declarations: null };
}

const pattern = /** @type {import('estree').Pattern} */ (context.visit(node));
const identifiers = extract_identifiers(node);

const id = b.id('$$source');
const value = b.id('$$value');

const block = b.block([
b.var(pattern, b.call('$.get', id)),
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);

const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];

for (const id of identifiers) {
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
);
}

return { id, declarations };
}

/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {import('./types.js').ComponentClientTransformState} state
* @param {import('estree').Expression} arg
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
function_visitor,
get_assignment_value,
serialize_get_binding,
serialize_set_binding
serialize_set_binding,
create_derived,
create_derived_block_argument
} from '../utils.js';
import {
AttributeAliases,
Expand Down Expand Up @@ -646,15 +648,6 @@ function collect_parent_each_blocks(context) {
);
}

/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {import('../types.js').ComponentClientTransformState} state
* @param {import('estree').Expression} arg
*/
function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}

/**
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name
Expand Down Expand Up @@ -2594,6 +2587,45 @@ export const template_visitors = {
AwaitBlock(node, context) {
context.state.template.push('<!>');

let then_block;
let catch_block;

if (node.then) {
/** @type {import('estree').Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.then));

if (node.value) {
const argument = create_derived_block_argument(node.value, context);

args.push(argument.id);

if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}

then_block = b.arrow(args, block);
}

if (node.catch) {
/** @type {import('estree').Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.catch));

if (node.error) {
const argument = create_derived_block_argument(node.error, context);

args.push(argument.id);

if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}

catch_block = b.arrow(args, block);
}

context.state.init.push(
b.stmt(
b.call(
Expand All @@ -2606,28 +2638,8 @@ export const template_visitors = {
/** @type {import('estree').BlockStatement} */ (context.visit(node.pending))
)
: b.literal(null),
node.then
? b.arrow(
node.value
? [
b.id('$$anchor'),
/** @type {import('estree').Pattern} */ (context.visit(node.value))
]
: [b.id('$$anchor')],
/** @type {import('estree').BlockStatement} */ (context.visit(node.then))
)
: b.literal(null),
node.catch
? b.arrow(
node.error
? [
b.id('$$anchor'),
/** @type {import('estree').Pattern} */ (context.visit(node.error))
]
: [b.id('$$anchor')],
/** @type {import('estree').BlockStatement} */ (context.visit(node.catch))
)
: b.literal(null)
then_block,
catch_block
)
)
);
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'normal', 'const');
then_scope.declare(id, 'derived', 'const');
value_scope.declare(id, 'normal', 'const');
}
}
Expand All @@ -627,7 +627,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'normal', 'const');
catch_scope.declare(id, 'derived', 'const');
error_scope.declare(id, 'normal', 'const');
}
}
Expand Down
146 changes: 85 additions & 61 deletions packages/svelte/src/internal/client/dom/blocks/await.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,111 +7,135 @@ import {
set_current_reaction,
set_dev_current_component_function
} from '../../runtime.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import { INERT } from '../../constants.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { DEV } from 'esm-env';
import { queue_micro_task } from '../task.js';
import { hydrating } from '../hydration.js';
import { set, source } from '../../reactivity/sources.js';

const PENDING = 0;
const THEN = 1;
const CATCH = 2;

/**
* @template V
* @param {Comment} anchor
* @param {(() => Promise<V>)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: V) => void)} then_fn
* @param {null | ((anchor: Node, value: import('#client').Source<V>) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void}
*/
export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
const component_context = current_component_context;
/** @type {any} */
let component_function;
if (DEV) {
component_function = component_context?.function ?? null;
}
var component_context = current_component_context;

/** @type {any} */
let input;
var component_function = DEV ? component_context?.function : null;

/** @type {V | Promise<V>} */
var input;

/** @type {import('#client').Effect | null} */
let pending_effect;
var pending_effect;

/** @type {import('#client').Effect | null} */
let then_effect;
var then_effect;

/** @type {import('#client').Effect | null} */
let catch_effect;
var catch_effect;

var input_source = source(/** @type {V} */ (undefined));
var error_source = source(undefined);
var resolved = false;

/**
* @param {(anchor: Comment, value: any) => void} fn
* @param {any} value
* @param {PENDING | THEN | CATCH} state
* @param {boolean} restore
*/
function create_effect(fn, value) {
set_current_effect(effect);
set_current_reaction(effect); // TODO do we need both?
set_current_component_context(component_context);
if (DEV) {
set_dev_current_component_function(component_function);
function update(state, restore) {
resolved = true;

if (restore) {
set_current_effect(effect);
set_current_reaction(effect); // TODO do we need both?
set_current_component_context(component_context);
if (DEV) set_dev_current_component_function(component_function);
}

if (state === PENDING && pending_fn) {
if (pending_effect) resume_effect(pending_effect);
else pending_effect = branch(() => pending_fn(anchor));
}

if (state === THEN && then_fn) {
if (then_effect) resume_effect(then_effect);
else then_effect = branch(() => then_fn(anchor, input_source));
}

if (state === CATCH && catch_fn) {
if (catch_effect) resume_effect(catch_effect);
else catch_effect = branch(() => catch_fn(anchor, error_source));
}

if (state !== PENDING && pending_effect) {
pause_effect(pending_effect, () => (pending_effect = null));
}
var e = branch(() => fn(anchor, value));
if (DEV) {
set_dev_current_component_function(null);

if (state !== THEN && then_effect) {
pause_effect(then_effect, () => (then_effect = null));
}

if (state !== CATCH && catch_effect) {
pause_effect(catch_effect, () => (catch_effect = null));
}
set_current_component_context(null);
set_current_reaction(null);
set_current_effect(null);

// without this, the DOM does not update until two ticks after the promise,
// resolves which is unexpected behaviour (and somewhat irksome to test)
flush_sync();
if (restore) {
if (DEV) set_dev_current_component_function(null);
set_current_component_context(null);
set_current_reaction(null);
set_current_effect(null);

return e;
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
flush_sync();
}
}

const effect = block(() => {
var effect = block(() => {
if (input === (input = get_input())) return;

if (is_promise(input)) {
const promise = /** @type {Promise<any>} */ (input);
var promise = input;

if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
destroy_effect(pending_effect);
}

pending_effect = branch(() => pending_fn(anchor));
}

if (then_effect) pause_effect(then_effect);
if (catch_effect) pause_effect(catch_effect);
resolved = false;

promise.then(
(value) => {
if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect);

if (then_fn) {
then_effect = create_effect(then_fn, value);
}
set(input_source, value);
update(THEN, true);
},
(error) => {
if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect);

if (catch_fn) {
catch_effect = create_effect(catch_fn, error);
}
set(error_source, error);
update(CATCH, true);
}
);
} else {
if (pending_effect) pause_effect(pending_effect);
if (catch_effect) pause_effect(catch_effect);

if (then_fn) {
if (then_effect) {
destroy_effect(then_effect);
if (hydrating) {
if (pending_fn) {
pending_effect = branch(() => pending_fn(anchor));
}

then_effect = branch(() => then_fn(anchor, input));
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
queue_micro_task(() => {
if (!resolved) update(PENDING, true);
});
}
} else {
set(input_source, input);
update(THEN, false);
}

// Inert effects are proactively detached from the effect tree. Returning a noop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default test({
prop3: { prop7: 'seven' },
prop4: { prop10: 'ten' }
}));
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
<p>the promise is pending</p>
{:then}
<p>the promise is resolved</p>
{/await}
{/await}
Loading
Loading