Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
de84f6b
init
Ocean-OS Aug 29, 2025
0bbc880
remove temporary test thing
Ocean-OS Aug 29, 2025
aafa27f
don't parallelize if only one derived can be parallelized
Ocean-OS Aug 29, 2025
2f7319c
fix
Ocean-OS Aug 29, 2025
c0723ad
fix?
Ocean-OS Aug 29, 2025
aeff5f3
lint
Ocean-OS Aug 29, 2025
710d6a6
fix
Ocean-OS Aug 29, 2025
e07672b
fix
Ocean-OS Aug 29, 2025
a5a5b8d
tweak
Ocean-OS Aug 29, 2025
76b47d7
more
Ocean-OS Aug 29, 2025
f57ec19
doh
Ocean-OS Aug 29, 2025
cd8969a
try adding regular `await`ed variables
Ocean-OS Aug 30, 2025
e80ab33
Merge branch 'parallelize-async-work' of https://github.com/sveltejs/…
Ocean-OS Aug 30, 2025
1b55045
doh
Ocean-OS Aug 30, 2025
8091a6e
more
Ocean-OS Aug 30, 2025
f5972d3
fix
Ocean-OS Aug 30, 2025
fc03ba7
more
Ocean-OS Aug 30, 2025
ccc9939
fix
Ocean-OS Aug 30, 2025
9ab08aa
fix
Ocean-OS Aug 30, 2025
1c2ea75
make it work in dev
Ocean-OS Aug 30, 2025
c8cc931
try this
Ocean-OS Aug 30, 2025
464d7b6
remove unused
Ocean-OS Aug 30, 2025
7788c30
try this
Ocean-OS Aug 30, 2025
c17a1e3
cleanup
Ocean-OS Aug 30, 2025
c41900c
more
Ocean-OS Aug 30, 2025
dad4889
this probably isn't necessary?
Ocean-OS Aug 31, 2025
b138c60
remove unused param
Ocean-OS Aug 31, 2025
82cb515
parallelize `await` expressions in instance
Ocean-OS Aug 31, 2025
d399d8a
fix
Ocean-OS Aug 31, 2025
a819a67
fix?
Ocean-OS Aug 31, 2025
01d6a75
try parallelizing awaited `$state`
Ocean-OS Aug 31, 2025
41eeaba
eh that'll work for now
Ocean-OS Aug 31, 2025
24164f9
fix
Ocean-OS Aug 31, 2025
eb61f7a
tweak
Ocean-OS Aug 31, 2025
4ee33d4
ok *this* should fix it
Ocean-OS Aug 31, 2025
1080beb
remove unused
Ocean-OS Aug 31, 2025
e2c32f9
dedupe parallelized `await` stmts
Ocean-OS Aug 31, 2025
06d43df
make `$.all` a stmt if all awaits are stmts
Ocean-OS Aug 31, 2025
d4d8253
Merge branch 'main' into parallelize-async-work
Ocean-OS Aug 31, 2025
229ab37
tweak
Ocean-OS Sep 5, 2025
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/spicy-hotels-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: parallelize more async work
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,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_chunks: [],
current_parallelized_chunk: null
};

const module = /** @type {ESTree.Program} */ (
Expand Down
18 changes: 16 additions & 2 deletions packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import type {
AssignmentExpression,
UpdateExpression,
VariableDeclaration,
Declaration
Pattern
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js';
Expand Down Expand Up @@ -83,6 +83,20 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
/** async deriveds and certain awaited variables are chunked so they can be parallelized via `Promise.all` */
readonly parallelized_chunks: ParallelizedChunk[];
current_parallelized_chunk: ParallelizedChunk | null;
}

export interface ParallelizedChunk {
declarators: Array<{
id: Pattern | null;
init: Expression;
}>;
kind: VariableDeclaration['kind'] | null;
/** index in instance body */
position: number;
bindings: Binding[];
}

export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
Expand Down
78 changes: 76 additions & 2 deletions packages/svelte/src/compiler/phases/3-transform/client/utils.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -28,6 +29,79 @@ export function is_state_source(binding, analysis) {
);
}

/**
* @param {Expression} expression
* @param {Scope} scope
* @param {Analysis | ComponentAnalysis} analysis
* @param {Binding[]} bindings bindings currently being parallelized (and cannot be accessed)
* @returns {boolean}
*/
export function can_be_parallelized(expression, scope, analysis, bindings) {
let should_stop = false;
/** @type {Set<string>} */
const references = new Set();
walk(/** @type {Node} */ (expression), null, {
ArrowFunctionExpression(_, { stop }) {
should_stop = true;
stop();
},
FunctionExpression(_, { stop }) {
should_stop = true;
stop();
},
Identifier(node, { path }) {
if (is_reference(node, /** @type {Node} */ (path.at(-1)))) {
references.add(node.name);
}
},
MemberExpression(node, { stop }) {
should_stop = true;
stop();
},
CallExpression(node, { stop }) {
should_stop = true;
stop();
},
NewExpression(node, { stop }) {
should_stop = true;
stop();
},
StaticBlock(node, { stop }) {
should_stop = true;
stop();
}
});
if (should_stop) {
return false;
}
for (const reference of references) {
const binding = scope.get(reference);
if (!binding || binding.declaration_kind === 'import') {
return false;
}
if (binding.scope !== analysis.module.scope) {
if (!('template' in analysis)) {
return false;
}
if (binding.scope !== analysis.instance.scope) {
return false;
}
}

if (bindings.includes(binding)) {
return false;
}

if (binding.kind === 'derived') {
const init = /** @type {CallExpression} */ (binding.initial);
if (analysis.async_deriveds.has(init)) {
return false;
}
}
}
return true;
}

/**
* @param {Identifier} node
* @param {ClientTransformState} state
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
/** @import { Expression, ExpressionStatement, Node, Program } from 'estree' */
/** @import { ComponentContext, ParallelizedChunk } from '../types' */
import * as b from '#compiler/builders';
import { is_expression_async } from '../../../../utils/ast.js';
import { get_rune } from '../../../scope.js';
import { can_be_parallelized } from '../utils.js';

/**
* @param {ExpressionStatement} node
* @param {ComponentContext} context
*/
export function ExpressionStatement(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
const position = /** @type {Program} */ (parent).body?.indexOf?.(node);
if (node.expression.type === 'CallExpression') {
const rune = get_rune(node.expression, context.state.scope);

Expand All @@ -25,6 +29,40 @@ export function ExpressionStatement(node, context) {
return b.empty;
}
}
if (
node.expression.type === 'AwaitExpression' &&
!is_expression_async(node.expression.argument) &&
context.state.analysis.instance?.scope === context.state.scope
) {
const current_chunk = context.state.current_parallelized_chunk;
const parallelize = can_be_parallelized(
node.expression.argument,
context.state.scope,
context.state.analysis,
current_chunk?.bindings ?? []
);
if (parallelize) {
const declarator = {
id: null,
init: /** @type {Expression} */ (context.visit(node.expression.argument))
};
if (current_chunk) {
current_chunk.declarators.push(declarator);
current_chunk.position = position;
} else {
/** @type {ParallelizedChunk} */
const chunk = {
kind: null,
declarators: [declarator],
position,
bindings: []
};
context.state.current_parallelized_chunk = chunk;
context.state.parallelized_chunks.push(chunk);
}
return b.empty;
}
}

context.next();
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types' */
/** @import { ComponentContext } from '../types' */
import is_reference from 'is-reference';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';

/**
* @param {Identifier} node
* @param {Context} context
* @param {ComponentContext} context
*/
export function Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
Expand Down Expand Up @@ -35,6 +35,9 @@ export function Identifier(node, context) {
return b.id('$$props');
}
}
if (binding && context.state.current_parallelized_chunk?.bindings?.includes(binding)) {
context.state.current_parallelized_chunk = null;
}

return build_getter(node, context.state);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down Expand Up @@ -137,5 +137,52 @@ 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]));
body.push(transformed);
}
if (context.state.parallelized_chunks) {
let offset = 0;
for (const chunk of context.state.parallelized_chunks) {
if (chunk.declarators.length === 1) {
const declarator = chunk.declarators[0];
if (declarator.id === null || chunk.kind === null) {
body.splice(
chunk.position + offset,
0,
b.stmt(b.call(b.await(b.call('$.save', declarator.init))))
);
} else {
body.splice(
chunk.position + offset,
0,
b.declaration(chunk.kind, [
b.declarator(declarator.id, b.call(b.await(b.call('$.save', declarator.init))))
])
);
}
} else {
const pattern = b.array_pattern(chunk.declarators.map(({ id }) => id));
const init = b.call(
b.await(b.call('$.save', b.call('$.all', ...chunk.declarators.map(({ init }) => init))))
);
if (pattern.elements.every((element) => element === null)) {
body.splice(chunk.position + offset, 0, b.stmt(init));
} else {
body.splice(
chunk.position + offset,
0,
b.declaration(chunk.kind ?? 'const', [b.declarator(pattern, init)])
);
}
}
offset++;
}
}
return {
...node,
body
};
}
Loading
Loading