perf: better effect pruning

#16625

Pull request

Open
R
Rich-Harris
Aug 14, 2025, 9:25 PM

We prune effects that have no dependencies and no children. But in many cases we can be more aggressive — if we have an {#if browser} block, for example, browser is never going to change and so it's useless to add the block effect to the effect tree, other than to indirectly add its children.

Another example is an attachment that doesn't read anything reactive (i.e. {@attach (node) => {...}}, which is probably 99% of cases). Today, if you use log_effect_tree to look at the effect tree immediately after mounting this component...

<input
  value='hello'
  {@attach (node) => node.select()}
/>

{#if true}
  <p>it's true</p>
{/if}

...you see this:

image

After this PR, it looks like this — notice the intermediate block effects have been replaced by their children:

image

This means less memory pressure (effects can be garbage collected), and faster traversal. Though admittedly it's hard to see a clear win in the benchmark results:

Results of `pnpm bench:compare`
sbench_create_signals
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  261.31ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 279.61ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼   56.63ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 63.94ms
sbench_create_0to1
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 6.99ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼     5.60ms
sbench_create_1to1
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 20.06ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 20.07ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 2.02ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 2.04ms
sbench_create_2to1
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 17.44ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 17.35ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 2.07ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.93ms
sbench_create_4to1
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 16.07ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 16.07ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 2.03ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.96ms
sbench_create_1000to1
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 14.35ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 14.25ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.99ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 2.01ms
sbench_create_1to2
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 9.09ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 9.15ms
sbench_create_1to4
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 7.75ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 7.66ms
sbench_create_1to8
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 6.78ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  6.59ms
sbench_create_1to1000
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 7.35ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 7.40ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 0.21ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 0.21ms
kairo_avoidable_owned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 511.49ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 505.36ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼    17.24ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 19.71ms
kairo_avoidable_unowned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 600.08ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 594.71ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 19.25ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 19.15ms
kairo_broad_owned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 489.90ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 489.56ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.86ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼   1.71ms
kairo_broad_unowned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 482.70ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 484.67ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.90ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.84ms
kairo_deep_owned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 209.22ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 210.22ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.21ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼   1.10ms
kairo_deep_unowned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 898.47ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 899.15ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.09ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.09ms
kairo_diamond_owned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 421.64ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 423.07ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  9.04ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 9.35ms
kairo_diamond_unowned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 490.68ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 499.09ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 9.36ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 9.15ms
kairo_triangle_owned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 134.03ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 134.58ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.68ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.61ms
kairo_triangle_unowned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 230.25ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 230.06ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.85ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.72ms
kairo_mux_owned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 324.89ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 324.79ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.43ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.48ms
kairo_mux_unowned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 452.41ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 458.13ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.43ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.42ms
kairo_repeated_owned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 76.93ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 76.88ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.38ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.31ms
kairo_repeated_unowned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 74.53ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 74.47ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.38ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.33ms
kairo_unstable_owned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 116.03ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 116.20ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼  1.21ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.26ms
kairo_unstable_unowned
  time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 133.59ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 134.06ms
  gc_time: fastest is a (better-effect-pruning)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.24ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 1.27ms
mol_bench_owned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 308.95ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 304.83ms
mol_bench_unowned
  time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 324.30ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 322.13ms
  gc_time: fastest is b (main)
    a: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ 0.42ms
    b: ◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼   0.37ms

I suspect this is a case where a benchmark fails to capture the reality of a large application.

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Info

Assignees None
Reviewers None
Labels None
Milestone None