diff --git a/.changeset/spicy-hotels-applaud.md b/.changeset/spicy-hotels-applaud.md new file mode 100644 index 0000000000..343a8baf4e --- /dev/null +++ b/.changeset/spicy-hotels-applaud.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: parallelize more async work 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 940d6a9e00..54a984edd3 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 @@ -63,7 +63,7 @@ import { VariableDeclaration } from './visitors/VariableDeclaration.js'; /** @type {Visitors} */ const visitors = { - _: function set_scope(node, { next, state }) { + _: function set_scope(node, { path, next, state }) { const scope = state.scopes.get(node); if (scope && scope !== state.scope) { @@ -84,6 +84,9 @@ const visitors = { } else { next(); } + if (node.type !== 'VariableDeclaration' && path.at(-1)?.type === 'Program' && state.analysis.instance) { + state.current_parallelized_chunk = null; + } }, AnimateDirective, ArrowFunctionExpression, @@ -176,7 +179,9 @@ export function client_component(analysis, options) { update: /** @type {any} */ (null), after_update: /** @type {any} */ (null), template: /** @type {any} */ (null), - memoizer: /** @type {any} */ (null) + memoizer: /** @type {any} */ (null), + parallelized_derived_chunks: [], + current_parallelized_chunk: null }; const module = /** @type {ESTree.Program} */ ( 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 59c024dfb7..4e95ee05b9 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 @@ -7,7 +7,7 @@ import type { AssignmentExpression, UpdateExpression, VariableDeclaration, - Declaration + Pattern } from 'estree'; import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; @@ -83,6 +83,18 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly instance_level_snippets: VariableDeclaration[]; /** Snippets hoisted to the module */ readonly module_level_snippets: VariableDeclaration[]; + readonly parallelized_derived_chunks: ParallelizedChunk[]; + current_parallelized_chunk: ParallelizedChunk | null; +} + +export interface ParallelizedChunk { + declarators: Array<{ + id: Pattern; + init: Expression; + }>; + kind: VariableDeclaration['kind']; + /** index in instance body */ + position: number; } export type Context = import('zimmerframe').Context; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 19a4342b5e..45bd55f1f4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,7 +1,7 @@ -/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ +/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ -/** @import { Analysis } from '../../types.js' */ +/** @import { Analysis, ComponentAnalysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; import { is_simple_expression } from '../../../utils/ast.js'; @@ -15,6 +15,7 @@ import { import { dev } from '../../../state.js'; import { walk } from 'zimmerframe'; import { validate_mutation } from './visitors/shared/utils.js'; +import is_reference from 'is-reference'; /** * @param {Binding} binding @@ -28,6 +29,60 @@ export function is_state_source(binding, analysis) { ); } +/** + * @param {Expression} expression + * @param {Scope} scope + * @param {Analysis | ComponentAnalysis} analysis + * @returns {boolean} + */ +export function can_be_parallelized(expression, scope, analysis) { + let has_closures = false; + /** @type {Set} */ + const references = new Set(); + walk(expression, null, { + ArrowFunctionExpression(_, { stop }) { + has_closures = true; + stop(); + }, + FunctionExpression(_, { stop }) { + has_closures = true; + stop(); + }, + Identifier(node, { path }) { + if (is_reference(node, /** @type {Node} */ (path.at(-1)))) { + references.add(node.name); + } + } + }); + if (has_closures) { + return false; + } + for (const reference of references) { + const binding = scope.get(reference); + if (!binding || binding.declaration_kind === 'import') { + return false; + } + if ('template' in analysis) { + if (binding.scope !== analysis.instance.scope) { + return false; + } + } else if (binding.scope !== analysis.module.scope) { + return false; + } + + if (binding.kind === 'derived') { + const init = /** @type {CallExpression} */ (binding.initial); + if (analysis.async_deriveds.has(init)) { + return false; + } + } + if (!binding.mutated && !binding.reassigned) { + continue; + } + } + return true; +} + /** * @param {Identifier} node * @param {ClientTransformState} state diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 07342da314..65532d42ec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -5,10 +5,10 @@ import * as b from '#compiler/builders'; import { add_state_transformers } from './shared/declarations.js'; /** - * @param {Program} _ + * @param {Program} node * @param {ComponentContext} context */ -export function Program(_, context) { +export function Program(node, context) { if (!context.state.analysis.runes) { context.state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) @@ -137,5 +137,20 @@ export function Program(_, context) { add_state_transformers(context); - context.next(); + /** @type {Program['body']} */ + const body = []; + for (let i = 0; i < node.body.length; i++) { + const transformed = /** @type {Program['body'][number]} */ (context.visit(node.body[i])); + const chunk = context.state.parallelized_derived_chunks?.at(-1); + body.push(transformed); + if (chunk && chunk.position === i) { + const pattern = b.array_pattern(chunk.declarators.map(({ id }) => id)); + const init = b.call('$.all', b.array(chunk.declarators.map(({ init }) => init))); + body.push(b.declaration(chunk.kind, [b.declarator(pattern, b.await(init))])); + } + } + return { + ...node, + body + }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 0998dc4778..247ddfcf58 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -1,12 +1,18 @@ -/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { CallExpression, Expression, Identifier, Literal, Program, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { Binding } from '#compiler' */ -/** @import { ComponentContext } from '../types' */ +/** @import { ComponentContext, ParallelizedChunk } from '../types' */ import { dev, is_ignored, locate_node } from '../../../../state.js'; import { extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; -import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; +import { + can_be_parallelized, + get_prop_source, + is_prop_source, + is_state_source, + should_proxy +} from '../utils.js'; import { is_hoisted_function } from '../../utils.js'; import { get_value } from './shared/declarations.js'; @@ -200,6 +206,18 @@ export function VariableDeclaration(node, context) { const is_async = context.state.analysis.async_deriveds.has( /** @type {CallExpression} */ (init) ); + let parallelize = false; + if ( + is_async && + context.state.analysis.instance && + context.state.scope === context.state.analysis.instance.scope && + !dev + ) { + parallelize = can_be_parallelized(value, context.state.scope, context.state.analysis); + } + + /** @type {VariableDeclarator[]} */ + const derived_declarators = []; if (declarator.id.type === 'Identifier') { let expression = /** @type {Expression} */ ( @@ -212,7 +230,7 @@ export function VariableDeclaration(node, context) { if (is_async) { const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init); let call = b.call( - '$.async_derived', + '$.async_derived' + (parallelize ? '_p' : ''), b.thunk(expression, true), location ? b.literal(location) : undefined ); @@ -220,14 +238,14 @@ export function VariableDeclaration(node, context) { call = b.call(b.await(b.call('$.save', call))); if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); - declarations.push(b.declarator(declarator.id, call)); + derived_declarators.push(b.declarator(declarator.id, call)); } else { if (rune === '$derived') expression = b.thunk(expression); let call = b.call('$.derived', expression); if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); - declarations.push(b.declarator(declarator.id, call)); + derived_declarators.push(b.declarator(declarator.id, call)); } } else { const init = /** @type {CallExpression} */ (declarator.init); @@ -253,7 +271,9 @@ export function VariableDeclaration(node, context) { b.thunk(expression, true), location ? b.literal(location) : undefined ); - call = b.call(b.await(b.call('$.save', call))); + if (!parallelize) { + call = b.call(b.await(b.call('$.save', call))); + } } if (dev) { @@ -261,7 +281,7 @@ export function VariableDeclaration(node, context) { call = b.call('$.tag', call, b.literal(label)); } - declarations.push(b.declarator(id, call)); + derived_declarators.push(b.declarator(id, call)); } const { inserts, paths } = extract_paths(declarator.id, rhs); @@ -278,13 +298,13 @@ export function VariableDeclaration(node, context) { call = b.call('$.tag', call, b.literal(label)); } - declarations.push(b.declarator(id, call)); + derived_declarators.push(b.declarator(id, call)); } for (const path of paths) { const expression = /** @type {Expression} */ (context.visit(path.expression)); const call = b.call('$.derived', b.thunk(expression)); - declarations.push( + derived_declarators.push( b.declarator( path.node, dev @@ -295,6 +315,32 @@ export function VariableDeclaration(node, context) { } } + if (!parallelize) { + declarations.push(...derived_declarators); + } else if (derived_declarators.length > 0) { + /** @type {ParallelizedChunk['declarators']} */ + const declarators = derived_declarators.map(({ id, init }) => ({ + id, + init: /** @type {Expression} */ (init) + })); + if ( + context.state.current_parallelized_chunk && + context.state.current_parallelized_chunk.kind === node.kind && + context.state.current_parallelized_chunk.position === + /** @type {Program} */ (context.path.at(-1)).body.indexOf(node) + ) { + context.state.current_parallelized_chunk.declarators.push(...declarators); + } else { + const chunk = { + kind: node.kind, + declarators, + position: /** @type {Program} */ (context.path.at(-1)).body.indexOf(node) + }; + context.state.current_parallelized_chunk = chunk; + context.state.parallelized_derived_chunks.push(chunk); + } + } + continue; } } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c5b7bb845c..c88114de01 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -99,6 +99,7 @@ export { with_script } from './dom/template.js'; export { + all, async_body, for_await_track_reactivity_loss, save, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 1ea1bbe561..0767bd3688 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -189,3 +189,12 @@ export async function async_body(fn) { unsuspend(); } } + +/** + * @template T + * @param {Array>} promises + * @returns {Promise>} + */ +export function all(...promises) { + return Promise.all(promises.map((promise) => save(promise).then((restore) => restore()))); +}