Skip to content
4 min read

Blast Radius #1: The Svelte Compiler Got 55% Faster. The Fix Was 3 Files.

A developer asked Svelte about performance. Then he fixed it himself: 5 PRs in 8 days. The dependency graph reveals why his 3-file fix touched 2,036 files.

By CodeLayers Team

Blast Radius #1: The Svelte Compiler Got 55% Faster. The Fix Was 3 Files.

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:

ComponentBeforeAfterSpeedup
80+ selectors, 12 elements3.405 ms2.680 ms21%
Nested each blocks2.034 ms1.575 ms23%
100 rules, 50 elements10.099 ms4.564 ms55%

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.)

Full Impact view showing blast radius across all depth rings

The three code files live in different layers of the compiler:

  • state.js sits 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 into state.js.
  • index.js is 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.js handles 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.

Zoomed in on the analysis phase cluster with selected file panel

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:

PRWhat it fixedSpeedupExplore
#17811Parser hot paths18% fasterView in 3D
#17823Analysis phase (this PR)21-55% fasterView in 3D
#17839Element interactivity caching~8% faster
#17844O(n²) scope name scanning~10% fasterView in 3D
#17846CSS selector pruning~16% fasterView 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

Explore the Svelte PR in 3D

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.

Want to see your code in 3D?

Download on the App Store

Get notified about updates and new features: