Skip to content

Chapter 8: Ajax and the End of the White Page

There’s a particular kind of waiting that shaped the modern frontend.

Click. White page. Wait. New page.

For the document web, this was normal. A user clicked a link or submitted a form, the browser requested a new resource, the current document disappeared, and a new one arrived. The model was clear, addressable, and resilient. It also wasn’t continuous. Every meaningful transition belonged to the network, and every transition meant the page went blank while it happened.

For text and link navigation, the white page was fine — even a feature, in the sense that it gave the user feedback that something was happening. For applications, it was a problem. A user changed a filter and waited for an entire page to reload. They submitted a comment and lost their scroll position. They expanded a record and watched the whole interface flash. They searched and waited. They saved and waited. The browser was doing exactly what it had been designed to do. The experience felt less like software and more like transportation.

The fix had a name. It was called Ajax, and the story of where it came from is more surprising than the name suggests.

Most accounts of Ajax start in 2005. The technology had been in browsers for years by then.

In 1998 and 1999, a small team at Microsoft was building Outlook Web Access — a browser-based version of Outlook intended to give Exchange Server customers a way to read corporate email without installing the desktop client. The product had ambitious requirements for its day. It needed to show folder lists, message threads, calendar entries, and contacts in a single interface that felt responsive. Full-page reloads on every interaction were going to make the product unusable.

The OWA team’s solution was to add an ActiveX control to Internet Explorer that let JavaScript make HTTP requests in the background. The control was called Microsoft.XMLHTTP. It shipped as part of Internet Explorer 5 in March 1999. Alex Hopmann, the engineer most often credited with the design, has written publicly that the team didn’t expect the API to become a major piece of the web platform — they were solving a specific product problem for a specific enterprise customer.

The API spread anyway. Mozilla reverse-engineered it for the Mozilla browser in late 2000 and standardized the constructor name as XMLHttpRequest (without the vendor prefix), which is the name the rest of the industry then adopted. Safari added support in 2004. Opera followed. By the time the rest of the web noticed what was possible with the API, every major browser had it.

The capability was already in the field. It was waiting for someone to use it at scale.

In April 2004, Google launched Gmail. The headline announcement was the storage offer — one gigabyte of free email storage, at a moment when Hotmail offered two megabytes and Yahoo Mail offered four. The storage was the headline. The product itself was the bigger story.

Gmail was a web-based email client that felt like an application. The interface didn’t reload between actions. Selecting a conversation didn’t navigate to a new page; it expanded the conversation in place. Searching didn’t replace the inbox; it filtered it. Composing a message opened a panel within the current page. The whole product was built on asynchronous requests against the server, with JavaScript managing the interface in between.

Plenty of people, the first time they saw Gmail, assumed it was using some kind of plugin. It wasn’t. It was using XMLHttpRequest and a lot of carefully written JavaScript.

Ten months later, on February 8, 2005, Google launched Maps. The product reset what the web was capable of in a single afternoon.

Before Google Maps, web maps worked by submitting a form to a server and waiting for a new image to arrive. The user typed an address, pressed enter, waited for the page to reload, and saw a static map. Panning meant clicking a directional arrow, which submitted another form, which produced another static image. The model worked but wasn’t pleasant.

Google Maps was different in a way that was viscerally obvious the first time anyone used it. You could grab the map with the mouse and drag. The map followed your cursor in real time. New tiles loaded continuously at the edges of the viewport as you moved. Zooming in didn’t reload the page; it animated. The interaction had the quality of native software running on the user’s machine, except the data was coming from Google’s servers continuously as the viewport changed. The implementation used XMLHttpRequest to request map tiles on demand and the DOM to position them.

The combination of Gmail and Google Maps in the same eighteen-month window did something to the field. They were proof — visible, functional, free to use — that the browser could run applications that didn’t feel like documents at all.

On February 18, 2005, ten days after Google Maps launched, Jesse James Garrett published an essay on the website of his consulting firm Adaptive Path. The essay was titled Ajax: A New Approach to Web Applications.

Garrett’s essay didn’t introduce any new technology. The technologies it described — JavaScript, XMLHttpRequest, the DOM, CSS, XML or JSON for data — had all existed for years. What the essay did was name the pattern.

Ajax isn’t a technology. It’s really several technologies, each flourishing in its own right, coming together in powerful new ways.

The acronym stood for Asynchronous JavaScript and XML, though by 2005 the XML part was already being replaced by JSON in most production applications. Garrett’s claim was that the combination of these technologies, used the way Gmail and Google Maps were using them, represented a new style of web application — one that closed the gap between web pages and desktop software.

The essay went viral in a way few technical essays do. Within a year, Ajax had become a job title, a conference topic, a category of library, a buzzword in technology marketing, and the organizing concept for a wave of frontend work that lasted through the rest of the decade. The naming mattered because it gave the industry a label to coordinate around. The pattern wasn’t new in 2005, but the conversation about it was.

The right credit on Ajax is hard to assign cleanly. Microsoft’s OWA team built the original API. Google’s Gmail and Maps teams showed what the API could do at scale. Garrett gave the pattern its name. The web industry then built the libraries and frameworks that made the pattern accessible to teams that weren’t Google.

The architectural idea underneath Ajax was simple. Don’t reload the whole page when only part of the page needs to change.

A search box could fetch matching results. A cart could update its total. A message could send and appear in a thread. A dashboard could refresh a panel. A form could validate a username before submission. A map could pan without loading a new page.

The application began to feel continuous because the document stayed alive.

The early mechanics were rough. XMLHttpRequest wasn’t elegant. Browser differences still mattered. Data formats varied. Error handling was manual. DOM updates were imperative. The effect was transformative anyway. A simplified modern version of the same pattern looks like this:

const results = document.querySelector('#results')
const search = document.querySelector('#search')
search.addEventListener('input', async () => {
const response = await fetch(`/api/books?q=${encodeURIComponent(search.value)}`)
const books = await response.json()
results.innerHTML = books
.map(book => `<li>${book.title}</li>`)
.join('')
})

This uses modern fetch rather than XMLHttpRequest, but the shift is the same. User interaction no longer has to equal full navigation. The page stays. The data changes. Only the affected region of the DOM updates.

That’s the beginning of the page becoming an application container.

The trade is straightforward enough to summarize. The page stays alive, and the page now owns more.

A traditional full-page navigation delegated a lot of responsibilities to the browser. The browser knew it was loading. The URL changed. The old document went away. The new one arrived. Assistive technologies could encounter a new page. The lifecycle was crude but complete.

Ajax broke each of these guarantees and asked the application to recover them by hand. If a request was in flight, the application had to show that somewhere — a spinner, a disabled button, a status message. If the request failed, the application had to show that, too, in a way that didn’t tear up the existing UI. If the user changed the input before the previous request finished, the application had to decide whether to cancel the old request, ignore its response, or somehow merge results. The URL had to be kept in sync with new state manually using the History API (added later) or it would silently drift, and bookmarks and the back button would stop working. Accessibility announcements that the browser had handled for free during a full-page navigation now required aria-live regions and careful coordination.

A small Ajax-driven save button might look like this:

button.addEventListener('click', async () => {
button.disabled = true
status.textContent = 'Saving…'
try {
await fetch('/api/profile', {
method: 'POST',
body: new FormData(form)
})
status.textContent = 'Saved'
} catch {
status.textContent = 'Could not save'
} finally {
button.disabled = false
}
})

The handler is doing eight things: reading form data, disabling the UI, showing a loading state, sending the request, handling success, handling failure, updating a message, restoring the button. Now add analytics, audit logging, draft clearing, optimistic UI, validation, retry, navigation, and accessibility announcements. The click handler becomes an integration point for half the application.

Anyone who has built service-oriented backends will recognize the shape of this problem. Coarse-grained, page-sized server interactions are easy to reason about and slow; fine-grained, per-component interactions are responsive and require careful coordination to avoid inconsistency. Ajax was the moment frontend code started looking like distributed-systems code, and the field would spend the next twenty years figuring out how to organize the result.

Ajax also created a new separation. HTML on one side, data on the other.

In the document model, the server returned a representation the browser could display directly. After Ajax, the server often returned JSON that only application JavaScript knew how to interpret. The change was sometimes obvious (REST APIs returning JSON arrays) and sometimes less so (server-rendered fragments returning HTML strings that the client knew how to inject).

The JSON path gave the frontend more control. It also made the page more dependent on JavaScript. If the script failed to load, or the rendering code had a bug, or the client’s data model drifted from the server’s, the user saw nothing — and the platform couldn’t recover semantics from raw JSON the way it could from HTML.

This is the question Carson Gross’s htmx revives, twenty years later. The htmx argument is that the server can still return meaningful HTML fragments, and that hypermedia — markup that carries its own meaning — is a stronger contract than JSON-plus-renderer. The position isn’t that JSON is wrong. The position is that the original Ajax pattern had a fork in the road, and the JSON side of the fork has costs the field has spent two decades paying.

Kitsune’s position is different but related. If the client renders or enhances the interface, it should use browser-native semantics — real markup, real attributes, real events, real form elements — so that the platform’s contracts (accessibility, focus, history, validation, copy, selection) continue to apply to what the client produced.

The Ajax-era choice between HTML fragments and JSON-and-a-renderer turned out to be one of the most consequential design decisions in frontend history. The next several chapters keep returning to it.

The history of the frontend often treats Ajax as a stepping stone to single-page applications. It was. It was also something more flexible than the SPA architectures that followed.

Ajax could enhance a traditional page. It could power a single widget. It could update a section. It could support progressive enhancement. The page could still be server-rendered, and forms could still be forms, and links could still be links, with JavaScript adding fluidity at specific points where it earned its place.

That middle ground got lost when the SPA model crystallized. By 2010 or so, Ajax had stopped meaning “the page sometimes updates a region asynchronously” and had started meaning “the page is now an application, and the application owns everything.” The SPA framework era — Backbone, Knockout, Angular, Ember, eventually React — accelerated the shift. The page became, in many product teams’ minds, a single mount point. The browser became a runtime to host a framework rather than a platform with its own primitives.

The middle ground is worth recovering, and the modern browser makes recovery far more practical than it was. We now have fetch, AbortController, FormData, URLSearchParams, templates, custom elements, observers, the History API, view transitions, and a much more capable CSS. We can build fluid interactions without automatically turning the entire app into a framework-owned island.

Plenty of applications still need long-running client-side state. The point isn’t that SPAs are wrong. The point is that the choice — how much of the page should be client-owned — used to be made interaction by interaction, and it became a single architectural commitment somewhere around 2010. Whether that commitment is still the right one for any given product is a question this book will return to.

Ajax ended the white page. It also created the coordination problem that the rest of the frontend’s history is the story of solving.

The first response to that problem was a generation of libraries built to make XMLHttpRequest, DOM manipulation, event handling, and animation tolerable across the browsers of the mid-2000s. The most important of those libraries was jQuery, and that’s the next chapter.

Return to the document app from the previous chapters.

Add a page with a search field and a results list. Use JavaScript to fetch data and update only the results area.

You can use a local JSON file as the data source:

[
{ "title": "Kindred", "author": "Octavia E. Butler" },
{ "title": "A Wizard of Earthsea", "author": "Ursula K. Le Guin" },
{ "title": "Neuromancer", "author": "William Gibson" }
]

Implement:

  1. A search input.
  2. A loading message.
  3. An error message.
  4. A rendered list of results.
  5. A no-results state.

Then improve it:

  • Use AbortController to cancel a previous request if a new search begins.
  • Keep the current search term in the URL query string using history.pushState.
  • Make sure the results region has an accessible label.
  • Consider whether updates should be announced to assistive technologies using aria-live.

Reflection:

  1. What did partial updates improve?
  2. What state does the page now need to manage that it didn’t before?
  3. What happens if you navigate away and back — does the URL restore the search?
  4. What accessibility concerns appeared the moment the page stopped reloading?
  5. At what point would this kind of code want a larger architecture around it?

The white page is gone. The coordination problem has arrived.