perf: don't reschedule the whole batch on writes
#16612
Closing issue
Describe the bug
While investigating async compat issues with LayerChart, I noticed after bumping Svelte from 5.34.1 to 5.35.4 (and beyond) begins to throw effect_update_depth_exceeded errors we had not prior.
I believe this stems from how we setup motion state / value tracking. Let me know if this should be handled differently, or is in fact a regression. The only changeset for 5.35.4 is:
fix: abort and reschedule effect processing after state change in user effect
As always, thanks for all the hard work!
Reproduction
- Checkout LayerChart PR 629 /
update-depsbranch pnpm installpnpm dev- Open the Area example (or many others with motion such as Bars and Pack). For even more taxing examples, view Force Disjoint Graph, Force Tree, or Force Lattice)
Logs
client:733 [vite] connecting...
client:827 [vite] connected.
Area:24 Last ten effects were: (10) [null, ƒ, ƒ, ƒ, null, ƒ, ƒ, ƒ, ƒ, ƒ]0: null1: () => { motion.set(getValue()); }2: () => { motion.set(getValue()); }3: () => {…}4: null5: () => { motion.set(getValue()); }6: () => { refProp($.get(ref)); }7: () => { refProp($.get(ref)); }8: () => { svgRefProp($.get(svgRef)); }9: () => { motion.set(getValue()); }length: 10[[Prototype]]: Array(0)
log_effect_stack @ chunk-REG52R6L.js?v=13908794:1877
infinite_loop_guard @ chunk-REG52R6L.js?v=13908794:1897
flush_queued_root_effects @ chunk-REG52R6L.js?v=13908794:1918
flushSync @ chunk-REG52R6L.js?v=13908794:2034
Svelte4Component @ chunk-Q6R3MBGE.js?v=13908794:839
(anonymous) @ chunk-Q6R3MBGE.js?v=13908794:789
initialize @ client.js?v=13908794:528
navigate @ client.js?v=13908794:1643
await in navigate
start @ client.js?v=13908794:340
await in start
(anonymous) @ Area:24
Promise.then
(anonymous) @ Area:23
chunk-REG52R6L.js?v=13908794:1877 Last ten effects were: (10) [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, null, ƒ, ƒ, ƒ]
log_effect_stack @ chunk-REG52R6L.js?v=13908794:1877
infinite_loop_guard @ chunk-REG52R6L.js?v=13908794:1897
flush_queued_root_effects @ chunk-REG52R6L.js?v=13908794:1918
chunk-REG52R6L.js?v=13908794:199 Uncaught Svelte error: effect_update_depth_exceeded
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
https://svelte.dev/e/effect_update_depth_exceeded
effect_update_depth_exceeded @ chunk-REG52R6L.js?v=13908794:199
infinite_loop_guard @ chunk-REG52R6L.js?v=13908794:1885
flush_queued_root_effects @ chunk-REG52R6L.js?v=13908794:1918
chunk-REG52R6L.js?v=13908794:199 Uncaught (in promise) Svelte error: effect_update_depth_exceeded
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
https://svelte.dev/e/effect_update_depth_exceeded
effect_update_depth_exceeded @ chunk-REG52R6L.js?v=13908794:199
infinite_loop_guard @ chunk-REG52R6L.js?v=13908794:1885
flush_queued_root_effects @ chunk-REG52R6L.js?v=13908794:1918
flushSync @ chunk-REG52R6L.js?v=13908794:2034
Svelte4Component @ chunk-Q6R3MBGE.js?v=13908794:839
(anonymous) @ chunk-Q6R3MBGE.js?v=13908794:789
initialize @ client.js?v=13908794:528
navigate @ client.js?v=13908794:1643
await in navigate
start @ client.js?v=13908794:340
await in start
(anonymous) @ Area:24
Promise.then
(anonymous) @ Area:23
System Info
System:
OS: macOS 15.5
CPU: (10) arm64 Apple M1 Max
Memory: 109.75 MB / 32.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.17.0 - ~/Library/Caches/fnm_multishells/70747_1754315319738/bin/node
npm: 10.9.2 - ~/Library/Caches/fnm_multishells/70747_1754315319738/bin/npm
pnpm: 9.1.1 - ~/Library/pnpm/pnpm
bun: 1.1.33 - ~/.bun/bin/bun
Browsers:
Brave Browser: 136.1.78.102
Chrome: 138.0.7204.184
Safari: 18.5
npmPackages:
svelte: 5.35.4 => 5.35.4
Severity
blocking an upgrade
Pull request
This makes flushing effects much faster in scenarios where there's a lot of effects and many of them write to state.
We added this "bail and reschedule" mechanism mainly to prevent subsequent effects from being called when a prior effect execution did e.g. cause an if block to become false and the effect about to run next therefore being destroyed.
This change addresses that issue by flushing the newly created batch right away, but only executing its branches. Through this we also get rid of the false-positive loop protection error.
The one downside is that effect execution ordering is slightly different: Only things scheduled within the current batch are executed in order, all new (pre-)effects will run afterwards. The worst thing that can happen as a result is rerunning render or user effects more often than they need to, but I expect this to be minimal in comparison to the perf wins.
Fixes #16548
If we agree on the fix being sensible I'll adjust the tests, add some comments etc
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:, ordocs:. - 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 testand lint the project withpnpm lint
Info
⚠️ No Changeset found
Latest commit: e11e381
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
This PR includes no changesets
When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
pnpm add https://pkg.pr.new/svelte@16612closing in favor of #16631
This makes flushing effects much faster in scenarios where there's a lot of effects and many of the write to state. We added this "bail and reschedule" mechanism mainly to prevent subsequent effects from being called when a prior effect execution did e.g. cause an if block to become false and the effect about to run next therefore being destroyed. This change addresses that issue by flushing the newly created batch right away, but only executing its branches. Through this we also get rid of the false-positive loop protection error. The one downside is that effect execution ordering is slightly different: Only things scheduled within the current batch are executed in order, all new (pre-)effects will run afterwards. The worst thing that can happen as a result is rerunning render or user effects more often than they need to, but I expect this to be minimal in comparison to the perf wins. Fixes #16548