TanStack AI version
0.47.3
Framework/Library version
@tanstack/ai-client: 0.25.2 | @tanstack/ai-svelte: 0.18.1
Describe the bug and the steps to reproduce it
The live subscription loop awaits processIncomingChunk() for every stream chunk.
After processing a chunk, processIncomingChunk() awaits setTimeout(resolve, 0) whenever deferred processing is enabled and the document is visible. This gives the UI an opportunity to paint, but it also schedules one macrotask for every reasoning, text, tool-input and lifecycle chunk.
The timer was introduced in commit 4ab149fd to let the UI paint between live chunks. PR #1012 later skipped it for hidden documents after background timer clamping reduced draining to approximately one chunk per second. Visible documents still pay the timer cost for every chunk, independently of whether that chunk publishes a UI update.
On streams with thousands of small chunks, timer scheduling becomes the dominant processing cost and the subscription consumer falls seconds behind an already-completed transport.
In one observed run, five streams contained 1,368–2,595 chunks each. Their transports completed in 2.98–4.12 seconds, while append() completed another 0.18–4.09 seconds later; the processing tails totaled 13.15 seconds even though chunk receipt lag remained between 2 and 64 milliseconds.
This behavior is independent of persistence. Live streams use the deferred path whether or not a persistor is configured. Persisted replay explicitly passes defer: false, so replay does not incur the per-chunk timer cost.
Expected behavior
Live chunks should be processed immediately and in order. UI-facing text publication should continue to use the existing configurable ChunkStrategy, while terminal, interrupt and error state remains immediate.
The subscription should not schedule one timer per chunk. If draining a buffered backlog would otherwise create a long task, it can process a bounded slice and use the existing setTimeout(resolve, 0) once between slices. This does not require a new scheduling primitive or public scheduler option.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://stackblitz.com/edit/vitejs-vite-drnv6eeo?file=package.json,index.html,src%2Fmain.ts
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
Yes, I am also opening a PR that solves the problem along side this issue
Terms & Code of Conduct
TanStack AI version
0.47.3
Framework/Library version
@tanstack/ai-client: 0.25.2 | @tanstack/ai-svelte: 0.18.1
Describe the bug and the steps to reproduce it
The live subscription loop awaits
processIncomingChunk()for every stream chunk.After processing a chunk,
processIncomingChunk()awaitssetTimeout(resolve, 0)whenever deferred processing is enabled and the document is visible. This gives the UI an opportunity to paint, but it also schedules one macrotask for every reasoning, text, tool-input and lifecycle chunk.The timer was introduced in commit
4ab149fdto let the UI paint between live chunks. PR #1012 later skipped it for hidden documents after background timer clamping reduced draining to approximately one chunk per second. Visible documents still pay the timer cost for every chunk, independently of whether that chunk publishes a UI update.On streams with thousands of small chunks, timer scheduling becomes the dominant processing cost and the subscription consumer falls seconds behind an already-completed transport.
In one observed run, five streams contained 1,368–2,595 chunks each. Their transports completed in 2.98–4.12 seconds, while
append()completed another 0.18–4.09 seconds later; the processing tails totaled 13.15 seconds even though chunk receipt lag remained between 2 and 64 milliseconds.This behavior is independent of persistence. Live streams use the deferred path whether or not a persistor is configured. Persisted replay explicitly passes
defer: false, so replay does not incur the per-chunk timer cost.Expected behavior
Live chunks should be processed immediately and in order. UI-facing text publication should continue to use the existing configurable
ChunkStrategy, while terminal, interrupt and error state remains immediate.The subscription should not schedule one timer per chunk. If draining a buffered backlog would otherwise create a long task, it can process a bounded slice and use the existing
setTimeout(resolve, 0)once between slices. This does not require a new scheduling primitive or public scheduler option.Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://stackblitz.com/edit/vitejs-vite-drnv6eeo?file=package.json,index.html,src%2Fmain.ts
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
Yes, I am also opening a PR that solves the problem along side this issue
Terms & Code of Conduct