Describe the bug
A component with a deep reactive graph (here layerchart's BarChart) whose data comes from a remote query that the page also awaits locks the browser's main thread. There is no error, no failed boundary and no recovery short of a reload. The update never lands and the tab never converges. A press that does not wedge leaves the chart on its old data, and the next one wedges. Sometimes the first press already does.
This is a regression in SvelteKit 3. The same page, same layerchart, same svelte, same vite, is fine on @sveltejs/kit 2.64.0. inside is the mode that freezes, plain is a control that redraws the same chart from a plain awaited promise:
|
svelte 5.56.7 |
svelte 5.56.9 |
@sveltejs/kit 2.64.0 |
inside fine, ~30ms/press |
inside fine, ~30ms/press |
@sveltejs/kit 3.0.0-next.23 |
inside FROZEN |
inside FROZEN |
It tracks the Kit version, not the Svelte version. Raw output is logs/version-matrix.txt in the reproduction, produced by logs/version-matrix.sh. One caveat on that table: under Kit 2 the latched mode renders no chart at all (its $effect latch never populates), so inside is the only like-for-like cell across the rows. It is also the shape the real application used.
It also needs three ingredients together, which is what took a while to pin down. Two neighbouring shapes are fine and look like counter-examples:
| Chart data comes from |
Awaited region on the page |
Result |
a plain Promise, awaited in the boundary |
yes (the promise) |
fine |
local $state |
yes (a remote query) |
fine |
the remote query's value, via an $effect latch |
yes (the same query) |
freeze |
| the remote query's value, awaited in the boundary |
yes (the same query) |
freeze |
So it is not "an await on the page". The second row awaits a remote query that re-resolves on every interaction, right beside a chart that redraws on every interaction, and stays responsive. And it is not "an awaited promise feeding a chart". The first row does exactly that, redraws every time, and is fine. It is specifically the chart's data being downstream of a remote query the page awaits.
Reproduction
https://github.com/half2me/svelte-layerchart-bug-repro
pnpm install
pnpm dev # http://localhost:5199
Pick a mode with the radio buttons, then press bump a few times. node verify.mjs drives all four:
plain 33ms/11bars / 24ms/12bars / 29ms/13bars / 28ms/14bars / 28ms/15bars
beside 113ms/11bars / 88ms/12bars / 98ms/13bars / 96ms/14bars / 93ms/15bars
latched FROZEN
inside FROZEN
The bar count is part of the evidence. The data is 10 + clicks rows, so 11bars, 12bars and so on is how you know the chart really redrew rather than the mode quietly doing nothing. In the failing modes a press that does not wedge leaves the chart at 10bars, meaning the update never landed at all, and the next press wedges.
Logs
node instrument.mjs counts recomputations inside Svelte's client runtime. instrument/patch-svelte.mjs adds counters to update_derived and is_dirty (Vite serves Svelte from source in dev, and the probe keeps streaming after the tab wedges). Counters are reset before each press, so each line is the cost of that one press. Full output in logs/instrument.txt:
=== mode: plain ===
page load update_derived=3430 is_dirty=723853
click 1 responded in 31ms update_derived= 1170 is_dirty= 95968 bars=11
click 2 responded in 21ms update_derived= 1041 is_dirty= 95513 bars=12
click 3 responded in 27ms update_derived= 1152 is_dirty= 101858 bars=13
=== mode: beside ===
page load update_derived=3431 is_dirty=724198
click 1 responded in 102ms update_derived= 1189 is_dirty= 99435 bars=11
click 2 responded in 79ms update_derived= 1057 is_dirty= 98798 bars=12
=== mode: inside ===
page load update_derived=3430 is_dirty=723850
click 1 WEDGED. Tab stopped answering, watching the probe stream.
[probe] update_derived=250000 is_dirty=1834196 t=4953ms
[probe] update_derived=500000 is_dirty=3593596 t=7067ms
[probe] update_derived=1000000 is_dirty=6991365 t=10267ms
[probe] update_derived=2000000 is_dirty=13787272 t=16548ms
[probe] update_derived=3000000 is_dirty=20581544 t=23011ms
[probe] update_derived=4500000 is_dirty=30774685 t=32287ms
still wedged after 25s
About 1,100 derived recomputations for a press that works, against 4.5 million and climbing for one that does not, with no sign of converging.
Which deriveds, at the 1,000,000 mark:
[probe:top] recomputations derived
[probe:top] 687650 () => this._getStackConfig()
[probe:top] 137026 () => { const config = get(this.#stackConfig); if (!config || !config.layout.startsWith("stack")) re
[probe:top] 87965 () => context().series.divergingEdgeKeys ? context().series.divergingEdgeKeys.has(get(s).key) ? "edg
[probe:top] 40255 () => { const propsData = this.props.data; if (equals(propsData, null, false) && (!Array.isArray(pro
[probe:top] 17593 () => extractLayerProps(restProps, "lc-bars-bar")
[probe:top] 12580 () => exclude_from_object(props, [ "ssr", "pointerEvents", "width", "height", "position", "children"
[probe:top] 6227 () => this.resolveDomain("y")
and a stack sample from inside the loop:
[probe:stack]
at update_derived (svelte/src/internal/client/reactivity/deriveds.js)
at get (svelte/src/internal/client/runtime.js)
at layerchart.js:3551
at update_reaction (svelte/src/internal/client/runtime.js)
at execute_derived (svelte/src/internal/client/reactivity/deriveds.js)
at update_derived (svelte/src/internal/client/reactivity/deriveds.js)
at get (svelte/src/internal/client/runtime.js)
at SeriesState.getStackValue (layerchart.js:3613)
Everything hot is layerchart's own chart-context deriveds re-entering themselves, with the is_dirty walk on top. Nothing in application code is looping. The app's own deriveds each recompute a handful of times while the tab is stuck.
Unwrapping the query into a plain promise first, getRows(args).then((r) => $state.snapshot(r)), does not help either, which suggests the cause is the query's own reactive machinery rather than the value it hands on.
Things that do not help
From the original application, all still freeze:
- moving the chart out of the boundary into a sibling branch of the page
- feeding it from
$state latched in an $effect, so its update lands after the batch
- remounting with
{#key} instead of updating
renderContext="canvas" (one canvas draw instead of a component per bar)
- extracting the awaiting markup into a child component
layerchart@2.2.0 instead of 2.0.2 (better, still freezes)
The only thing that resolves it is not awaiting that query on that page: reading it through .current, latching it, and rendering from the latch, which costs the page its server-rendered content.
What I could not reduce
I could not get below layerchart. A synthetic component with a comparably deep derived DAG (depth 12, fan-in 3, every node returning a fresh object, 90 leaf consumers), with and without an $effect measurement cycle, does not reproduce it in any mode. So I cannot rule out that layerchart is doing something unusual. But the version matrix isolates the SvelteKit half: same chart, same data, same presses, and only the Kit version differs.
Related
Filed here rather than added to those because the bisect points at Kit: the same Svelte builds are fine under Kit 2.64.0. Happy to move it if that reading is wrong.
System Info
@sveltejs/kit 3.0.0-next.23, fine on 2.64.0
svelte 5.56.9 and 5.56.7, both with compilerOptions.experimental.async and experimental.remoteFunctions
layerchart 2.0.2
vite 8.2.1, Node 22.22, Chromium
Both experimental flags are required to reproduce. See vite.config.ts in the reproduction.
Severity
Blocking an app from using await on any page that charts query data.
Describe the bug
A component with a deep reactive graph (here
layerchart'sBarChart) whose data comes from a remote query that the page alsoawaits locks the browser's main thread. There is no error, nofailedboundary and no recovery short of a reload. The update never lands and the tab never converges. A press that does not wedge leaves the chart on its old data, and the next one wedges. Sometimes the first press already does.This is a regression in SvelteKit 3. The same page, same
layerchart, samesvelte, samevite, is fine on@sveltejs/kit2.64.0.insideis the mode that freezes,plainis a control that redraws the same chart from a plain awaited promise:svelte5.56.7svelte5.56.9@sveltejs/kit2.64.0insidefine, ~30ms/pressinsidefine, ~30ms/press@sveltejs/kit3.0.0-next.23insideFROZENinsideFROZENIt tracks the Kit version, not the Svelte version. Raw output is
logs/version-matrix.txtin the reproduction, produced bylogs/version-matrix.sh. One caveat on that table: under Kit 2 thelatchedmode renders no chart at all (its$effectlatch never populates), soinsideis the only like-for-like cell across the rows. It is also the shape the real application used.It also needs three ingredients together, which is what took a while to pin down. Two neighbouring shapes are fine and look like counter-examples:
Promise, awaited in the boundary$state$effectlatchSo it is not "an
awaiton the page". The second row awaits a remote query that re-resolves on every interaction, right beside a chart that redraws on every interaction, and stays responsive. And it is not "an awaited promise feeding a chart". The first row does exactly that, redraws every time, and is fine. It is specifically the chart's data being downstream of a remote query the page awaits.Reproduction
https://github.com/half2me/svelte-layerchart-bug-repro
pnpm install pnpm dev # http://localhost:5199Pick a mode with the radio buttons, then press bump a few times.
node verify.mjsdrives all four:The bar count is part of the evidence. The data is
10 + clicksrows, so11bars,12barsand so on is how you know the chart really redrew rather than the mode quietly doing nothing. In the failing modes a press that does not wedge leaves the chart at10bars, meaning the update never landed at all, and the next press wedges.Logs
node instrument.mjscounts recomputations inside Svelte's client runtime.instrument/patch-svelte.mjsadds counters toupdate_derivedandis_dirty(Vite serves Svelte from source in dev, and the probe keeps streaming after the tab wedges). Counters are reset before each press, so each line is the cost of that one press. Full output inlogs/instrument.txt:About 1,100 derived recomputations for a press that works, against 4.5 million and climbing for one that does not, with no sign of converging.
Which deriveds, at the 1,000,000 mark:
and a stack sample from inside the loop:
Everything hot is
layerchart's own chart-context deriveds re-entering themselves, with theis_dirtywalk on top. Nothing in application code is looping. The app's own deriveds each recompute a handful of times while the tab is stuck.Unwrapping the query into a plain promise first,
getRows(args).then((r) => $state.snapshot(r)), does not help either, which suggests the cause is the query's own reactive machinery rather than the value it hands on.Things that do not help
From the original application, all still freeze:
$statelatched in an$effect, so its update lands after the batch{#key}instead of updatingrenderContext="canvas"(one canvas draw instead of a component per bar)layerchart@2.2.0instead of2.0.2(better, still freezes)The only thing that resolves it is not
awaiting that query on that page: reading it through.current, latching it, and rendering from the latch, which costs the page its server-rendered content.What I could not reduce
I could not get below
layerchart. A synthetic component with a comparably deep derived DAG (depth 12, fan-in 3, every node returning a fresh object, 90 leaf consumers), with and without an$effectmeasurement cycle, does not reproduce it in any mode. So I cannot rule out thatlayerchartis doing something unusual. But the version matrix isolates the SvelteKit half: same chart, same data, same presses, and only the Kit version differs.Related
techniq/layerchart#895: same symptom, reported against layerchartsveltejs/svelte#18624: layerchart's maintainer escalating that one to Sveltesveltejs/svelte#18503:$derivedlosing memoization when invalidated while another batch is pending, which looks like the same shape as the profile abovesveltejs/svelte#18558: "entangle batches", open, lists #18624 among what it fixesFiled here rather than added to those because the bisect points at Kit: the same Svelte builds are fine under Kit 2.64.0. Happy to move it if that reading is wrong.
System Info
@sveltejs/kit3.0.0-next.23, fine on 2.64.0svelte5.56.9 and 5.56.7, both withcompilerOptions.experimental.asyncandexperimental.remoteFunctionslayerchart2.0.2vite8.2.1, Node 22.22, ChromiumBoth experimental flags are required to reproduce. See
vite.config.tsin the reproduction.Severity
Blocking an app from using
awaiton any page that charts query data.