Incremental HTML updates
Performance is about the work a request costs the server. This page is about the two costs the server never sees: the bytes that go on the wire, and the time a reader spends looking at nothing.
They are separate problems. A page can be cheap to render and still take two seconds to become useful, because one query on it is slow. A page can render in microseconds and still re-send a navigation bar the browser has had open for twenty minutes. Popcorn Web has one mechanism for each, plus a third for the case where the server is the one that learns something new.
Better than static HTML
Section titled “Better than static HTML”A static file is the floor everyone measures a web framework against. Nothing to compute, cache it forever, serve it from an edge. It is hard to argue with.
It is also, on the second page view, strictly worse than what this framework sends.
That is the whole argument, and it is worth being precise about why it holds. A
static server answers every request identically because it has to: two browsers
asking for /orders?page=2 get the same bytes, and one of them may have arrived
from /orders?page=1 while the other opened a bookmark. Sending the difference
would require knowing which. Popcorn Web knows, because the browser tells it in
the request, and the answer is the changed region rather than the document.
The catch a claim like this usually hides is that you have paid for it somewhere else — a client-side router, a hydration pass, a build step, a page that shows nothing until a bundle loads. There is none of that here. What reaches the browser is server-rendered HTML from first byte to last, and the one script involved moves finished markup into place. Turn it off and you are back to the static-file behavior you started with, which is covered in full below.
Three mechanisms, three questions
Section titled “Three mechanisms, three questions”The three differ in when the server knows something the browser does not, and that is the only question you need to pick between them.
The times are illustrative, but the events are the distinction. A dashed tick is the browser asking. Async rendering asks once and receives one response in parts. Partial updates ask again and receive a much smaller second answer. Live rendering asks once, keeps the pale connection open, and receives a mark only when the server has something new.
The rule for choosing between the second and the third is about the URL, not about the mechanism. State that can live in the URL belongs there — a sort order, a page number, a filter — because that makes the page shareable and back-navigable, and partial updates handle it with no code. Reach for live rendering when there is no request to attach the new information to, because what changed happened on the server while nobody was typing.
Async rendering — making the first view early
Section titled “Async rendering — making the first view early”A page is usually as slow as its slowest query. The handler waits for everything, the template renders once, and the reader gets a blank tab until the last dependency answers.
An {await} block breaks that coupling. The section renders a fallback
immediately and the response commits with it; when the value settles, the
finished markup is written into the same response and takes the fallback’s
place.
The important property is not the total. It is that the status code, the head, and every value that was ready leave the server before the slow work finishes. Nothing about the handler changes to get this: whether a response streams is a property of the templates it composed.
Async Rendering covers pw.Go, the
timeouts, and what a failed boundary does to a response that has already
committed.
Live rendering — when nobody asked
Section titled “Live rendering — when nobody asked”Async rendering delivers a slow section once. A chat log, a metrics panel and a notification feed want the opposite: the server learns something, and a region of a page somebody is already looking at should say so.
The alternative most applications reach for is polling, and its cost is not the requests — it is that almost all of them find nothing.
A live source is declared external live and bound in the same {await} clause
an async value is, so a source that changes from async to live changes no
template that calls it. The handler changes nothing at all, and the browser
holds one connection back to the page’s own URL for as long as the screen is
open.
Live Rendering covers the source signature, reconnection, and what happens when a subscription outlives its data.
Partial updates — every view after the first
Section titled “Partial updates — every view after the first”Every layout and page of a rendered chain is already an update boundary, with an identity and a digest of what it rendered written onto it by generation. A request arriving with the old digests can be answered with the difference.
An ordinary component is deliberately not a boundary, and that decision is what keeps the mechanism cheap.
The runtime intercepts a same-origin link or a GET form, re-requests the page’s
own URL, and applies what comes back — so a search form refines the page it is on
with no application JavaScript at all. Nothing is written on either side; this is
what enabled = true buys on its own.
Partial Updates covers the two other paths — redrawing one component, and answering a mutation with the regions it changed — along with the validator key that enabling this requires.
What survives with JavaScript off
Section titled “What survives with JavaScript off”Here the three genuinely differ, and rounding that off to “it degrades gracefully” would cost somebody an afternoon.
Partial updates are the outlier, and the reason is structural rather than
careful. There is no fallback implementation to maintain, because the runtime
never replaced anything: a link is an <a href> and a filter is a
<form method="get">, and the runtime is an optimization of what that markup
already does by itself. A request that carries no update header is answered with
the ordinary document, so a crawler, curl, and a browser with scripting
disabled are all unaffected. That is also the standard the intercepting half is
held to — every gesture it takes over has to reach the destination the browser
would have reached on its own.
Async rendering has to ask, because a browser with scripting off is not a
crawler and sends nothing that says so. <noscript> is the one HTML feature
that fires precisely when scripting is off, so the framework contributes a block
to the head of a streamed page that redirects to that same page under a marker
parameter — and the marked request renders buffered. The reader lands on the
page they asked for, complete, at the same path. What they give up is the
progressive delivery and one round trip on the first page of a visit; a cookie
remembers the answer for the rest of it, and a scripted browser never sees any
of it. Turn it off with scriptless_detection = false, or take the older
site-wide answer with streaming = false, which gives up the early paint for
everybody.
Live rendering is the one that cannot be rescued, and it should not pretend otherwise: delivering without being asked is the feature. It is not nothing, though. The buffered branch renders a live boundary from its first delivered value, so a scriptless reader gets a real snapshot rather than a fallback that never resolves.
When not to reach for any of this
Section titled “When not to reach for any of this”A page that reloads in a hundred milliseconds needs none of it. Each mechanism adds something real — a browser runtime on every document, a secret to deploy, a rule that every re-render must be free of side effects, a held-open connection per open screen — and a reload nobody notices is not a cost worth paying to remove.
Reach for async rendering when one section is measurably slower than the rest of the page, not when the page as a whole is slow; if everything is slow, the fix is in the queries. Reach for partial updates when a screen is refreshed often enough that the reload is the thing people complain about. Reach for live rendering when the alternative is polling that mostly finds nothing.
And when the application wants to own the swapping itself, none of these is the right shape — Fragments and islands renders one template with no negotiation and no ordering guarantee, and hands it to whatever library you chose.
