Appearance
6.1.1 — The Browser as a Program
Part 5 ended with bytes arriving on a socket. A TCP connection was opened, TLS was negotiated, an HTTP response came back, and the last thing we said about it was that the body is a stream of characters that starts <!DOCTYPE html>.
This page is about the program that receives those bytes. Not "the browser" as a product with a logo, but the browser as a set of operating-system processes doing a specific job, because almost every strange behaviour in frontend engineering — why a slow function freezes a scroll, why one tab crashing does not kill the others, why an iframe from another site cannot read your page — comes directly out of that structure.
1. A browser is many processes, not one
Open a browser, open the operating system's task manager (Chapter 2.2 covers what a process is), and you will see the browser listed a dozen times. That is not a bug and it is not memory waste for its own sake. Each of those entries has a distinct job.
The browser process owns the window itself: the tab strip, the address bar, bookmarks, history, and — importantly — the only permission to touch the disk and the network directly. It is the parent of everything else.
A renderer process owns the contents of a page. HTML parsing, CSS, JavaScript, layout and paint all happen here. This is where your code runs, and it is sandboxed: it cannot open a file, cannot open a socket, cannot read another tab's memory. When it needs any of those things, it asks the browser process over an internal message channel and the browser process decides.
The network process does everything Part 5 described — DNS, TCP, TLS, HTTP, the disk cache — for all tabs at once, which is why a connection can be shared and why the HTTP cache is not per-tab.
The GPU process turns the drawing instructions produced by renderers into actual pixels using the graphics card, for every tab in one place. It is separate because graphics drivers crash, and a driver crash should grey out the window for a moment rather than take down the browser.
Why the split exists at all
Early browsers were a single process. Three problems forced the change, and each one is worth understanding because each shows up again elsewhere.
Stability. One page's runaway script or one plugin's null pointer took the whole browser with it, losing every open tab. With one process per page, a crash kills one tab. You have seen the result: the sad-page icon in a single tab while the rest keep working.
Responsiveness. If page A is running a two-second JavaScript loop and page B shares the same thread, page B freezes for two seconds. Separate processes get separately scheduled by the operating system (Chapter 2.3), so a busy tab steals CPU but not your ability to switch tabs.
Security, which turned out to be the strongest reason. The renderer parses the most hostile input on earth: HTML, CSS, JavaScript, images and fonts from anyone. Assume it will eventually be compromised. Put it in a sandbox with no direct access to disk, network or other pages, and a successful exploit inside the renderer still has to break a second barrier to do real damage.
Site isolation, and the CPU bug that forced it
Originally one renderer process could host several pages, including a page and the third-party iframes inside it. Site isolation changed that: each site gets its own renderer process, including sites loaded in iframes.
The direct trigger was Spectre (Chapter 1.5). Spectre lets code read memory it is not allowed to read, by measuring how long things take rather than by reading them directly. Every software fix for it inside the JavaScript engine was a patch on a symptom. The structural fix is simpler to state: if another site's data is never in this process's memory, no amount of clever timing can read it.
The cost is real — more processes, more memory, roughly 10–13% more on typical browsing according to Chrome's own published measurements — and it was shipped anyway, which tells you how the trade was judged.
The consequence for you as an engineer: two documents from different origins genuinely cannot share memory or synchronous access, no matter what the JavaScript looks like. That is not a policy the browser could relax if it wanted to. It is now a process boundary, enforced by the kernel.
2. The main thread, and the sentence that explains Part 6
Inside a renderer process there are several threads, but one of them does almost everything you care about. It is called the main thread, and here is the sentence to carry through the rest of this Part:
Parsing HTML, running JavaScript, computing styles, laying out the page, and deciding what to paint all happen on one thread, one thing at a time.
Nothing about frontend performance makes sense without it. A for loop that takes 300 ms does not "slow the page down" in some vague way — it stops the page for 300 ms, because during those 300 ms the main thread cannot handle a click, cannot run an animation frame, and cannot lay out anything.
The threads that are not the main thread are worth naming now, because later pages send work to them deliberately:
- The compositor thread can scroll and can animate certain properties without the main thread's help. This is why a page with a stuck main thread sometimes still scrolls — and why some animations stutter while others do not. Chapter 6.1.2 explains exactly which ones.
- Raster threads turn drawing instructions into bitmaps.
- Web workers run your JavaScript off the main thread, with no DOM access (Chapter 6.3.3).
The browser's event loop is the same idea as Node's, with one addition
Chapter 3.6.8 taught the JavaScript event loop: a task queue, a microtask queue drained completely after each task, and the rule that nothing interrupts a running function. All of that is unchanged here. The browser adds one thing: a rendering opportunity.
Between tasks, the browser may decide it is time to produce a frame. If the display refreshes 60 times per second, a frame is due every 16.7 ms, and the whole render step — run requestAnimationFrame callbacks, recalculate style, do layout, paint, composite — has to fit inside what is left of that budget after your JavaScript has had its turn.
js
// (1) A task: this callback is queued as a macrotask.
setTimeout(() => {
console.log('task');
// (2) A microtask: runs BEFORE the browser gets a chance to paint.
Promise.resolve().then(() => console.log('microtask'));
// (3) A frame callback: runs at the START of the next render step.
requestAnimationFrame(() => console.log('rAF — about to render'));
}, 0);
// prints: task → microtask → rAF — about to renderLine (1) queues a task. Line (2) queues a microtask, and microtasks are drained to empty before the loop moves on, so it runs before any rendering. Line (3) asks to be called at the beginning of the next render step, which is the correct place to make visual changes because whatever you write there is guaranteed to be picked up by the layout and paint that immediately follow.
The practical rule that falls out of this: an infinite chain of microtasks starves rendering completely. A promise that resolves and immediately queues another promise never lets the loop reach the render step, and the page freezes with no long function anywhere in the profile. A setTimeout chain does not do this, because each task yields.
3. What actually happens between the URL and the first byte
Part 5 covered DNS, TCP, TLS and HTTP. What it did not cover is the surprising amount of work the browser process does before any of that, and a few decisions it makes after the response arrives. Here is the honest sequence.
Is this even a URL? Type banana into the address bar and you get a search. Type banana.com and you get a navigation. The browser applies a set of heuristics — does it have a scheme, does it contain a dot followed by a known-looking suffix, does it match a local hostname — and when in doubt it hands the string to the default search engine. This is the first branch and it is pure user-interface logic, not networking.
Is HTTPS mandatory for this host? Before any request leaves the machine, the browser checks its HSTS list (Chapter 5.7). If the host is on it — either because the site sent the header before or because it is in the preloaded list compiled into the browser — a typed http:// is rewritten to https:// internally. Nothing hits the network in plain text.
Is there a service worker for this scope? A service worker is a script the site previously registered that sits between the page and the network and can answer requests itself (Chapter 6.8.2). If one is registered for this URL's scope, the browser starts it and gives it the request first. A response can therefore come from the site's own JavaScript, from a cache, without a single packet.
Then the network process runs Part 5 — HTTP cache lookup first, and only on a miss the DNS, TCP, TLS and HTTP sequence.
What kind of thing came back? The Content-Type header decides. text/html means render it. application/pdf means hand it to the PDF viewer. An unknown type means download it. The header is authoritative and the file extension is not, which is why serving JavaScript as text/plain breaks a page and why X-Content-Type-Options: nosniff exists — it tells the browser to stop guessing when the type looks wrong, closing an attack where a user-uploaded "image" is actually a script.
Commit to a renderer. Only now does the browser process pick a renderer process — a new one if this is a different site, per site isolation — and hand it the stream. The tab's spinner starts, the old page is still on screen until the new one has something to show, and the URL in the address bar changes at this moment and not before. That last detail is deliberate: the address bar must never show a URL whose content has not been committed, or a slow malicious site could display a bank's URL over its own page.
A navigation that goes nowhere still costs something. If the server never responds, the browser has already resolved DNS, opened a socket and possibly negotiated TLS. This is why prefetching and preconnecting exist as separate optimisations in Chapter 6.7 — they let you pay those costs early for a URL you expect to need.
The back/forward cache, and why your page might be disqualified
When you navigate away, the browser would rather not destroy the page. The back/forward cache (universally shortened to bfcache) freezes the whole renderer — the DOM, the JavaScript heap, the scroll position — and keeps it in memory. Pressing Back then restores it instantly with no network request and no re-execution.
Certain things disqualify a page from it, and they are worth knowing because "back is slow on our site" is nearly always one of these:
- An open connection that cannot be frozen, such as a WebSocket or an in-flight
fetch(Chapter 5.8), depending on the browser's rules. - A
Cache-Control: no-storeheader on the page itself. - Using the old
unloadevent, which by definition assumes the page is being destroyed. This is the common one. Replaceunloadwithpagehide, and detect restoration with thepersistedflag:
js
// (1) Fired when the page is frozen OR destroyed. event.persisted tells you which.
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
// (2) Going into the bfcache. Pause timers, close sockets, but keep state.
pauseLivePriceStream();
}
});
// (3) Fired when the page is shown, including a restore from the bfcache.
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// (4) We were restored, not loaded. Nothing re-ran, so re-sync explicitly.
resumeLivePriceStream();
refreshBasketCount();
}
});Line (1) is the replacement for unload. Line (2) runs only when the page is being frozen rather than thrown away, which is your cue to release the things that block freezing. Line (3) is the mirror: it fires on a normal load and on a bfcache restore, and line (4) is where you handle the case that surprises people — no module re-executed, no framework re-mounted, no data was re-fetched. A basket count from four minutes ago is still on screen unless you refresh it here.
4. Why the browser asks for /favicon.ico when you never mentioned it
Load a page in a fresh browser with no <link> tag for an icon and watch the network panel: there is a request for /favicon.ico that your HTML never asked for. Server logs are full of them, and 404s for that path are the single most common "error" in a small site's logs.
The history explains it completely. In 1999, Internet Explorer 5 added the ability to show a small picture next to a bookmarked site. There was no markup for it and no standard. The implementation was the simplest possible thing: when a site is bookmarked, request /favicon.ico from the root of the server, and if it exists, use it. The name is a contraction of "favourites icon", after the Internet Explorer name for bookmarks.
It spread because it worked with no cooperation from site authors, other browsers matched the behaviour to be compatible, and later they started requesting it on every page load rather than only on bookmarking. The <link rel="icon"> tag was standardised afterwards to give authors control, but the fallback request never went away, because removing it would break every site that still relies on the convention. A default that requires nothing from the author is nearly impossible to retire.
This same shape — a well-known path at the root of a server, requested without being linked — turns out to be a general pattern of the web, and once you see it you notice it everywhere:
| Path | Who requests it | What it means |
|---|---|---|
/favicon.ico | Browsers | Site icon |
/robots.txt | Crawlers | Which paths not to fetch |
/sitemap.xml | Crawlers | Every URL worth indexing |
/.well-known/… | Various clients | A registered directory for machine-readable files |
/apple-touch-icon.png | iOS | Home-screen icon |
/.well-known/ is the one that got standardised properly, in RFC 8615, precisely because the ad-hoc versions were multiplying. It is a reserved directory where anything needing a fixed location can live without colliding with a site's own URLs: /.well-known/acme-challenge/ proves domain control when a certificate is issued (Chapter 5.7), /.well-known/security.txt publishes a contact for vulnerability reports, and /.well-known/change-password lets a password manager deep-link to the right page.
The engineering lesson worth taking out of the favicon story is not about icons. It is that a convention which requires zero adoption effort will be implemented by everyone, will then be depended upon by everyone, and will therefore outlive the reason it existed. Chapter 5.1 made the same point about protocol ossification. It applies just as hard one layer up.
What the interviewer will push on
"Why does a browser use multiple processes?" Stability, responsiveness and security, in that historical order but with security now dominant. The tell is naming site isolation and connecting it to Spectre — the fix for a hardware timing attack was a process boundary, because software patches inside one address space could only ever chase symptoms. Weak answers stop at "so a tab crash doesn't kill the browser".
"What runs on the main thread?" HTML parsing, JavaScript, style calculation, layout, paint scheduling — one at a time. The follow-up is usually "so what does not?", and the answer is compositing, rastering and workers. If you can say why a stuck main thread sometimes still lets you scroll, you have understood the split.
"Walk me through what happens when you type a URL." After Part 5's DNS/TCP/TLS/HTTP, the parts people forget are the pre-network steps — is it a URL or a search, the HSTS upgrade, the service worker check — and the post-response steps: Content-Type decides what happens, and the address bar only changes at commit. Volunteering the commit rule signals that you know why it is a security decision.
"A user says Back is slow on our site. Where do you look?" The back/forward cache. Then name the disqualifiers you can actually fix: an unload listener, Cache-Control: no-store on the document, an open connection. The common wrong answer is to start optimising the page load, which is treating the symptom — a bfcache hit skips loading entirely.
"Why does the browser request /favicon.ico?" A 1999 Internet Explorer convention that needed no markup, was copied for compatibility, and can never be removed. Use it to make the general point about defaults that require no adoption effort.
One thing to volunteer: mention that an infinite microtask chain freezes rendering while a setTimeout chain does not, and that this is one of the few page freezes with no long function in the profiler. It shows you understand that the render step is a participant in the event loop rather than something the browser does whenever it feels like it.
Recall
- A browser is many processes: a browser process (window, disk, network permission), one renderer per site, a shared network process, and a GPU process. The renderer is sandboxed on purpose because it parses hostile input.
- Site isolation gives every site its own renderer, including iframes. It was forced by Spectre: if another site's data is never in this process, timing tricks cannot read it. Cost was roughly 10–13% more memory, paid willingly.
- The main thread does parsing, JavaScript, style, layout and paint — one thing at a time. Every performance rule in Part 6 descends from this sentence.
- The browser event loop is Chapter 3.6.8's loop plus a render step with a 16.7 ms budget at 60 Hz. Microtasks drain before rendering, so an endless microtask chain freezes the page with no long function to blame.
- Before the network: URL-or-search, the HSTS upgrade, the service worker. After the response:
Content-Typedecides, then commit — the moment the address bar changes, deliberately late. - The back/forward cache freezes the whole page in memory. An
unloadlistener,no-storeon the document, or an open connection can disqualify it. Usepagehide/pageshowand checkevent.persisted, because on a restore nothing re-ran. /favicon.icois a 1999 Internet Explorer convention with no markup requirement, which is exactly why it is unremovable./.well-known/(RFC 8615) is the standardised version of the same idea.
Self-test: Why is the renderer the least privileged process? · What did Spectre change about browser architecture, and why could software alone not fix it? · Why can a page freeze with no long-running function in the profile? · Name two things that disqualify a page from the bfcache · Why does the address bar update at commit rather than at click?
Next: 6.1.2 follows the bytes into the renderer — how HTML text becomes a DOM tree, how the DOM and the CSS combine into something with sizes and positions, and where the pipeline can be cut short so that an animation never touches the main thread at all.