A developer from Oslo opened a GitHub issue three days before this PR. His question was simple: "What's Svelte's roadmap on improving tool performance?"
Rich Harris's answer was shorter: no formal roadmap. Profile it. Fix what you find.
So Mathias Picker did exactly that. He profiled the Svelte compiler, found two algorithmic problems hiding at the intersection of two subsystems, and opened a pull request that touched 3 code files.
Rich Harris's review: "fantastic!"
The PR shipped the same day. Svelte's compiler analysis phase got 20-55% faster. One of those files, state.js, is the shared global state imported by 30 files across all three compiler phases. Change it, and 2,036 downstream files are in the blast radius.
GitHub said 4 files changed. The dependency graph said 2,036.
The PR
sveltejs/svelte#17823 — "perf: optimize compiler analysis phase"
Svelte's compiler works in three phases: parse (text to AST), analyze (extract meaning), and transform (generate JavaScript). The analysis phase figures out scoping, reactivity, and CSS pruning.
Two problems were hiding inside it.
Problem 1: CSS pruning walked the stylesheet once per element
The compiler needs to determine which CSS rules actually apply to which elements. Unused rules get pruned. The old code looped over every element and walked the entire CSS AST each time:
// Before: O(n × m), n elements, m CSS rules
for (const node of analysis.elements) {
prune(analysis.css.ast, node); // walks entire stylesheet each time
}With 50 elements and 100 CSS rules, that's 50 full tree walks over the stylesheet AST. The fix inverts the loop. Walk the stylesheet once, match elements inside each selector:
// After: one walk, elements matched inside
prune(analysis.css.ast, analysis.elements);Problem 2: Deep-cloning a stack on every AST node
Svelte supports <!-- svelte-ignore --> comments that suppress warnings. The compiler tracks these in a stack. The old code called structuredClone(ignore_stack) on every single AST node, even though ignore comments appear maybe 0-5 times per component:
// Before: deep-clone on every AST node visit
ignore_map.set(node, structuredClone(ignore_stack));A typical component has 500-2,000 AST nodes. That's 500-2,000 deep clones of an array that almost never changes. The fix is in state.js. Cache the snapshot and only rebuild when push_ignore or pop_ignore actually changes the stack:
export function get_ignore_snapshot() {
if (cached_ignore_snapshot === null) {
cached_ignore_snapshot = ignore_stack.map((s) => new Set(s));
}
return cached_ignore_snapshot;
}Results across 500 compilations:
| Component | Before | After | Speedup |
|---|---|---|---|
| 80+ selectors, 12 elements | 3.405 ms | 2.680 ms | 21% |
| Nested each blocks | 2.034 ms | 1.575 ms | 23% |
| 100 rules, 50 elements | 10.099 ms | 4.564 ms | 55% |
What the dependency graph shows
A GitHub diff won't show you where these 3 files sit in Svelte's architecture. (The fourth changed file is a .changeset metadata entry, not code.)

The three code files live in different layers of the compiler:
state.jssits at the compiler root (packages/svelte/src/compiler/state.js). It holds shared global state (warnings, filename, source, ignore stack) and is imported by 30 files across all three compiler phases. Parse, analyze, transform all reach intostate.js.index.jsis the analysis phase entry point (phases/2-analyze/index.js). It orchestrates scoping, reactivity, and CSS pruning. Every component Svelte compiles passes through this file.css-prune.jshandles CSS dead-code elimination (phases/2-analyze/css/css-prune.js). It figures out which CSS rules apply to which elements and marks the rest for removal.
Select state.js in the 3D graph and expand the blast radius. 30 direct importers across all three phases. Those feed into phase entry points, which feed into the compiler root, which feeds into every test harness. End result: 2,036 of 3,301 files light up. That's 62% of the Svelte codebase, from a change to 3 files.

state.js is the most connected file Mathias touched. It's not in the analysis phase at all. It's below it, shared by everything. The code paths he fixed run on every AST node of every component, inside a module that the entire compiler depends on.
The bigger story
Mathias didn't stop at one PR. Over eight days, he opened five performance pull requests:
| PR | What it fixed | Speedup | Explore |
|---|---|---|---|
| #17811 | Parser hot paths | 18% faster | View in 3D |
| #17823 | Analysis phase (this PR) | 21-55% faster | View in 3D |
| #17839 | Element interactivity caching | ~8% faster | — |
| #17844 | O(n²) scope name scanning | ~10% faster | View in 3D |
| #17846 | CSS selector pruning | ~16% faster | View in 3D |
All merged by Rich Harris. All shipped within days.
It started with one issue: "What's Svelte's roadmap on improving tool performance?" Mathias grabbed a profiler and answered it himself.
See it yourself
Click any node. Rotate the graph. Select a changed file and watch the blast radius expand through the depth rings.
Want this on every PR your team opens? The CodeLayers GitHub Action posts an interactive 3D visualization as a comment — two-minute setup, runs on every push.
Blast Radius is a weekly series where we visualize real open-source PRs as 3D dependency graphs. Got a PR you want us to look at? Tell us on Bluesky — we'll run it through the graph.