Konrad Kowalski (rootsher)Principal Platform & Reliability Architect100101111101001001001101010000111100101111111001

AJAX: the moment a page started behaving like an application

date
category
Frontend
reading
4 min / 714 words

The classic web had a simple model: the user acts, the browser sends a request, the server renders a new document, the browser replaces the old document with the new one.

The problem showed up once interactions became too small to justify a full navigation.

Changing a table sort, autocomplete, updating the cart or refreshing part of a dashboard do not need a new document. They only need new data.

And that is exactly where AJAX came in.

A request without navigation

The key change was simple:

text
before:

interaction
  |
  v
navigation
  |
  v
new document


after:

interaction
  |
  v
background request
  |
  v
update fragment of current document

For the first time at scale, the browser started keeping the current document while talking to the server.

Technically the foundation was XMLHttpRequest.

js
const xhr = new XMLHttpRequest();

xhr.open("GET", "/api/search?q=keyboard");

xhr.onload = () => {
  const results = JSON.parse(xhr.responseText);
  renderResults(results);
};

xhr.send();

Today that code looks archaic, but architecturally it changed the web enormously.

A request stopped meaning navigation.

The server stopped returning only documents

In the classic model an endpoint usually answered with something like this:

text
GET /products/42
  |
  v
HTML

Once dynamic clients appeared, a second layer of endpoints started to grow:

text
GET /products/42
  |
  v
HTML

GET /api/products/42
  |
  v
DATA

The browser started deciding for itself what to do with the response.

It could fetch data:

json
{
  "id": 42,
  "name": "Mechanical Keyboard",
  "price": 129
}

and update only a fragment of the document:

js
priceElement.textContent = "$129";

That looks like a small change.

In practice it meant the birth of a new problem:

part of the presentation logic started living on the client.

Document state starts turning into application state

In a classic application the state was often encoded in the URL, the session and the database.

A new document was simply another snapshot of that state.

After AJAX, the browser started holding information the server no longer had to represent as a full document straight away.

For example:

js
let selectedCategory = "keyboards";
let currentPage = 3;
let sort = "price";
let query = "mechanical";

Now the UI can change many times without navigation.

That gives much better interactivity, but it creates a synchronisation problem:

text
server state
  ^
  v
client state
  ^
  v
DOM

Previously the DOM was mostly the effect of a server response.

Now it becomes a mutable representation of application state.

Google Maps showed what it was all for

One of the historically most important examples was Google Maps.

Imagine a map working under the classic document model.

The user drags the map by 50 pixels:

text
drag
  |
  v
request
  |
  v
render entire page
  |
  v
reload

Practically useless.

Fetching data dynamically allowed something entirely different:

text
drag map
  |
  v
calculate new viewport
  |
  v
request missing data
  |
  v
update map

The document stays the same.

Only its content and state change.

That is a fundamental step towards SPA, but it is not SPA yet.

AJAX did not immediately mean a JSON API

The name itself expands to:

Asynchronous JavaScript and XML

And XML really was an important data exchange format back then.

But AJAX was never truly about XML.

The core of the mechanism was:

JavaScript can make a request without navigating the document and use the response to change the current UI.

The response could be:

text
XML
JSON
HTML
text
later practically any binary format

That is why the modern:

js
const response = await fetch("/api/products");
const products = await response.json();

is conceptually a descendant of exactly the same change.

A curiosity

XMLHttpRequest did not start out as an open standard designed for modern web applications.

The mechanism grew out of a Microsoft implementation tied to Outlook Web Access and was initially available through ActiveX.

Only later were similar capabilities implemented natively by other browsers and standardised.

It is a good example of a common pattern in how the web platform evolves:

text
vendor-specific capability
  |
  v
real application use case
  |
  v
cross-browser adoption
  |
  v
standardization

Many modern Web APIs travelled a similar road.

The price of interactivity

AJAX solved the problem of full reloads, but not for free.

Once an application modifies the document on its own, it has to know:

text
which fragment of the UI is current
which data has already been fetched
what to do when a request fails
what to do when two requests come back out of order
how to restore state after a refresh
how to tie the URL to the current view

Consider a simple autocomplete:

text
"k"
  |
  v
request A

"ke"
  |
  v
request B

There is no guarantee the responses come back in the same order.

text
request B -> 80 ms
request A -> 300 ms

If the client blindly renders the last response, the result for "k" can overwrite the newer result for "ke".

Suddenly the frontend has problems previously known mostly to distributed systems:

asynchrony, event ordering and stale data.

From DOM manipulation to applications

The first AJAX applications often looked more or less like this:

js
fetchData(data => {
  document.getElementById("price").innerHTML = data.price;
  document.getElementById("stock").innerHTML = data.stock;
  document.getElementById("title").innerHTML = data.title;
});

With a handful of elements this works fine.

With hundreds of dependent elements it starts to get harder.

The code has to keep consistency manually between:

text
application state
  |
  v
DOM state

This is the problem that will generate the next wave of tools and architectures:

text
jQuery     - simplifies DOM manipulation and requests
Backbone   - tries to give the application model some order
AngularJS  - binds state to the view
React      - describe the UI as a function of state instead of syncing the DOM by hand

But before we get there, a more important change happens.

If an application can fetch data without reloading the document, why should the server render the next views at all?

The browser could receive the application once and then manage routing, state and rendering on its own.

That is how the era of Single Page Applications and Client-Side Rendering begins.