Skip to main content

Streaming responses render incrementally in the UI

ab-000143 · ai-chat-visibility.loading-and-streaming.streaming-incremental
Severity: highactive

Why it matters

Configuring streaming at the API level but awaiting the full response on the frontend is a silent no-op: the server dutifully streams tokens into a reader that buffers them all before a single setState call, producing the exact frozen-UI experience that streaming was supposed to eliminate. This bug pattern is extremely common when developers half-migrate from non-streaming to streaming and forget to update the consuming hook or component state logic.

Severity rationale

High because the server cost of streaming is paid while delivering zero perceived-performance benefit to users.

Remediation

In your chat hook at src/hooks/useChat.ts, read the response body with getReader() and update message state on every decoded chunk inside the loop rather than after it completes. Strongly prefer the Vercel AI SDK's useChat, which handles incremental state correctly by default.

const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  setMessage(prev => prev + decoder.decode(value));
}

Detection

  • ID: ai-chat-visibility.loading-and-streaming.streaming-incremental

  • Severity: high

  • What to look for: Even if streaming is configured at the API level, check that the frontend actually renders partial responses as they arrive. List all state update calls inside the stream reading loop and classify whether they append partial content or replace the full message. In the Vercel AI SDK, useChat handles this automatically. For custom implementations, check for state setter calls inside a while-loop reading from a ReadableStream.

  • Pass criteria: The message state is updated during streaming — not just when the stream is complete. The partial message appears in the DOM and grows as tokens arrive with fewer than 500ms between visible updates. Report even on pass: "Found incremental state updates in [component/hook name] using [mechanism]."

  • Fail criteria: The stream is consumed fully before the message state is updated, resulting in the full message appearing at once even though streaming is configured at the API level.

  • Skip (N/A) when: Same condition as response-streaming-progressive — no streaming implementation detected.

  • Detail on fail: "API route streams correctly but frontend awaits full response — partial tokens are never displayed to the user"

  • Remediation: In your chat hook at src/hooks/useChat.ts or equivalent, read the stream incrementally:

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      setMessage(prev => prev + decoder.decode(value));
    }
    

    Using the Vercel AI SDK's useChat handles all of this automatically and is strongly recommended over manual stream reading.

Taxons

History