Learning Objectives
- Describe a full-page round trip in order — request, server work, response, teardown, rebuild — and say what the browser destroys at each stage
- Enumerate what a navigation discards: scroll position, focus and selection, unsubmitted input, and the entire JavaScript environment the page had built
- Do the arithmetic that matters — bytes on the wire against bytes that actually changed — and reason about the wait separately from the bytes
- Name what this model gets right for free, in enough detail to notice when a later step gives it up
- Judge when the postback is still the correct design, and identify the two things that move an interface out of its range
The pattern
Before there was any other option, an application was a set of pages. A filter, a sort, a page of results, a saved edit, a checkbox: every one was a link or a form. Following it meant leaving the page you were on. The industry word for it was the postback. The page submits back to itself, the server reads what you asked for, and the response is the next version of the page.
The loop is short enough to write in one line. The user acts, the browser issues a request, the server does whatever work the action implies, the server renders an entire HTML document, the browser throws away the document it had and builds the new one. Then it waits for the next action and does it again.
That is the entire client. There is no code in it, as step 01 showed. Look at the other half of the exchange, what comes back:
Read the Content-Length next to the request that produced it. The question was eight characters long. The answer was a building.
What a full page costs
"Slow" is not a useful description of this, because a postback on a fast server over a fast link is not slow. The costs are specific, and some of them survive even when the server answers in a millisecond.
The bytes
The document comes back in full every time, whether one list item changed or none did. A page of any real complexity is tens of kilobytes of HTML before the interesting part; the interesting part is often a few hundred bytes. That ratio is the number to hold on to. Every later technique in this series is an attempt to send only the second number.
Be careful how far you push that argument. One part of it stopped being true a long time ago. The subresources usually do not come back: a stylesheet, a script bundle, or a logo with a long Cache-Control lifetime is served from the browser's own cache without a request going out at all. Compression takes a further bite out of what remains. So the honest version of the complaint is narrower than the folklore: what repeats is the HTML document and the server work that produced it, not the whole page weight. That is still the wrong thing to repeat, and it is usually the most expensive thing on the page to generate.
The wait
The classic symptom was the white flash: the old page disappearing, the window going blank, the new page painting in from the top. That specific symptom is largely gone. Chrome has held the previous page's pixels on screen across same-origin navigations since 2019 rather than blanking, and cross-document view transitions now let a site animate the swap deliberately. If you learned that postbacks flash white, check it in a current browser before you repeat it.
What did not go away is the wait itself. The old document stays live for the whole of it. It scrolls, it accepts keystrokes, and clicking a different link will cancel the navigation you started. Nothing the user does in that window is remembered: the document that would have remembered it is already on its way out, and the one that would honor it does not exist yet. Type into a field during a postback and the keystrokes land somewhere that is about to be discarded. The gap is at least network latency plus server render time. Queueing, time to first byte, and parsing the new document all add to it. And it happens on every interaction, not just the expensive ones.
The state
This is the part that is easy to underrate, because each item sounds minor and the list does not.
- Scroll position. A navigation to a URL you have not visited starts at the top. If the control you used was halfway down a long page, you are now looking at the header instead of at the thing you changed.
- Focus and selection. Focus resets to the new document. A keyboard user who had tabbed to the eleventh control starts over at the first; a screen reader announces an entirely new page rather than the change the user asked for.
- Unsubmitted input. Anything typed into a field that was not part of the submission is gone. The browser does restore form values when you navigate back to a page you have already seen. That is session history restoration, and it is useful. It does nothing here, because you are going forward to a URL you have not been to.
- The JavaScript environment. Every variable, every closure, every timer, every open connection, every cached result the page had computed. Not reset. Gone. The new document gets a new global object and runs its scripts again from the first line. Anything that has to survive must have been written down somewhere outside the document: the URL, a cookie,
sessionStorage, or the server.
Read that last one as a design constraint. In this model the URL is the application's memory. State that is not in the URL, or in something the server can look up from the URL, does not exist after the next click.
What this model gets right, for free
None of the above is an argument that the postback is broken. What it does correctly matters, because every later step in this series takes one of these back off the browser and re-implements it by hand:
- History. Each navigation is a session history entry. Nobody called anything to make that happen.
- The back button. It returns to the previous view, restores its scroll position, and restores its form values.
- Bookmarking, sharing, and opening in a new tab. The URL names the view, so the view can be saved, sent to someone, or opened twice.
- Reload. Pressing refresh gets you the same page you were looking at, not the application's front door.
One caveat: that list holds for GET. A literal postback in the ASP.NET WebForms sense POSTs to its own URL, and a POST forfeits the last two items. The URL no longer describes the view, so there is nothing useful to bookmark, and refreshing produces the browser's "confirm form resubmission" prompt. The standard answer, then and now, is Post/Redirect/Get: answer the POST with a redirect to a GET URL, so the page the user ends up on is one they can bookmark and reload safely. Even by the mid-2000s, keeping the URL honest took deliberate work.
Demo: the postback
The page below is the step 01 feature rebuilt as a postback. The form has no action, so it submits to its own URL; changing the limit is therefore a full navigation, and the frame loads a whole new document.
Do it in this order, because the order is what makes the cost visible. One: type something into the field marked Notes (not submitted). Two: scroll all the way down to the bottom marker, where the limit control is waiting. Three: change the limit and press Apply.
You land back at the top of the page. The notes field is empty. The load counter in the dark strip has gone up by one, because that counter is in sessionStorage. A plain variable would have been zeroed along with everything else. The page will tell you, in pixels and characters, what it was holding before you pressed the button.
Notice what it took for that page to be able to report its own losses: it had to write two numbers to sessionStorage on the way out and read them back on the way in. That is a handful of lines to produce a description of state that the browser used to just keep. Recovering the state itself costs considerably more. The rest of this series is largely about paying for it.
The waterfall is empty again — and this time it is the whole document it missed
Step 01 established why <http-waterfall capture="true"> shows nothing for a form submission: it captures by patching fetch and XMLHttpRequest in a document, and a navigation is issued by the browser, not by any script. All of that still applies. The reason to raise it again is that the miss is larger here, and the shape of it is different:
- The request happened. The counter incremented, the list changed, the URL carries a new query string. Nothing failed.
- The request it could not see was the request for this page. In step 01 the panel missed a request for some JSON. Here it missed the document it is running inside. No amount of instrumentation on a page can capture the request that fetched that page, because the page was not there to instrument when the request went out.
- DevTools does show it. Open DevTools, switch to the Network panel, and press Apply again. The top entry is the document itself: type
document, with the?limit=query string, a status, and a transfer size. Everything the new page loads is listed beneath it. The browser has the complete record. The page has none of it.
Keep that asymmetry in mind for step 03. When the page starts making its own requests, the panel fills up. The tooling did not improve. The requests moved inside the page's reach, the same event as the page becoming responsible for them.
Why it was fine, and when it stopped being
It would be easy to read the previous two sections as an indictment. They are not. For a document, this design is not a compromise people put up with until something better arrived. It is correct, and it is still correct.
A document has one job: to be read, and to be findable and referenceable afterward. The page-per-URL model does that job exactly. Every view has an address. The address can be linked, bookmarked, mailed, cited, indexed by a search engine, opened in fifteen tabs, or printed. Find-in-page works because the content is in the document. The back button works because the browser knows where you were. None of it requires a line of JavaScript, so none of it breaks when the JavaScript fails to load, throws, or is turned off. An enormous amount of the web is documents: reference material, news, documentation, most of a course site, this page. For all of it, the correct architecture is the one described above.
Two things move an interface out of that range, and it takes both:
- The interaction rate goes up. A document is read; an application is operated: filtering, sorting, expanding, checking, dragging. When a user's session involves three navigations, a full round trip each time is invisible. When it involves three hundred, the same cost is the interface.
- The changed fraction goes down. Sorting a table of results changes the table. Ticking a checkbox changes a checkbox. When the part of the page that differs between two consecutive views is a small and shrinking share of the page, the document-shaped answer is mostly a re-send of things that were already correct.
Either one alone is survivable. Together they are the whole problem: a filter that changes eight list items should not cost a whole document, and if the user is going to change that filter forty times in a sitting, it especially should not.
What happened next is not a story of steady improvement. The techniques in the following steps solved the round-trip problem. Then, for about a decade, a great many applications used them to break every single item in the "for free" list above: URLs that described nothing, back buttons that did nothing or the wrong thing, pages that could not be bookmarked or shared, content that was invisible without JavaScript. The postback did not lose an argument. It got replaced, unevenly, by something faster that had to spend the next fifteen years re-learning what it had thrown out.
So can we do the communication ourselves?
That is the question the next step answers. Pause on it first: the answer is yes, and yes is the easy part.
If the page could issue the request itself, the response could be just the data: the eight list items, not the building. The page would still be there when the answer came back, so the scroll position, the focus, the notes field, and every variable the page had computed would still be there too. That much follows immediately, and it is a large win.
What follows a moment later is the bill. The browser was not only transferring bytes on that navigation. It was also deciding when the page had changed, telling the history stack about it, updating the address bar, making the back button mean something, reporting failures in a way users recognize, and announcing a new document to assistive technology. Take over the request and you have taken over all of that too, whether or not you have noticed:
Every line in that comment block is a real step in this series, and none of them is optional in a real application. "Can we do the communication ourselves?" has an obvious answer. The question step 01 ended on is the harder one: what was the browser doing here that you are now responsible for, and are you going to do it as well?
Step 03 is where the answer starts arriving, and where the name for all of this gets attached.
Next Steps
In Step 3: Ajax appears, the page makes the request itself for the first time:
- Where the name came from, what it originally stood for, and which letter of it never really held
- The shift itself: a request issued by the page, an answer that is data rather than a document, and a DOM update instead of a navigation
- Hello Ajax World walked through line by line, with the waterfall finally showing something
- The complexity critique, stated up front rather than discovered later: owning the request means owning state, history, and routing
- What the demo in this step would have to grow to keep everything the postback version handed you for nothing