I’ve been debugging some pretty bad frontend performance in an app using useThreadMessages with streaming enabled. In some cases the UI was dropping to 2 FPS 😭
I know we have a fair amount of re‑rendering going on, but even accounting for that, things felt way slower than expected. So I recorded a performance trace in Chrome and found this:
updateFromUIMessageChunks in deltas.ts is taking a big chunk of the main thread time
- Inside that function, this
assert line seems to be the hotspot :
|
assert( |
|
messagePart.id === message.id, |
|
`Expecting to only make one UIMessage in a stream, but have ${JSON.stringify(message)} and created ${JSON.stringify(messagePart)}`, |
|
); |
it is surprising, because this is just an equality check. But my theory is that, regardless of whether the equality check passes, the JSON.stringify calls in the template string are always evaluated eagerly.
Since updateFromUIMessageChunks is called for every delta while streaming, this means we’re doing a full JSON.stringify of the whole message on every new delta.
useThreadMessages({ stream: true })
↓
useStreamingThreadMessages
↓
useStreamingUIMessages
↓
useDeltaStreams (fetches stream deltas)
↓
useEffect in useStreamingUIMessages (on new deltas)
↓
updateFromUIMessageChunks
this happens with both useThreadMessages and useUIMessages
Possible fix
If my understanding is correct, this should be a relatively easy performance win by making the error message lazy, so JSON.stringify only runs on failure.
Happy to provide more details if helpful.
I’ve been debugging some pretty bad frontend performance in an app using
useThreadMessageswith streaming enabled. In some cases the UI was dropping to 2 FPS 😭I know we have a fair amount of re‑rendering going on, but even accounting for that, things felt way slower than expected. So I recorded a performance trace in Chrome and found this:
updateFromUIMessageChunksindeltas.tsis taking a big chunk of the main thread timeassertline seems to be the hotspot :agent/src/deltas.ts
Lines 80 to 83 in b48f51c
it is surprising, because this is just an equality check. But my theory is that, regardless of whether the equality check passes, the
JSON.stringifycalls in the template string are always evaluated eagerly.Since
updateFromUIMessageChunksis called for every delta while streaming, this means we’re doing a fullJSON.stringifyof the whole message on every new delta.useThreadMessages({ stream: true }) ↓ useStreamingThreadMessages ↓ useStreamingUIMessages ↓ useDeltaStreams (fetches stream deltas) ↓ useEffect in useStreamingUIMessages (on new deltas) ↓ updateFromUIMessageChunksthis happens with both useThreadMessages and useUIMessages
Possible fix
If my understanding is correct, this should be a relatively easy performance win by making the error message lazy, so
JSON.stringifyonly runs on failure.Happy to provide more details if helpful.