跪拜 Guibai
← All articles
Artificial Intelligence

The Byte-by-Byte Pipeline That Powers Every AI Typing Effect

By 东风破_ ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Every AI chat UI relies on streaming, but most developers treat it as a black box. Getting the byte-boundary and event-boundary handling wrong produces garbled text, broken JSON parsing, or silent truncation — bugs that are hard to reproduce because they depend on network chunking. This walkthrough makes the pipeline explicit so teams can debug it rather than guess at it.

Summary

A non-streaming LLM call returns one JSON blob; a streaming call returns a raw byte stream that the browser must parse into incremental text. The Fetch API exposes this as a ReadableStream, whose `getReader()` yields Uint8Array chunks that arrive at unpredictable boundaries — a single `read()` might contain half an SSE event, one complete event, or fragments of several. A working frontend pipeline therefore needs a TextDecoder in streaming mode to handle multi-byte UTF-8 characters split across chunks, and a buffer that splits on SSE double-newline delimiters while preserving trailing partial events for the next read.

The article walks through a complete Vue 3 implementation: a `parseSSEEvent` function that extracts `data:` payloads, recognizes `[DONE]`, and fires a callback with each `delta.content`; a `readSSEStream` loop that feeds the decoder and buffer together; and a reactive `content.value += delta` that produces the typewriter effect without timers. It also explains why `fetch()` beats `EventSource` for chat (POST, custom headers, cancellation) and why SSE beats WebSocket for one-directional text generation.

A companion piece on the BFF server side is promised, but the client-side takeaway is already complete: the illusion of live typing is just a disciplined pipeline across byte, event, and security boundaries.

Takeaways
Streaming LLM responses arrive as raw bytes in a ReadableStream, not as a parseable JSON object.
TextDecoder must run in streaming mode (`stream: true`) or multi-byte UTF-8 characters split across network chunks will decode as garbage.
SSE events are delimited by double newlines; a single `reader.read()` can return partial events, so a buffer must accumulate bytes and split on complete event boundaries only.
The `[DONE]` sentinel in SSE signals the stream end and should halt parsing.
`fetch()` + ReadableStream is preferred over `EventSource` for chat because it supports POST, custom headers, request cancellation, and error handling.
SSE is sufficient for one-directional text generation; WebSocket adds unnecessary complexity unless bidirectional communication is required.
API keys must live on a BFF server, never in frontend environment variables, to avoid exposing them to the browser.
Using Vue's text interpolation (`{{ content }}`) rather than `v-html` prevents the model's output from being executed as HTML.
Conclusions

Many developers assume streaming is just a loop over `response.body`, but the real work is handling the mismatch between network chunk boundaries and application-level event boundaries.

The choice of `fetch()` over `EventSource` is not stylistic — it's a hard requirement driven by POST, authentication headers, and the need to cancel in-flight requests when users regenerate or switch conversations.

SSE's simplicity is a feature, not a limitation: for the dominant LLM use case of one question → one streaming answer, WebSocket's bidirectional overhead buys nothing.

The three-boundary model (byte, event, security) generalizes beyond Vue and Fetch to any frontend stack consuming streaming LLM APIs.

Concepts & terms
ReadableStream
A browser API representing a stream of data arriving over time. `response.body` from a Fetch call is a ReadableStream; calling `getReader()` on it returns chunks of raw bytes as Uint8Arrays.
Uint8Array
A typed array holding 8-bit unsigned integers (0–255). Network data arrives as raw bytes in this format and must be decoded into text via TextDecoder before use.
TextDecoder (streaming mode)
A browser API that converts byte sequences into strings. When `stream: true` is passed, it retains incomplete multi-byte characters across calls, preventing corruption when a UTF-8 character is split between two network chunks.
SSE (Server-Sent Events)
A protocol where the server sends a stream of text events over HTTP, delimited by double newlines. Each event can carry a `data:` field; LLM APIs typically embed JSON chunks there and signal completion with `[DONE]`.
BFF (Backend For Frontend)
An intermediary server that sits between the browser and third-party APIs. It holds secrets like API keys and forwards requests, so the browser never directly accesses the upstream service.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗