Lesson 20 of 30
await response.json() blocks on the entire response finishing before showing anything — reading that same response as a stream instead lets the UI render pieces of it as they actually arrive.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Why does await response.json() block progressive rendering of a large response?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
This is a different shape of problem than an earlier lesson's one-directional, ongoing connection: that pattern is about a persistent stream of discrete events arriving over time. This lesson is about one single response that itself arrives incrementally — a large JSON array being parsed and rendered item by item as chunks land, or an AI-generated response's words appearing progressively as they're produced, which is a genuinely common, current example of exactly this same pattern.
The default way of handling a response, await response.json(), necessarily blocks on the entire body finishing before any of it can be touched — parsing can't start until every byte has arrived, so nothing can render until the whole thing is done, no matter how large the response is.
The alternative is reading the response body as a stream instead of waiting for it to complete: process each chunk as it arrives, and update UI state incrementally with whatever's been parsed so far, rather than doing one big replace once everything is finally in.
The exact streaming API surface varies by runtime and can shift over time, so treat this as the conceptual shape rather than an API to copy verbatim — the idea that matters is reading chunks as they arrive instead of awaiting the whole body:
async function streamItems(url: string, onItem: (item: Item) => void) {
const response = await fetch(url);
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (reader) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Extract whatever complete items exist in the buffer so far (the exact
// parsing depends on the wire format), emit each one immediately, and
// keep only the leftover partial chunk for next time.
const { items, rest } = extractCompleteItems(buffer);
items.forEach(onItem);
buffer = rest;
}
}
Wiring that into a component means updating state as each item is parsed out, rather than waiting for the whole loop to finish:
function useStreamedItems(url: string) {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
let cancelled = false;
streamItems(url, (item) => {
if (!cancelled) setItems((prev) => [...prev, item]);
});
return () => {
cancelled = true;
};
}, [url]);
return items;
}
A chat UI that shows an AI-generated response's words appearing one after another is doing the exact same thing at a smaller grain: reading the response as it streams in, and appending each piece to the visible text as it lands, instead of waiting for generation to finish and showing the whole message at once.
response.json() isn't slow — it's blocking by design. It has to wait for the complete body because it needs the whole thing to produce a valid parsed value; that's correct behavior for that method, not a bug, which is exactly why a different approach is needed when progressive rendering is the goal."How would you show a large AI-generated response as it's being produced, instead of waiting for the whole thing?" is an increasingly common practical question, and correctly framing the fix as reading the response as a stream and rendering incrementally, rather than reaching for polling or a persistent connection, shows real familiarity with how this actually gets implemented.