The cost of parsing every token
Re-parsing markdown and re-highlighting a code block on every chunk turns an O(n) stream into O(n²) work, and the fix is not a faster parser — it is not re-parsing content that has already finished rendering.
You have windowed the transcript and the node count is down. The presentation delay is better and still not good, and the flame chart keeps naming the same frame inside the long task: your markdown parser.
This one is not a memory problem or a DOM problem. It is an arithmetic problem, and it is the kind that gets worse in a way nobody notices during development, because the answers you test with are short.
The arithmetic
Take the naive streaming render. A chunk arrives, you append it to the accumulated answer, and you hand the whole accumulated string to the markdown parser to turn into elements. Now count the work.
On chunk one the parser sees roughly one chunk of text. On chunk two it sees two. On chunk n it sees n. Total parser input across the stream is 1 + 2 + 3 + … + n, which is n(n+1)/2 — work that grows with the square of the answer length, to deliver an answer that grew linearly.
That is not a claim about any particular parser. It is what the loop does. A parser twice as fast halves the constant and leaves the shape exactly where it was, which is why the instinct to go shopping for a faster parser is the wrong instinct. Double the answer length and you have roughly quadrupled the parsing.
The same mistake, in a shipped product
This is not hypothetical, and it does not only happen on the client. open-webui issue #23733, opened 14 April 2026, reports exactly this loop on the server side of a widely deployed chat product. In the reporter’s words, the backend “re-serializes the entire accumulated output (all prior text, reasoning blocks, tool calls, images, sources) into one HTML string and emits it via Socket.IO on every SSE event.”
Same shape, different layer. A linearly growing accumulator, fully reprocessed per event. The issue was closed as not planned, so read it as a documented failure rather than as an accepted diagnosis with a fix attached.
A number this lesson deliberately does not quote
That issue carries a headline amplification figure, and you will find it repeated. This course does not use it. It is the reporter’s own arithmetic projected over an assumed cluster topology — a worked scenario, not a measurement with a disclosed method, and it was never independently checked. The mechanism in that issue is solid and citable. The multiplier is not, and quoting it would be exactly the move this course exists not to make.
The same rule kills every “N times faster” claim you will find for streaming-markdown libraries. Those figures are published by the authors of the libraries they measure, against corpora they chose and did not disclose. A speedup multiplier without a corpus is not a fact about your surface.
The fix is not a faster parser
The fix is to stop re-parsing content that has already finished rendering. Everything below the surface of that sentence is an implementation choice, and this course is not going to hand you one as though a source had endorsed it.
Here is what can be said honestly. The completed part of a streamed answer does not change. Whatever you produced from it last frame is still correct this frame. Only the tail is in motion, and the tail is small. So the work per chunk should be proportional to the tail, not to the whole. How you get there — caching parsed blocks by index, splitting at the last stable boundary, rendering the tail as plain text until it settles — is a design decision with real trade-offs in correctness, and you should pick one and measure it rather than adopt one because a blog post named it.
That such libraries exist at all is the useful signal. Streamdown is Vercel’s drop-in replacement for react-markdown built for exactly this situation, and its docs describe the hard part as incomplete syntax rather than raw speed: it “automatically detects and completes unterminated Markdown syntax” and “applies styling to partial content as it streams in,” where an ordinary renderer “will either render these incomplete elements incorrectly or not at all.” Vercel publishes that page and sells the platform the library leads to. What it is good evidence for is that the problem is real and current enough for a large vendor to ship a component around. It is not evidence about how much faster your surface would get.
Highlighting is the same loop, more expensive
Everything above applies twice over to syntax highlighting, because a code block inside a streaming answer gets re-tokenized on the same cadence as the markdown around it, and highlighting a block is typically more work per character than parsing the prose. A code block is also the single most likely thing to be in flight when a user reaches for stop, since it is where a long answer spends its longest stretch.
The cheap move here is not an algorithm at all: do not highlight an unterminated code block. Render it as preformatted text until the closing fence arrives, then highlight it once. That trades a moment of unstyled code for removing the hottest repeated work in the stream, and it is worth measuring before anything more sophisticated.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Why can this cost stay invisible even in a profile you already recorded — and what changes it?
Check your answer
Because you almost certainly recorded a short stream. The per-chunk parse is a modest task early in the answer and only becomes a long task as the accumulator grows, so a trace of a twenty-chunk reply shows a row of small, unremarkable blocks. Nothing in it is red.
What changes it is the second reading the profiling lesson already asked you to take: click stop late in a long stream, not early. If the late click is materially worse than the early one, the cost grows with transcript size, and this lesson is the one that explains why. That field exists in MEASUREMENTS.md for exactly this discrimination — a fixed per-chunk cost and a cost that scales with length are different problems with different fixes, and the two readings are what tell them apart.
Hands on
Make the quadratic visible, then remove it
Done when: MEASUREMENTS.md records total parse time across one long stream, before and after, at the same CPU throttling preset with the same prompt — plus the parse count and the largest single parse in each run, so the change in shape is visible and not just the change in total.
- Wrap the parse call in
performance.mark()andperformance.measure()so each parse becomes an entry in the Timings track. You added marks around the chunk-to-commit boundary in the profiling lesson; this is the same technique one level in. - Send a prompt that produces a genuinely long answer — long enough to be annoying, not a test sentence. Record a trace at your calibrated throttling preset.
- From the performance entries, write down three numbers: how many times the parser ran, the total time across all runs, and the duration of the single longest run. The third is the one that predicts whether a click lands in a long task.
- Plot the per-parse durations in order, even roughly. A flat line means you are not re-parsing the accumulator and this lesson is not your problem. A line that climbs is the quadratic, drawn.
- Change one thing: stop re-processing content that has already finished. Cache by block, split at the last stable boundary, or render the tail plainly — pick one, and write down which you picked and why, because the trade-off you accepted is part of the result.
- Deal with code blocks explicitly: hold highlighting until the closing fence, then highlight once. Record whether this alone moved the longest-single-parse number.
- Re-record with the same prompt at the same preset and write the three numbers again next to the first three. Then click stop late in the stream and re-read the phase split.
- Bring both sets of three numbers into the chat, with the implementation choice you made. I will push back if the two runs used different prompts or different presets, or if a speedup is being claimed from the total alone while the longest single parse is unchanged.
What this does not cover
This is the last lesson in the course, and what it hands you is not a finished surface. It is a filled-in MEASUREMENTS.md and a method: name the phase, name the function, change one thing, measure at the same throttling preset, and write down what you traded. Everything you do after this should leave that file bigger.
The place that method lives from here is this course’s budget reference, which is where you go next. It is the page to open at the start of an integration rather than the end of one, and the row that matters most is the one with no published standard behind it — the marginal payload you and the host team have to agree in writing.
Two boundaries stay where they were. Render frequency, and how often React is asked to do anything at all, is the React module: the keeping-the-stream-out-of-state lesson attacks input delay, and this module attacked presentation delay. And the stream itself — how chunks arrive, how a message is framed on the wire, how an answer is aborted or resumed — is the Streaming interfaces course throughout. This one only ever asked what the browser pays once the bytes are already here.
Read this next — primary source
perf: Exponential growth of backend, frontend and network bandwith usage with growing chat lengthopen-webui issue #23733, GitHub — free; opened 14 April 2026, closed as not planned. A user’s production incident report, not a benchmark with disclosed methodology.
This lesson takes the mechanism from it and nothing else: an accumulator that is re-serialised in full on every event, in a real shipped product. The full thread is worth reading for two reasons a summary cannot give you. The reporter walks through how they found it, which is a model for the investigation. And the maintainers closed it as not planned, which tells you something about how this class of bug gets triaged when the failure is distributed across a fleet rather than visible on one screen.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.