From Overreacted · · 4m
Progressive JSON: stop waiting for the last byte
Why JSON's all-or-nothing wire format wastes time, and how sending the tree breadth-first with placeholders unlocks progressive loading.
In brief
Why JSON's all-or-nothing wire format wastes time, and how sending the tree breadth-first with placeholders unlocks progressive loading. Send your data tree outside-in with placeholders for the slow parts, so one slow query stops holding up everything behind it. Originally reported by Overreacted.
Borrow the progressive JPEG trick
A progressive JPEG shows a fuzzy version of the whole image first, then sharpens it. Dan Abramov asks what happens if you apply that same idea to sending JSON over the wire.
A simple blog payload
Picture a tree with a header, a post containing comments, and a footer. It looks like nothing to transfer, and that is exactly where the problem hides.
JSON is all or nothing
You do not have a valid object tree until the final byte lands. Wait for the whole response, call JSON.parse, then finally start doing work.
“The client can't do anything with JSON until the server sends the last byte.”
One slow query blocks everything
If comments need a slow database trip, the ready header and footer sit in a buffer doing nothing. The client stays idle until the server finishes every last piece.
“Would you call that good engineering? And yet it's the status quo”
Try a streaming parser
A streaming parser can build an object tree from incomplete input. Ask it mid-transfer and you get the header, the post content, and whichever comments have arrived so far.
The objects come out malformed
The top-level object was supposed to have three properties, but footer is simply absent. Your types no longer describe what is actually sitting in memory.
You cannot tell what finished
Two comments showed up. Is that the whole list, or the first two of fifty? Nothing in the stream separates pending from done.
Why nobody streams JSON
Application code assumes types hold and that ready means complete. Naive streaming breaks both, so it survives only in niche corners.
“It's just too hard to actually take advantage of it in the application logic”
Crisp but only the top
This matches top-down image loading. Every pixel you have is perfect, you just have the top ten percent, so you still cannot see what the picture is.
HTML streams the same way
On a slow connection the browser receives the document in source order. The cutoff point is arbitrary, layout can lurch, and the footer waits even though the server had it ready early.
In order means in line
Order-preserving streaming turns your response into a queue. The slowest item decides when everything behind it gets to move.
“when we stream things in order they appear, a single slow part delays everything that comes after it”
Send breadth-first instead
Depth-first walks into post, then into comments, and stalls there. Breadth-first ships the outer shell immediately and fills the interior in later.
Placeholders hold the shape
The shell goes out as header: "$1", post: "$2", footer: "$3". Each marker is a labeled slot that a later row in the stream resolves.
Rows can arrive out of order
The server can emit $1 and $3 while $2 is still waiting on a database call. The client reassembles by id, not by arrival position.
Pending becomes a promise
Unresolved slots show up on the client as Promises. Header and footer are real strings right away, and post resolves the moment its row lands.
Do not over-stream
A separate row only pays off where the work is genuinely slow. With one slow post load and one slow comments load, three chunks is the right split.
Outline repeats to dedupe
A user object shared by header, sidebar, and footer can be emitted once as $1 and referenced three times. The same trick makes cyclic objects serializable, which plain JSON cannot do.
This is how RSC works
React Server Components serialize your component tree into exactly this kind of stream, sending props outside-in as each async component resolves on the server.
Suspense decides the reveal
React refuses to render a hole for a pending Promise. It shows the nearest Suspense fallback instead, so the loading sequence is designed rather than dictated by network timing.
“The data arrives as fast as it can in whatever order the server is ready to send it”
Streaming needs a programming model
Streaming alone does not help if your code cannot handle missing pieces. Abramov's challenge to other tools: if the client waits on the server finishing, or one slow step stalls the rest, you have found the bug.
The headline
Send your data tree outside-in with placeholders for the slow parts, so one slow query stops holding up everything behind it.

