From 0d8f27eae69760714c9c439f15af492f0b226ff9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Jan 2025 10:41:40 -0500 Subject: [PATCH] parallelize --- .../3-transform/client/transform-client.js | 3 +- .../phases/3-transform/client/types.d.ts | 7 +-- .../3-transform/client/visitors/Fragment.js | 38 +++++++++++-- .../client/visitors/RegularElement.js | 13 ++--- .../client/visitors/SvelteElement.js | 7 +-- .../client/visitors/TitleElement.js | 7 +-- .../client/visitors/shared/component.js | 13 ++--- .../client/visitors/shared/element.js | 21 +++----- .../client/visitors/shared/fragment.js | 8 +-- .../client/visitors/shared/utils.js | 53 ++++++++----------- .../internal/client/dom/blocks/boundary.js | 6 +++ packages/svelte/src/internal/client/index.js | 2 +- 12 files changed, 87 insertions(+), 91 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index e7a5e024af..616376b012 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -160,8 +160,7 @@ export function client_component(analysis, options) { }, namespace: options.namespace, bound_contenteditable: false, - init_is_async: false, - update_is_async: false + async: [] }, events: new Set(), preserve_whitespace: options.preserveWhitespace, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 46a268d514..06309ac34e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -75,9 +75,10 @@ export interface ComponentClientTransformState extends ClientTransformState { */ template_contains_script_tag: boolean; }; - // TODO it would be nice if these were colocated with the arrays they pertain to - init_is_async: boolean; - update_is_async: boolean; + /** + * Synthetic async deriveds belonging to the current fragment + */ + async: Array<{ id: Identifier; expression: Expression }>; }; readonly preserve_whitespace: boolean; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index e69243e9d7..0755126e2a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -75,8 +75,7 @@ export function Fragment(node, context) { }, namespace, bound_contenteditable: context.state.metadata.bound_contenteditable, - init_is_async: false, - update_is_async: false + async: [] } }; @@ -192,7 +191,7 @@ export function Fragment(node, context) { } if (state.update.length > 0) { - body.push(build_render_statement(state.update, state.metadata.update_is_async)); + body.push(build_render_statement(state.update)); } body.push(...state.after_update); @@ -205,12 +204,41 @@ export function Fragment(node, context) { } const async = - state.metadata.init_is_async || (state.analysis.is_async && context.path.length === 0); + state.metadata.async.length > 0 || (state.analysis.is_async && context.path.length === 0); if (async) { // TODO need to create bookends for hydration to work return b.block([ - b.function_declaration(b.id('$$body'), [b.id('$$anchor')], b.block(body), true), + b.function_declaration( + b.id('$$body'), + [b.id('$$anchor')], + b.block([ + b.var( + b.array_pattern(state.metadata.async.map(({ id }) => id)), + b.call( + b.member( + b.await( + b.call( + '$.suspend', + b.call( + 'Promise.all', + b.array( + state.metadata.async.map(({ expression }) => + b.call('$.async_derived', b.thunk(expression, true)) + ) + ) + ) + ) + ), + 'exit' + ) + ) + ), + ...body, + b.stmt(b.call('$.exit')) + ]), + true + ), b.var('fragment', b.call('$.comment')), b.var('node', b.call('$.first_child', b.id('fragment'))), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 5632d35b24..9446065919 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -409,9 +409,7 @@ export function RegularElement(node, context) { b.block([ ...child_state.init, ...element_state.init, - child_state.update.length > 0 - ? build_render_statement(child_state.update, child_state.metadata.update_is_async) - : b.empty, + child_state.update.length > 0 ? build_render_statement(child_state.update) : b.empty, ...child_state.after_update, ...element_state.after_update ]) @@ -420,9 +418,6 @@ export function RegularElement(node, context) { context.state.init.push(...child_state.init, ...element_state.init); context.state.update.push(...child_state.update); context.state.after_update.push(...child_state.after_update, ...element_state.after_update); - - context.state.metadata.init_is_async ||= child_state.metadata.init_is_async; - context.state.metadata.update_is_async ||= child_state.metadata.update_is_async; } else { context.state.init.push(...element_state.init); context.state.after_update.push(...element_state.after_update); @@ -632,10 +627,9 @@ function build_element_attribute_update_assignment( if (attribute.metadata.expression.has_state) { if (has_call) { - state.init.push(build_update(update, attribute.metadata.expression.is_async)); + state.init.push(build_update(update)); } else { state.update.push(update); - state.metadata.update_is_async ||= attribute.metadata.expression.is_async; } return true; } else { @@ -668,10 +662,9 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co if (attribute.metadata.expression.has_state) { if (has_call) { - state.init.push(build_update(update, attribute.metadata.expression.is_async)); + state.init.push(build_update(update)); } else { state.update.push(update); - state.metadata.update_is_async ||= attribute.metadata.expression.is_async; } return true; } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index c3d0360722..ba66fe29d6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -123,12 +123,7 @@ export function SvelteElement(node, context) { /** @type {Statement[]} */ const inner = inner_context.state.init; if (inner_context.state.update.length > 0) { - inner.push( - build_render_statement( - inner_context.state.update, - inner_context.state.metadata.update_is_async - ) - ); + inner.push(build_render_statement(inner_context.state.update)); } inner.push(...inner_context.state.after_update); inner.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 05ae059ad2..72cc57b068 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -8,7 +8,7 @@ import { build_template_chunk } from './shared/utils.js'; * @param {ComponentContext} context */ export function TitleElement(node, context) { - const { has_state, is_async, value } = build_template_chunk( + const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), context.visit, context.state @@ -18,12 +18,7 @@ export function TitleElement(node, context) { if (has_state) { context.state.update.push(statement); - context.state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } - context.state.init.push(statement); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 644c0478d2..0ab47afcbf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -94,10 +94,6 @@ export function build_component(node, component_name, context, anchor = context. } for (const attribute of node.attributes) { - if (attribute.type === 'Attribute' || attribute.type === 'SpreadAttribute') { - context.state.metadata.init_is_async ||= attribute.metadata.expression.is_async; - } - if (attribute.type === 'LetDirective') { if (!slot_scope_applies_to_itself) { lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default))); @@ -169,10 +165,11 @@ export function build_component(node, component_name, context, anchor = context. const id = b.id(context.state.scope.generate(attribute.name)); if (attribute.metadata.expression.is_async) { - // TODO parallelise these - context.state.init.push( - b.var(id, b.await(b.call('$.async_derived', b.thunk(arg, true)))) - ); + context.state.metadata.async.push({ + id, + expression: arg + }); + arg = b.call(id); } else { context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 2e746cbf78..e49dbaedb0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -83,7 +83,6 @@ export function build_set_attributes( context.state.init.push(b.let(attributes_id)); const update = b.stmt(b.assignment('=', attributes_id, call)); context.state.update.push(update); - context.state.metadata.update_is_async ||= is_async; return true; } @@ -115,7 +114,9 @@ export function build_style_directives( ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) : build_attribute_value(directive.value, context).value; - if (has_call) { + if (is_async) { + throw new Error('TODO'); + } else if (has_call) { const id = b.id(state.scope.generate('style_directive')); state.init.push(b.const(id, create_derived(state, b.thunk(value)))); @@ -133,14 +134,10 @@ export function build_style_directives( ); if (!is_attributes_reactive && has_call) { - state.init.push(build_update(update, is_async)); + state.init.push(build_update(update)); } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); - state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } state.init.push(update); } } @@ -165,7 +162,9 @@ export function build_class_directives( const { has_state, has_call, is_async } = directive.metadata.expression; let value = /** @type {Expression} */ (context.visit(directive.expression)); - if (has_call) { + if (is_async) { + throw new Error('TODO'); + } else if (has_call) { const id = b.id(state.scope.generate('class_directive')); state.init.push(b.const(id, create_derived(state, b.thunk(value)))); @@ -175,14 +174,10 @@ export function build_class_directives( const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); if (!is_attributes_reactive && has_call) { - state.init.push(build_update(update, is_async)); + state.init.push(build_update(update)); } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); - state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } state.init.push(update); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 5744cd51aa..7674fd1eb2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -69,7 +69,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { state.template.push(' '); - const { has_state, has_call, is_async, value } = build_template_chunk(sequence, visit, state); + const { has_state, has_call, value } = build_template_chunk(sequence, visit, state); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -79,14 +79,10 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_call && !within_bound_contenteditable) { - state.init.push(build_update(update, is_async)); + state.init.push(build_update(update)); } else if (has_state && !within_bound_contenteditable) { state.update.push(update); - state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index b8c0f438a1..528119b3fb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -14,7 +14,7 @@ import { locator } from '../../../../../state.js'; * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @returns {{ value: Expression, has_state: boolean, has_call: boolean, is_async: boolean }} + * @returns {{ value: Expression, has_state: boolean, has_call: boolean }} */ export function build_template_chunk(values, visit, state) { /** @type {Expression[]} */ @@ -26,16 +26,15 @@ export function build_template_chunk(values, visit, state) { let has_call = false; let has_state = false; let is_async = false; - let contains_multiple_call_expression = false; + let should_memoize = false; for (const node of values) { if (node.type === 'ExpressionTag') { const metadata = node.metadata.expression; - contains_multiple_call_expression ||= has_call && metadata.has_call; + should_memoize ||= (has_call || is_async) && (metadata.has_call || metadata.is_async); has_call ||= metadata.has_call; has_state ||= metadata.has_state; - is_async ||= metadata.is_async; } } @@ -49,32 +48,26 @@ export function build_template_chunk(values, visit, state) { quasi.value.cooked += node.expression.value + ''; } } else { - if (contains_multiple_call_expression) { - const id = b.id(state.scope.generate('stringified_text')); + const expression = /** @type {Expression} */ (visit(node.expression, state)); + + if (node.metadata.expression.is_async) { + const id = b.id(state.scope.generate('expression')); + state.metadata.async.push({ id, expression: b.logical('??', expression, b.literal('')) }); + + expressions.push(b.call(id)); + } else if (node.metadata.expression.has_call && should_memoize) { + const id = b.id(state.scope.generate('expression')); state.init.push( - b.const( - id, - create_derived( - state, - b.thunk( - b.logical( - '??', - /** @type {Expression} */ (visit(node.expression, state)), - b.literal('') - ), - is_async - ) - ) - ) + b.const(id, create_derived(state, b.thunk(b.logical('??', expression, b.literal(''))))) ); - expressions.push(is_async ? b.await(b.call('$.get', id)) : b.call('$.get', id)); + expressions.push(b.call('$.get', id)); } else if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value: visit(node.expression, state), has_state, has_call, is_async }; + return { value: expression, has_state, has_call }; } else { - expressions.push(b.logical('??', visit(node.expression, state), b.literal(''))); + expressions.push(b.logical('??', expression, b.literal(''))); } quasi = b.quasi('', i + 1 === values.length); @@ -88,28 +81,26 @@ export function build_template_chunk(values, visit, state) { const value = b.template(quasis, expressions); - return { value, has_state, has_call, is_async }; + return { value, has_state, has_call }; } /** * @param {Statement} statement - * @param {boolean} is_async */ -export function build_update(statement, is_async) { +export function build_update(statement) { const body = statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]); - return b.stmt(b.call('$.template_effect', b.thunk(body, is_async))); + return b.stmt(b.call('$.template_effect', b.thunk(body))); } /** * @param {Statement[]} update - * @param {boolean} is_async */ -export function build_render_statement(update, is_async) { +export function build_render_statement(update) { return update.length === 1 - ? build_update(update[0], is_async) - : b.stmt(b.call('$.template_effect', b.thunk(b.block(update), is_async))); + ? build_update(update[0]) + : b.stmt(b.call('$.template_effect', b.thunk(b.block(update)))); } /** diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c2d976c244..ed2cddbed2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -285,3 +285,9 @@ export async function suspend(promise) { } }; } + +export function exit() { + set_active_effect(null); + set_active_reaction(null); + set_component_context(null); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 0a17a54621..c9b259c4df 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, suspend } from './dom/blocks/boundary.js'; +export { boundary, exit, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get,