Skip to content

perf: make async blocker analysis linear - #18549

Merged
dummdidumm merged 5 commits into
sveltejs:mainfrom
jschaf:joe/x/linear-blocker-analysis
Aug 20, 2026
Merged

dummdidumm merged 5 commits into
sveltejs:mainfrom
jschaf:joe/x/linear-blocker-analysis

Conversation

@jschaf

@jschaf jschaf commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

trace_references recreated a fresh seen set for every CallExpression, so touch re-walked the same transitive assignment graph once per call. For N calls reaching an N-deep binding chain, this was $O(N^2)$.

Share one seen set per trace_references invocation. Use separate sets for the write-directed CallExpression touches and the read-directed ReturnStatement touches. The shared set ensures each assignment-value expression is walked at most once, making the traversal linear. touch only ever adds to a fixed target set, so skipping an already-seen expression never drops a binding: compiler output is byte-identical.

calculate_blockers performance

Apple M4 Max; Node 24.18.0; pnpm 10.4.0; Hyperfine 1.20.0. Each command used 3 warmups and 15 measured runs. Hyperfine timed batched compiler processes; the matching compiler-import/source-generation baseline was subtracted, then the mean was divided by the batch size.

Assignments / calls Before (ms) After (ms) Speedup
50 1.73 1.17 1.5×
100 4.43 2.28 1.9×
200 11.42 4.38 2.6×
400 37.26 8.53 4.4×
800 183.95 20.72 8.9×
Benchmark code and Hyperfine commands

Save as $BASE/blocker-hyperfine.mjs:

Before: 602a873b6b82bba4e6edc91b039e2f6defbe3fc4
After: ca8cb55e23cd15d626a4ca01aa5ed073a689b519

import { readFile } from 'node:fs/promises';
import process from 'node:process';
import { pathToFileURL } from 'node:url';

const iterations_by_size = new Map([
	[50, 150],
	[100, 80],
	[200, 50],
	[400, 25],
	[800, 15]
]);

function component(size) {
	const assignments = Array.from(
		{ length: size },
		(_, i) => `let value_${i + 1} = value_${i};`
	).join('\n\t');
	const calls = Array.from({ length: size }, () => `sink(value_${size});`).join('\n\t\t');

	return `<script>
	let value_0 = await Promise.resolve(0);
	${assignments}

	function sink(value) {
		return value;
	}

	function run() {
		${calls}
		return value_${size};
	}
</script>

<p>{run()}</p>
`;
}

async function bench(mode, root, size_arg) {
	const size = Number(size_arg);
	const iterations = iterations_by_size.get(size);
	if (iterations === undefined) throw new Error(`Unsupported size: ${size_arg}`);

	const compiler_url = pathToFileURL(`${root}/packages/svelte/src/compiler/index.js`).href;
	const { compile } = await import(compiler_url);
	const source = component(size);

	if (mode === 'baseline') return;
	if (mode !== 'compile') throw new Error(`Unsupported mode: ${mode}`);

	for (let i = 0; i < iterations; i += 1) {
		compile(source, {
			filename: 'Blocker.svelte',
			generate: 'client',
			dev: false,
			experimental: { async: true }
		});
	}
}

async function table(results_dir) {
	console.log('| Assignments / calls | Before (ms) | After (ms) | Speedup |');
	console.log('| ---: | ---: | ---: | ---: |');

	for (const [size, iterations] of iterations_by_size) {
		const result = JSON.parse(await readFile(`${results_dir}/${size}.json`, 'utf8'));
		const means = new Map(result.results.map(({ command, mean }) => [command, mean]));
		const before = ((means.get('before') - means.get('before baseline')) * 1000) / iterations;
		const after = ((means.get('after') - means.get('after baseline')) * 1000) / iterations;

		console.log(
			`| ${size} | ${before.toFixed(2)} | ${after.toFixed(2)} | ${(before / after).toFixed(1)}× |`
		);
	}
}

const [mode, ...args] = process.argv.slice(2);

if ((mode === 'compile' || mode === 'baseline') && args.length === 2) {
	await bench(mode, args[0], args[1]);
} else if (mode === 'table' && args.length === 1) {
	await table(args[0]);
} else {
	throw new Error(
		'Usage: blocker-hyperfine.mjs <compile|baseline> <worktree> <size> | table <results-dir>'
	);
}
REPO=/opt/p/svelte
BASE=$(mktemp -d /tmp/svelte-blocker-hf.XXXXXX)
BEFORE="$BASE/before"
AFTER="$BASE/after"
SCRIPT="$BASE/blocker-hyperfine.mjs"
RESULTS="$BASE/results"

git -C "$REPO" worktree add --detach \
  "$BEFORE" \
  602a873b6b82bba4e6edc91b039e2f6defbe3fc4

git -C "$REPO" worktree add --detach \
  "$AFTER" \
  ca8cb55e23cd15d626a4ca01aa5ed073a689b519

# The compared commits do not change dependencies, so both isolated
# worktrees use the existing pnpm installation.
ln -s "$REPO/node_modules" "$BEFORE/node_modules"
ln -s "$REPO/node_modules" "$AFTER/node_modules"
ln -s "$REPO/packages/svelte/node_modules" "$BEFORE/packages/svelte/node_modules"
ln -s "$REPO/packages/svelte/node_modules" "$AFTER/packages/svelte/node_modules"

mkdir -p "$RESULTS"

mise exec node@24 -- sh -c \
  'node --version && pnpm --version && hyperfine --version'

for SIZE in 50 100 200 400 800; do
  mise exec node@24 -- hyperfine \
    --warmup 3 \
    --runs 15 \
    --style basic \
    --export-json "$RESULTS/$SIZE.json" \
    --command-name before \
      "node $SCRIPT compile $BEFORE $SIZE" \
    --command-name 'before baseline' \
      "node $SCRIPT baseline $BEFORE $SIZE" \
    --command-name after \
      "node $SCRIPT compile $AFTER $SIZE" \
    --command-name 'after baseline' \
      "node $SCRIPT baseline $AFTER $SIZE"
done

mise exec node@24 -- node "$SCRIPT" table "$RESULTS"

git -C "$REPO" worktree remove --force "$BEFORE"
git -C "$REPO" worktree remove --force "$AFTER"
rm -rf "$BASE"
git -C "$REPO" worktree prune

Complements #18548.

trace_references() re-created a fresh seen set for every CallExpression, so
touch() re-walked the same transitive assignment graph once per call. For N
calls reaching an N-deep binding chain this was O(N^2).

Share one seen set per trace_references() invocation (separate sets for the
writes-directed CallExpression touches and the reads-directed ReturnStatement
touches) so each assignment-value expression is walked at most once, making the
traversal linear. touch() only ever adds to a fixed target set, so skipping an
already-seen expression never drops a binding: compiler output is byte-identical.
@pkg-svelte-dev

pkg-svelte-dev Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Install the latest version of svelte from 79ef7d7:

pnpm add https://pkg.svelte.dev/svelte/c/79ef7d744bea869d4ef45cac5f2c0337e28f6a4e

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/svelte/pr/18549

Note

This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed.

@changeset-bot

changeset-bot Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 79ef7d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dummdidumm

Copy link
Copy Markdown
Member

Thank you - though I think we can omit the new tests.

@jschaf

jschaf commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Thank you - though I think we can omit the new tests.

Sure thing, done.

@dummdidumm dummdidumm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you!

there was another PR merged in the meantime which meant I had to add a new Set() but the cases where it's used are rare so should not affect perf.

@dummdidumm
dummdidumm merged commit 545205b into sveltejs:main Aug 20, 2026
10 of 11 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 20, 2026
Rich-Harris pushed a commit that referenced this pull request Aug 20, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## svelte@5.56.10

### Patch Changes

- fix: preserve CSS escape sequences when printing selectors
([#18667](#18667))

- fix: parse `:nth-child(2n of.foo)` where `of` is not followed by
whitespace ([#18611](#18611))

- fix: transform expressions inside labeled statements during server
compilation ([#18617](#18617))

- docs: clarify that context lookup includes the current component and
all ancestors ([#18581](#18581))

- fix: apply CSS custom properties with falsy values on components
([#18634](#18634))

- fix: correctly print `{#await ... catch x}` et al
([#18645](#18645))

- fix: ignore comments of Program node during migration script
([#18656](#18656))

- fix: reliably resolve append_style to its correct root
([#18614](#18614))

- fix: clean up removed capture event handlers from spread attributes
([#18618](#18618))

- fix: don't corrupt renderer type during SSR's legacy `bind:` retry
loop ([#18616](#18616))

- fix: treat concise arrow function bodies as implicit returns when
calculating blockers
([#18613](#18613))

- fix: give effect teardowns the value from before the first write in a
flush ([#18620](#18620))

- fix: avoid double-calling a derived reference when destructuring
`$derived` of another `$derived` during server-side rendering
([#18668](#18668))

- fix: preserve namespaces in CSS type selectors
([#18678](#18678))

- fix: increment private state fields through a non-`this` receiver
([#18622](#18622))

- chore: deduplicate client and server context helpers
([#18580](#18580))

- fix: release `last_propagated_event` after event propagation settles
so it no longer retains the last event's target subtree
([#18569](#18569))

- fix: allow custom elements to receive async values as props
([#18661](#18661))

- fix: strip comments from inline `style` values in linear time
([#18553](#18553))

- fix: prevent declaration comments from breaking server derived
references ([#18641](#18641))

- perf: make async blocker analysis scale linearly with the number of
top-level references
([#18549](#18549))

- fix: preserve short-circuiting for logical assignments to private
state fields ([#18594](#18594))

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dadezzz pushed a commit to dadezzz/events-cash-register that referenced this pull request Aug 25, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [svelte](https://svelte.dev) ([source](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte)) | [`5.56.9` → `5.56.10`](https://renovatebot.com/diffs/npm/svelte/5.56.9/5.56.10) | ![age](https://developer.mend.io/api/mc/badges/age/npm/svelte/5.56.10?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/svelte/5.56.9/5.56.10?slim=true) |

---

### Release Notes

<details>
<summary>sveltejs/svelte (svelte)</summary>

### [`v5.56.10`](https://github.com/sveltejs/svelte/blob/HEAD/packages/svelte/CHANGELOG.md#55610)

[Compare Source](https://github.com/sveltejs/svelte/compare/svelte@5.56.9...svelte@5.56.10)

##### Patch Changes

- fix: preserve CSS escape sequences when printing selectors ([#&#8203;18667](sveltejs/svelte#18667))

- fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace ([#&#8203;18611](sveltejs/svelte#18611))

- fix: transform expressions inside labeled statements during server compilation ([#&#8203;18617](sveltejs/svelte#18617))

- docs: clarify that context lookup includes the current component and all ancestors ([#&#8203;18581](sveltejs/svelte#18581))

- fix: apply CSS custom properties with falsy values on components ([#&#8203;18634](sveltejs/svelte#18634))

- fix: correctly print `{#await ... catch x}` et al ([#&#8203;18645](sveltejs/svelte#18645))

- fix: ignore comments of Program node during migration script ([#&#8203;18656](sveltejs/svelte#18656))

- fix: reliably resolve append\_style to its correct root ([#&#8203;18614](sveltejs/svelte#18614))

- fix: clean up removed capture event handlers from spread attributes ([#&#8203;18618](sveltejs/svelte#18618))

- fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop ([#&#8203;18616](sveltejs/svelte#18616))

- fix: treat concise arrow function bodies as implicit returns when calculating blockers ([#&#8203;18613](sveltejs/svelte#18613))

- fix: give effect teardowns the value from before the first write in a flush ([#&#8203;18620](sveltejs/svelte#18620))

- fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering ([#&#8203;18668](sveltejs/svelte#18668))

- fix: preserve namespaces in CSS type selectors ([#&#8203;18678](sveltejs/svelte#18678))

- fix: increment private state fields through a non-`this` receiver ([#&#8203;18622](sveltejs/svelte#18622))

- chore: deduplicate client and server context helpers ([#&#8203;18580](sveltejs/svelte#18580))

- fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree ([#&#8203;18569](sveltejs/svelte#18569))

- fix: allow custom elements to receive async values as props ([#&#8203;18661](sveltejs/svelte#18661))

- fix: strip comments from inline `style` values in linear time ([#&#8203;18553](sveltejs/svelte#18553))

- fix: prevent declaration comments from breaking server derived references ([#&#8203;18641](sveltejs/svelte#18641))

- perf: make async blocker analysis scale linearly with the number of top-level references ([#&#8203;18549](sveltejs/svelte#18549))

- fix: preserve short-circuiting for logical assignments to private state fields ([#&#8203;18594](sveltejs/svelte#18594))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zMS4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzEuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
renovate Bot added a commit to cigaleapp/cigale that referenced this pull request Aug 30, 2026
##### [v5.56.10](https://github.com/sveltejs/svelte/blob/HEAD/packages/svelte/CHANGELOG.md#55610)

##### Patch Changes

- fix: preserve CSS escape sequences when printing selectors ([#18667](sveltejs/svelte#18667))

- fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace ([#18611](sveltejs/svelte#18611))

- fix: transform expressions inside labeled statements during server compilation ([#18617](sveltejs/svelte#18617))

- docs: clarify that context lookup includes the current component and all ancestors ([#18581](sveltejs/svelte#18581))

- fix: apply CSS custom properties with falsy values on components ([#18634](sveltejs/svelte#18634))

- fix: correctly print `{#await ... catch x}` et al ([#18645](sveltejs/svelte#18645))

- fix: ignore comments of Program node during migration script ([#18656](sveltejs/svelte#18656))

- fix: reliably resolve append\_style to its correct root ([#18614](sveltejs/svelte#18614))

- fix: clean up removed capture event handlers from spread attributes ([#18618](sveltejs/svelte#18618))

- fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop ([#18616](sveltejs/svelte#18616))

- fix: treat concise arrow function bodies as implicit returns when calculating blockers ([#18613](sveltejs/svelte#18613))

- fix: give effect teardowns the value from before the first write in a flush ([#18620](sveltejs/svelte#18620))

- fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering ([#18668](sveltejs/svelte#18668))

- fix: preserve namespaces in CSS type selectors ([#18678](sveltejs/svelte#18678))

- fix: increment private state fields through a non-`this` receiver ([#18622](sveltejs/svelte#18622))

- chore: deduplicate client and server context helpers ([#18580](sveltejs/svelte#18580))

- fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree ([#18569](sveltejs/svelte#18569))

- fix: allow custom elements to receive async values as props ([#18661](sveltejs/svelte#18661))

- fix: strip comments from inline `style` values in linear time ([#18553](sveltejs/svelte#18553))

- fix: prevent declaration comments from breaking server derived references ([#18641](sveltejs/svelte#18641))

- perf: make async blocker analysis scale linearly with the number of top-level references ([#18549](sveltejs/svelte#18549))

- fix: preserve short-circuiting for logical assignments to private state fields ([#18594](sveltejs/svelte#18594))
renovate Bot added a commit to cigaleapp/cigale that referenced this pull request Aug 30, 2026
##### [v5.56.10](https://github.com/sveltejs/svelte/blob/HEAD/packages/svelte/CHANGELOG.md#55610)

##### Patch Changes

- fix: preserve CSS escape sequences when printing selectors ([#18667](sveltejs/svelte#18667))

- fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace ([#18611](sveltejs/svelte#18611))

- fix: transform expressions inside labeled statements during server compilation ([#18617](sveltejs/svelte#18617))

- docs: clarify that context lookup includes the current component and all ancestors ([#18581](sveltejs/svelte#18581))

- fix: apply CSS custom properties with falsy values on components ([#18634](sveltejs/svelte#18634))

- fix: correctly print `{#await ... catch x}` et al ([#18645](sveltejs/svelte#18645))

- fix: ignore comments of Program node during migration script ([#18656](sveltejs/svelte#18656))

- fix: reliably resolve append\_style to its correct root ([#18614](sveltejs/svelte#18614))

- fix: clean up removed capture event handlers from spread attributes ([#18618](sveltejs/svelte#18618))

- fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop ([#18616](sveltejs/svelte#18616))

- fix: treat concise arrow function bodies as implicit returns when calculating blockers ([#18613](sveltejs/svelte#18613))

- fix: give effect teardowns the value from before the first write in a flush ([#18620](sveltejs/svelte#18620))

- fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering ([#18668](sveltejs/svelte#18668))

- fix: preserve namespaces in CSS type selectors ([#18678](sveltejs/svelte#18678))

- fix: increment private state fields through a non-`this` receiver ([#18622](sveltejs/svelte#18622))

- chore: deduplicate client and server context helpers ([#18580](sveltejs/svelte#18580))

- fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree ([#18569](sveltejs/svelte#18569))

- fix: allow custom elements to receive async values as props ([#18661](sveltejs/svelte#18661))

- fix: strip comments from inline `style` values in linear time ([#18553](sveltejs/svelte#18553))

- fix: prevent declaration comments from breaking server derived references ([#18641](sveltejs/svelte#18641))

- perf: make async blocker analysis scale linearly with the number of top-level references ([#18549](sveltejs/svelte#18549))

- fix: preserve short-circuiting for logical assignments to private state fields ([#18594](sveltejs/svelte#18594))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants