Benchmarks
What pacing costs and what it changes, measured on the same streams for every approach. The code, the streams and the raw results are in benchmarks/ in the repository; the tables below are generated from those results.
Method
Streams. Two answers were recorded from a real model (Claude, through Claude Code's streaming output) with the time each chunk reached the client. Nine synthetic scenarios reuse the recorded text with arrival patterns chosen to stress one thing each: steady, bursty, a long stall, a fast model, a final burst, a long answer, chunks cut inside emoji and surrogate pairs, chunks cut inside Markdown constructs, and a hidden tab. They are generated from fixed seeds, so every run sees the same streams.
Approaches.
- as received: every chunk is shown the moment it arrives.
- fixed 60 cps and fixed 240 cps: a hand-written typing effect, a timer that shows one more character of
text.slice(0, count)at a constant rate. - AI SDK smoothStream(): the real transform from
ai7.0, with its defaults (word chunks, 10 ms delay), and once withIntl.Segmenterword chunks as its docs suggest for languages without spaces. It is run on the stream before it reaches the client. - smooth-stream-text: the core with default settings, and with
boundary: 'char'andunit: 'word'to show what those settings change. - smoothStream() then smooth-stream-text: both layers stacked.
Pacing metrics are measured in virtual time on a 60 Hz frame clock, so they are exact and repeatable: when each character becomes visible, how evenly the text grows, how long the end takes, and whether any frame shows text a renderer would draw wrongly. Lag and the size of a character are counted in UTF-16 code units, like JavaScript strings. Rendering cost is measured separately, in headless Chromium, replaying streams in real time and rendering Markdown with marked. Bundle size is measured with esbuild, per import.
What the numbers say
The cost is a steady delay. With the defaults, the first text appeared 0.08 to 0.15 s after the first chunk of the recorded answers, and half the characters were shown within about 0.2 s of arriving. That is the targetLatencyMs of 180 ms at work. After the stream closed, the rest drained within a second in every scenario.
On the two recorded answers, text grows more evenly than as received: the evenness figure (lower is better) falls by 40 to 55 percent, and the largest amount of text appearing in a single frame drops, from 186 to 105 characters on one answer and from 225 to 16 on the other. The largest remaining step on the English answer is a table row, which the Markdown rules hold until its line is complete.
No frame across all eleven streams ended inside a grapheme cluster. One frame ended inside an open Markdown construct: after the three-second stall, the holdMs safety valve released a held construct, as it is designed to. Rendering as received produced hundreds of such frames, and so did fixed-rate typing.
Pacing does not hide long network pauses. In the bursty scenario, where nothing arrives for up to 0.9 s, the reader still sees most of those pauses: once the backlog has drained, there is nothing left to show. A fixed slow rate hides them better, at the price of falling far behind.
Above 900 characters per second, the default ceiling, the display falls behind and then jumps (the fast scenario). Raise maxCharsPerSecond if your model is faster.
The Markdown rules cost some evenness. With boundary: 'char' the text grows more evenly, but frames with broken syntax appear. The default favours intact syntax.
The AI SDK's smoothStream() evens out the recorded English answer about as well, with less lag. With its default word chunks, text in Chinese or Japanese waits for the next space, so the multilingual answer showed nothing for 0.8 s. With Intl.Segmenter chunks, one segment per 10 ms fell behind on every stream, by up to several seconds. It also produced frames with broken Markdown. Its rows here assume its output reaches the browser exactly as it leaves the transform; in practice it crosses the network again, which can bunch chunks back together.
Fixed-rate typing falls behind real model output by seconds at 240 characters per second and by tens of seconds at 60.
Rendering is the main runtime cost. A paced answer changes on every frame while it appears, so a Markdown renderer runs up to 60 times a second: about twice as often as rendering once per chunk on the recorded answer, and more total rendering time. unit: 'word' changes the text less often. With the reveal, the spans still animating peak at a few hundred on a fast stream and are gone once the answer settles.
These benchmarks found a bug: after a pause inside a held construct, the next chunk appeared all at once. It is fixed in 0.2.1 (see the changelog), and the results below include the fix. They also showed that importing only the React or Vue hooks bundled the reveal as well, about twice the size of the core; that is fixed in 0.2.2.
Results
Streams
smooth-stream-text 0.2.2 built from v0.2.2, AI SDK ai 7.0.142, 2026-10-16.
| Stream | Origin | Characters | Chunks | Duration | Pattern |
|---|---|---|---|---|---|
| claude-markdown | recorded | 2625 | 146 | 7.2 s | recorded from Claude Code (claude-opus-5-5) stream-json partial messages on 2026-10-16 |
| claude-multilingual | recorded | 1120 | 105 | 6.8 s | recorded from Claude Code (claude-opus-5-5) stream-json partial messages on 2026-10-16 |
| bursty | synthetic | 2625 | 172 | 19.0 s | Groups of 3-8 chunks arrive together, then nothing for 300-900 ms. |
| fast | synthetic | 2625 | 44 | 1.5 s | A fast model: 60-character chunks every 33 ms, about 1,800 characters per second. |
| hidden-tab | synthetic | 2625 | 325 | 9.8 s | The steady stream, with the tab hidden from 1.5 s to 6 s. Animation frames stop while hidden and timers are throttled to once a second. |
| long | synthetic | 13133 | 946 | 33.1 s | A long answer: the Markdown text five times over (about 13,000 characters) at about 400 characters per second. |
| long-stall | synthetic | 2625 | 326 | 12.8 s | Steady arrival with a 3-second stall halfway through. |
| markdown-split | synthetic | 2625 | 330 | 22.5 s | Markdown with links, bold, inline code and a table, cut at random offsets, with a 400 ms pause after one chunk in eight. |
| steady | synthetic | 2625 | 325 | 9.8 s | Even arrival: 4-12 characters every 30 ms, about 270 characters per second. |
| tail-burst | synthetic | 2625 | 208 | 7.4 s | Steady for 60% of the answer, a 1.2-second pause, then the remaining 40% in one chunk as the stream closes. |
| unicode-split | synthetic | 1120 | 228 | 8.0 s | Multilingual text with emoji, cut at random UTF-16 offsets, so chunks often end inside a surrogate pair or an emoji sequence. |
Recorded streams
Real model output as it reached the client. Lag is the time from a character arriving to it being visible. Evenness is the coefficient of variation of characters added per 100 ms while the answer appears: lower is more even. Largest step is the most characters that appeared in one frame. Pauses are gaps of more than 250 ms without new text before the answer is complete. Broken frames end with an unclosed Markdown construct on the last line; split frames end inside a grapheme cluster.
claude-markdown: 2625 characters in 146 chunks over 7.2 s.
| Approach | First text (ms) | Lag p50 (ms) | Lag p95 (ms) | Tail (ms) | Evenness | Largest step | Pauses | Broken frames | Split frames |
|---|---|---|---|---|---|---|---|---|---|
| as received | 0 | 10 | 16 | 13 | 1.1 | 186 | 3 | 31 | 0 |
| fixed 60 cps | 17 | 18315 | 34747 | 36529 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 17 | 1980 | 3597 | 3779 | 0.06 | 5 | 0 | 176 | 0 |
| AI SDK smoothStream() | 0 | 57 | 261 | 13 | 0.67 | 84 | 3 | 17 | 0 |
| smoothStream() + Intl.Segmenter | 0 | 2365 | 4610 | 4696 | 0.27 | 21 | 0 | 164 | 0 |
| smooth-stream-text | 150 | 192 | 301 | 79 | 0.66 | 105 | 3 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 67 | 183 | 286 | 96 | 0.48 | 14 | 2 | 101 | 0 |
| smooth-stream-text, unit 'word' | 183 | 194 | 314 | 96 | 0.67 | 105 | 3 | 0 | 0 |
| smoothStream() then smooth-stream-text | 167 | 246 | 471 | 263 | 0.63 | 105 | 1 | 0 | 0 |
claude-multilingual: 1120 characters in 105 chunks over 6.8 s.
| Approach | First text (ms) | Lag p50 (ms) | Lag p95 (ms) | Tail (ms) | Evenness | Largest step | Pauses | Broken frames | Split frames |
|---|---|---|---|---|---|---|---|---|---|
| as received | 0 | 6 | 16 | 5 | 2.91 | 225 | 8 | 0 | 0 |
| fixed 60 cps | 17 | 6488 | 11705 | 12622 | 0.2 | 2 | 1 | 0 | 154 |
| fixed 240 cps | 17 | 316 | 788 | 872 | 0.75 | 5 | 6 | 0 | 36 |
| AI SDK smoothStream() | 800 | 114 | 422 | 422 | 1.59 | 81 | 7 | 0 | 0 |
| smoothStream() + Intl.Segmenter | 0 | 375 | 822 | 1055 | 0.65 | 10 | 2 | 0 | 10 |
| smooth-stream-text | 83 | 181 | 356 | 305 | 1.33 | 16 | 8 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 83 | 181 | 356 | 305 | 1.33 | 16 | 8 | 0 | 0 |
| smooth-stream-text, unit 'word' | 800 | 83 | 235 | 238 | 1.7 | 20 | 7 | 0 | 0 |
| smoothStream() then smooth-stream-text | 850 | 316 | 655 | 655 | 0.88 | 10 | 6 | 0 | 0 |
Text integrity across all streams
Totals over all eleven streams. Every approach delivered the complete text, and every frame was a prefix of it.
| Approach | Frames with broken Markdown | Frames splitting a grapheme cluster | Complete text |
|---|---|---|---|
| as received | 665 | 35 | all streams |
| fixed 60 cps | 9183 | 309 | all streams |
| fixed 240 cps | 2313 | 85 | all streams |
| AI SDK smoothStream() | 263 | 0 | all streams |
| smoothStream() + Intl.Segmenter | 2291 | 43 | all streams |
| smooth-stream-text | 1 | 0 | all streams |
| smooth-stream-text, boundary 'char' | 1649 | 0 | all streams |
| smooth-stream-text, unit 'word' | 1 | 0 | all streams |
| smoothStream() then smooth-stream-text | 0 | 0 | all streams |
Synthetic scenarios
Each scenario stresses one pattern. Open a row for the full table.
bursty: Groups of 3-8 chunks arrive together, then nothing for 300-900 ms.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 4 | 5 | 15 | 15 | 11 | 2.41 | 144 | 29 | 9 | 0 |
| fixed 60 cps | 4 | 12750 | 23910 | 25161 | 25161 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 4 | 205 | 460 | 606 | 311 | 0.79 | 4 | 16 | 183 | 0 |
| AI SDK smoothStream() | 4 | 70 | 388 | 904 | 11 | 1.68 | 84 | 26 | 13 | 0 |
| smoothStream() + Intl.Segmenter | 4 | 267 | 597 | 799 | 294 | 0.71 | 14 | 15 | 178 | 0 |
| smooth-stream-text | 70 | 131 | 404 | 948 | 94 | 1.47 | 105 | 21 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 37 | 172 | 347 | 437 | 277 | 0.96 | 7 | 19 | 156 | 0 |
| smooth-stream-text, unit 'word' | 104 | 88 | 621 | 948 | 94 | 1.66 | 105 | 24 | 0 | 0 |
| smoothStream() then smooth-stream-text | 104 | 254 | 671 | 1068 | 94 | 1.22 | 105 | 18 | 0 | 0 |
fast: A fast model: 60-character chunks every 33 ms, about 1,800 characters per second.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 0 | 7 | 14 | 15 | 15 | 0.21 | 60 | 0 | 10 | 0 |
| fixed 60 cps | 0 | 21174 | 40214 | 42331 | 42331 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 0 | 4774 | 9031 | 9515 | 9515 | 0.08 | 5 | 0 | 175 | 0 |
| AI SDK smoothStream() | 0 | 1407 | 2331 | 2381 | 2331 | 0.28 | 98 | 0 | 20 | 0 |
| smoothStream() + Intl.Segmenter | 0 | 5174 | 9997 | 10381 | 10381 | 0.28 | 21 | 0 | 165 | 0 |
| smooth-stream-text | 67 | 874 | 1178 | 1211 | 815 | 1.25 | 742 | 0 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 34 | 848 | 1178 | 1211 | 815 | 1.18 | 743 | 0 | 35 | 0 |
| smooth-stream-text, unit 'word' | 84 | 874 | 1178 | 1227 | 815 | 1.23 | 745 | 0 | 0 | 0 |
| smoothStream() then smooth-stream-text | 117 | 1574 | 2464 | 2598 | 2565 | 0.28 | 105 | 0 | 0 | 0 |
hidden-tab: The steady stream, with the tab hidden from 1.5 s to 6 s. Animation frames stop while hidden and timers are throttled to once a second.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 3 | 13 | 4020 | 4500 | 0 | 4.5 | 1207 | 1 | 36 | 0 |
| fixed 60 cps | 3 | 21383 | 36747 | 38450 | 38450 | 0.32 | 5 | 1 | 707 | 0 |
| fixed 240 cps | 3 | 5050 | 5633 | 5727 | 5700 | 0.64 | 8 | 1 | 178 | 0 |
| AI SDK smoothStream() | 3 | 70 | 4020 | 4530 | 0 | 4.48 | 1199 | 2 | 9 | 0 |
| smoothStream() + Intl.Segmenter | 3 | 2643 | 4350 | 4830 | 3617 | 3.94 | 902 | 1 | 127 | 0 |
| smooth-stream-text | 137 | 1097 | 4220 | 4680 | 83 | 1.34 | 105 | 2 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 70 | 1097 | 4220 | 4710 | 183 | 1.32 | 90 | 1 | 80 | 0 |
| smooth-stream-text, unit 'word' | 187 | 1097 | 4220 | 4680 | 67 | 1.31 | 105 | 2 | 0 | 0 |
| smoothStream() then smooth-stream-text | 153 | 1127 | 4220 | 4710 | 217 | 1.36 | 105 | 2 | 0 | 0 |
long: A long answer: the Markdown text five times over (about 13,000 characters) at about 400 characters per second.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 15 | 8 | 15 | 17 | 7 | 0.21 | 20 | 0 | 252 | 0 |
| fixed 60 cps | 32 | 92858 | 176570 | 185825 | 185823 | 0.02 | 2 | 0 | 3534 | 0 |
| fixed 240 cps | 15 | 10792 | 20620 | 21675 | 21657 | 0.04 | 5 | 0 | 877 | 0 |
| AI SDK smoothStream() | 15 | 23 | 57 | 258 | 7 | 0.34 | 89 | 1 | 110 | 0 |
| smoothStream() + Intl.Segmenter | 15 | 14643 | 28370 | 29592 | 29573 | 0.24 | 21 | 0 | 890 | 0 |
| smooth-stream-text | 115 | 170 | 250 | 420 | 123 | 0.48 | 112 | 1 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 65 | 172 | 203 | 277 | 190 | 0.12 | 9 | 0 | 543 | 0 |
| smooth-stream-text, unit 'word' | 148 | 170 | 250 | 420 | 123 | 0.5 | 107 | 1 | 0 | 0 |
| smoothStream() then smooth-stream-text | 115 | 188 | 275 | 520 | 207 | 0.52 | 114 | 4 | 0 | 0 |
long-stall: Steady arrival with a 3-second stall halfway through.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 3 | 10 | 17 | 17 | 17 | 0.61 | 12 | 1 | 87 | 0 |
| fixed 60 cps | 3 | 15537 | 29390 | 31047 | 31033 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 3 | 347 | 607 | 647 | 617 | 0.47 | 5 | 1 | 178 | 0 |
| AI SDK smoothStream() | 3 | 27 | 77 | 3030 | 17 | 0.74 | 84 | 2 | 25 | 0 |
| smoothStream() + Intl.Segmenter | 3 | 923 | 1823 | 1887 | 1850 | 0.4 | 12 | 1 | 187 | 0 |
| smooth-stream-text | 137 | 170 | 273 | 1543 | 67 | 0.85 | 105 | 4 | 1 | 0 |
| smooth-stream-text, boundary 'char' | 70 | 173 | 223 | 277 | 200 | 0.56 | 6 | 1 | 173 | 0 |
| smooth-stream-text, unit 'word' | 170 | 170 | 273 | 1543 | 67 | 0.9 | 105 | 5 | 1 | 0 |
| smoothStream() then smooth-stream-text | 137 | 193 | 317 | 3113 | 217 | 0.85 | 105 | 4 | 0 | 0 |
markdown-split: Markdown with links, bold, inline code and a table, cut at random offsets, with a 400 ms pause after one chunk in eight.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 0 | 8 | 17 | 17 | 0 | 1.26 | 14 | 37 | 89 | 0 |
| fixed 60 cps | 0 | 9692 | 20167 | 21650 | 21650 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 0 | 67 | 242 | 408 | 133 | 0.89 | 4 | 29 | 190 | 0 |
| AI SDK smoothStream() | 0 | 25 | 408 | 600 | 0 | 1.35 | 84 | 37 | 24 | 0 |
| smoothStream() + Intl.Segmenter | 0 | 108 | 600 | 800 | 133 | 0.76 | 19 | 21 | 201 | 0 |
| smooth-stream-text | 133 | 200 | 558 | 1108 | 83 | 1.27 | 105 | 9 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 67 | 200 | 283 | 392 | 250 | 0.79 | 8 | 0 | 286 | 0 |
| smooth-stream-text, unit 'word' | 183 | 208 | 558 | 1108 | 83 | 1.29 | 105 | 15 | 0 | 0 |
| smoothStream() then smooth-stream-text | 150 | 225 | 608 | 1108 | 133 | 1.29 | 105 | 11 | 0 | 0 |
steady: Even arrival: 4-12 characters every 30 ms, about 270 characters per second.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 3 | 7 | 17 | 17 | 0 | 0.23 | 12 | 0 | 86 | 0 |
| fixed 60 cps | 3 | 16950 | 32330 | 34033 | 34033 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 3 | 567 | 1150 | 1243 | 1233 | 0.07 | 5 | 0 | 176 | 0 |
| AI SDK smoothStream() | 3 | 27 | 90 | 330 | 0 | 0.43 | 84 | 1 | 25 | 0 |
| smoothStream() + Intl.Segmenter | 3 | 1657 | 3533 | 3627 | 3617 | 0.23 | 18 | 0 | 195 | 0 |
| smooth-stream-text | 137 | 170 | 273 | 473 | 83 | 0.61 | 105 | 3 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 70 | 173 | 213 | 277 | 183 | 0.17 | 6 | 0 | 159 | 0 |
| smooth-stream-text, unit 'word' | 187 | 170 | 277 | 473 | 83 | 0.62 | 105 | 3 | 0 | 0 |
| smoothStream() then smooth-stream-text | 153 | 190 | 320 | 607 | 217 | 0.62 | 105 | 3 | 0 | 0 |
tail-burst: Steady for 60% of the answer, a 1.2-second pause, then the remaining 40% in one chunk as the stream closes.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 3 | 7 | 17 | 17 | 7 | 3.36 | 1050 | 1 | 65 | 0 |
| fixed 60 cps | 3 | 16710 | 34190 | 36373 | 36373 | 0.04 | 2 | 0 | 706 | 0 |
| fixed 240 cps | 3 | 273 | 3823 | 4373 | 4373 | 0.27 | 5 | 1 | 180 | 0 |
| AI SDK smoothStream() | 3 | 40 | 1257 | 1323 | 1323 | 0.79 | 98 | 1 | 20 | 0 |
| smoothStream() + Intl.Segmenter | 3 | 1350 | 4907 | 5340 | 5340 | 0.28 | 22 | 0 | 184 | 0 |
| smooth-stream-text | 137 | 197 | 807 | 807 | 807 | 1.46 | 348 | 3 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 70 | 187 | 807 | 807 | 807 | 1.33 | 347 | 1 | 116 | 0 |
| smooth-stream-text, unit 'word' | 187 | 193 | 807 | 1237 | 807 | 1.31 | 330 | 3 | 0 | 0 |
| smoothStream() then smooth-stream-text | 137 | 233 | 1373 | 1557 | 1557 | 0.89 | 105 | 3 | 0 | 0 |
unicode-split: Multilingual text with emoji, cut at random UTF-16 offsets, so chunks often end inside a surrogate pair or an emoji sequence.
| Approach | First text | Lag p50 | Lag p95 | Lag max | Tail | Evenness | Largest step (chars) | Pauses | Broken MD | Split |
|---|---|---|---|---|---|---|---|---|---|---|
| as received | 11 | 8 | 16 | 17 | 2 | 0.45 | 17 | 0 | 0 | 35 |
| fixed 60 cps | 11 | 5085 | 10146 | 10686 | 10686 | 0.05 | 2 | 0 | 0 | 155 |
| fixed 240 cps | 11 | 16 | 57 | 95 | 2 | 0.35 | 4 | 0 | 0 | 49 |
| AI SDK smoothStream() | 61 | 23 | 254 | 715 | 19 | 0.85 | 75 | 2 | 0 | 0 |
| smoothStream() + Intl.Segmenter | 11 | 33 | 168 | 230 | 36 | 0.32 | 9 | 0 | 0 | 33 |
| smooth-stream-text | 77 | 177 | 237 | 278 | 202 | 0.29 | 6 | 0 | 0 | 0 |
| smooth-stream-text, boundary 'char' | 77 | 177 | 237 | 278 | 202 | 0.29 | 6 | 0 | 0 | 0 |
| smooth-stream-text, unit 'word' | 261 | 177 | 236 | 291 | 186 | 0.3 | 13 | 0 | 0 | 0 |
| smoothStream() then smooth-stream-text | 127 | 204 | 386 | 715 | 219 | 0.39 | 8 | 1 | 0 | 0 |
Rendering cost in a browser
Chromium 156.0.8078.4 (headless), Apple M1 Pro, 10 cores, 16 GB, darwin, 2026-10-16. Streams replayed in real time; Markdown rendered with marked into innerHTML. Renders and render time count calls to the Markdown renderer (or appends for plain text). Peak elements is the most elements inside the output at once; with the reveal these are the spans still animating.
| Stream | Approach | Renders | Render time (ms) | Long tasks | Frame p95 (ms) | Peak elements |
|---|---|---|---|---|---|---|
| claude-markdown | markdown as received | 146 | 60 | 0 | 16.8 | 90 |
| claude-markdown | markdown paced | 291 | 103 | 0 | 16.7 | 90 |
| claude-markdown | markdown paced, unit word | 239 | 77 | 0 | 16.7 | 90 |
| claude-markdown | plain text as received | 146 | 2 | 0 | 16.8 | 0 |
| claude-markdown | plain text paced + reveal | n/a | n/a | 0 | 16.7 | 298 |
| fast | markdown as received | 44 | 26 | 0 | 16.7 | 90 |
| fast | markdown paced | 111 | 38 | 0 | 16.8 | 90 |
| fast | markdown paced, unit word | 102 | 35 | 0 | 16.7 | 90 |
| fast | plain text as received | 44 | 1 | 0 | 16.7 | 0 |
| fast | plain text paced + reveal | n/a | n/a | 0 | 16.7 | 390 |
| long | markdown as received | 946 | 753 | 1 | 16.7 | 450 |
| long | markdown paced | 1516 | 1116 | 2 | 16.7 | 450 |
| long | markdown paced, unit word | 1283 | 917 | 0 | 16.8 | 450 |
| long | plain text as received | 946 | 14 | 0 | 16.7 | 0 |
| long | plain text paced + reveal | n/a | n/a | 0 | 16.8 | 209 |
Bundle size
smooth-stream-text 0.2.2, esbuild 0.28.2, minified, ESM, target es2020, react/vue/svelte external, 2026-10-16.
| Import | Minified | gzip | brotli |
|---|---|---|---|
| core: createSmoothStream | 9.6 kB | 4.2 kB | 3.9 kB |
| react: useSmoothText | 10.8 kB | 4.7 kB | 4.4 kB |
| react: SmoothText | 19.1 kB | 7.8 kB | 7.3 kB |
| react: everything | 21.5 kB | 8.7 kB | 8.0 kB |
| dom: createSmoothText | 17.1 kB | 7.0 kB | 6.5 kB |
| vue: useSmoothStream | 10.6 kB | 4.6 kB | 4.3 kB |
| svelte: smoothStream | 10.1 kB | 4.4 kB | 4.1 kB |
| web-component | 19.9 kB | 7.9 kB | 7.4 kB |
Limits of this benchmark
- Two recorded answers from one model and one network path. Other providers and networks produce other rhythms. Record your own with
node scripts/record-claude-trace.mjsor write a trace file by hand. - Synthetic scenarios are designed, not sampled from traffic. Use them to see how an approach reacts to a pattern, not as typical numbers.
- "Broken Markdown" is a heuristic on the last visible line (unclosed inline code, emphasis, strikethrough or link). It does not run a full parser.
- Browser numbers depend on the machine. Compare rows from the same run.
- Pacing runs in virtual time: it measures the controller, not timer jitter in a busy page.
Reproduce
git clone https://github.com/crmapache/smooth-stream-text
cd smooth-stream-text && npm install && npm run build
cd benchmarks && npm install
npm run traces # rebuilds the synthetic streams from fixed seeds
npm run bench # pacing, browser, size, then results/summary.md