FoundationsGENERALENGINE-SPECIFIC

The Browser Is a Runtime

Not a document viewer with scripting bolted on: a sandboxed application platform with a scheduler, a memory model, a renderer and a security boundary.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What is actually executing my application, and what services does it provide?

The user intent

Someone wants to use a product. They type an address, or tap a link, and expect a working application — one that responds to a keystroke faster than they can notice.

The obvious build

The browser displays HTML. CSS makes it look right and JavaScript makes it interactive. Beyond that it is a black box, and the framework deals with it.

Why it breaks

The black box has a scheduler, and it is the reason a for loop over 50,000 rows freezes the page while a fetch over the same data does not. No amount of framework knowledge explains that difference; the runtime model does.

How it breaks in a real browser
  • The black box has a scheduler, and it is the reason a for loop over 50,000 rows freezes the page while a fetch over the same data does not. No amount of framework knowledge explains that difference; the runtime model does.
  • It has a memory model, and a component that adds a resize listener without removing it will hold its entire subtree alive across every route change until the tab is closed.
  • It has a rendering pipeline with distinct stages, so changing width and changing transform cost the browser different amounts of work for visually similar results (The Cost of a Change).
  • It has a security boundary, and "it works on my machine" versus "it fails in production" is very often the same code on two different origins (The Same-Origin Policy).
  • It is many browsers, on devices spanning an order of magnitude in CPU speed. A page that is instant on the machine it was built on can be unusable on the median phone that loads it.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The browser is a host environment: it supplies a JavaScript engine plus a large set of APIs that are not part of the language at all. setTimeout, fetch, document and localStorage are the browser's, not JavaScript's — which is why the same engine in a different host has none of them.
  • It schedules work. One thread per page owns your JavaScript, the DOM, style and layout. That thread runs tasks to completion, drains a microtask queue, and only then gets a chance to produce a frame (The Event Loop, Precisely).
  • It renders through a pipeline. DOM plus CSSOM become computed styles, which become geometry, which becomes paint commands, which the compositor turns into a frame (The Rendering Pipeline).
  • It isolates. The origin — scheme, host and port together — is the unit of trust. Code from one origin cannot read another's DOM, cookies or storage unless the other origin opts in (Origins and the Sandbox).
  • It manages resources on your behalf: it caches responses, prioritises requests, reuses connections, decodes images off the main thread, and evicts memory under pressure. Some of that you can influence; much of it you can only avoid fighting.
  • It persists. Cookies, storage, caches and service workers outlive the page and often the session, which makes "what does a returning user have" a design question rather than an implementation detail (Choosing Browser Storage).

What this makes the browser do

And which of it is avoidable.

  • Parsing HTML into a tree, incrementally, as bytes arrive — the parser does not wait for the whole document (Streaming HTML).
  • Parsing CSS into the CSSOM, then computing a final value for every property on every element.
  • Compiling and executing JavaScript on the same thread that owns the DOM, so script and rendering are in direct competition for it.
  • Computing geometry for every box whose size or position could have changed, then producing paint commands, then compositing layers into a frame.
  • Doing all of the above repeatedly, ideally once per display refresh, while also handling input, timers, network callbacks and garbage collection.

What the runtime actually provides

It is worth separating the language from the host. JavaScript the language gives you values, functions, objects, promises as a concept, and a job queue. It does not give you a timer, a network call, a document, or a way to store anything. Every one of those is supplied by the browser, and that is why the same engine embedded in a server runtime has an entirely different surface.

This matters practically rather than pedantically: when something behaves strangely, knowing whether you are looking at a language behaviour or a host behaviour tells you where to look. Promise ordering is the language. Whether a timer fires while a tab is backgrounded is emphatically the host.

  • Execution — a JavaScript engine, plus WebAssembly, plus the compile and optimise pipeline underneath both.
  • Scheduling — the event loop, task sources, the microtask checkpoint, and the rendering opportunity between them (The Event Loop, Precisely).
  • Document — the DOM and CSSOM as live, mutable object models of structure and style (The DOM Is Not Your HTML).
  • Rendering — style, layout, paint and compositing, running on the browser's schedule and not yours (The Rendering Pipeline).
  • Networking — connection reuse, prioritisation, caching, and the fetch API on top of all of it (Browser HTTP Caching).
  • Storage — cookies, Web Storage, IndexedDB, Cache Storage, each with a different lifetime and a different failure mode (Choosing Browser Storage).
  • Security — origins, CORS, CSP, cookie attributes, secure contexts, and the sandbox around it all (The Browser Security Model).
  • Accessibility — an accessibility tree derived from your DOM and handed to assistive technology (The Accessibility Tree).
The browser as a host environment
callsqueues workruns tasksrendering opportunityconstrainspartitionsYour application codeOrigin sandboxJavaScript engineBrowser APIs (DOM, fetch, storage, timers)Event loop / schedulerNetwork stack + HTTP cacheCookies / Storage / IndexedDBStyle → Layout → Paint → CompositePixels + accessibility tree
UserLLMAgentToolDataDecisionHumanGuardrail

One thread, and everything wants it

The single most consequential fact about the browser runtime is that one thread per page runs your JavaScript and owns the DOM and computes style and layout. The compositor and some other work live elsewhere, but the thread you write code on is the one the user is waiting for.

Because tasks run to completion, a task that takes a long time cannot be interrupted. The browser is not ignoring the user's click during that time — it is unable to get to it. That is why "the page froze" and "my function was slow" are the same sentence, and why the fix is almost always to break the work up or move it, not to make it marginally faster (Yielding and Scheduling).

Two readings of the same line
Framework-shaped
setRows(sortBig(rows))
// "update some state"
Runtime-shaped
setRows(sortBig(rows))
// a task on the only thread that can paint:
//   sortBig runs to completion — no input, no frame
//   -> state change -> reconcile -> DOM mutation
//   -> style -> layout? -> paint? -> composite
// how long is the task? what did it invalidate?
// does the user get a frame before it finishes?

The second reading tells you why the click that arrived mid-sort felt ignored, and which of the three available fixes — yield, move off-thread, or do less work — actually applies. The first tells you that state changed.

The same code, two very different machines

Frontend code runs on hardware you do not choose, over networks you do not control, in browsers with different engines and different versions. This is the structural difference from backend engineering: there, you pick the runtime and the machine, and you can measure the one place your code runs. Here, the population of machines *is* the runtime.

The practical consequence is that a single local measurement is not evidence. Local profiling tells you where time goes on one device; only field data tells you what your users experience, and the two regularly disagree about which problem matters most (Measure Before Optimising).

Where "works on my machine" comes from
TriggerSymptomCauseResponse
Development machine is 5–10x faster than the median deviceInteraction feels instant locally, sluggish in support ticketsJavaScript parse, compile and execute all scale with CPU; so do style and layoutProfile with CPU throttling, and trust field percentiles over local timings (Interaction Responsiveness).
Local network is fast and warmLoad is instant locally, slow on first visit in the wildNo cold cache, no real latency, no connection setup costTest with an empty cache and simulated latency; read the waterfall, not the total (Reading a Network Waterfall).
Only one browser testedA feature throws on load for a subset of usersAPI availability differs by engine, version and secure contextFeature-detect rather than assume; fail to a working baseline (Polyfills vs Transpilation).
Always tested on a fresh page loadApp degrades after prolonged useRetention accumulates across navigations that never unload the documentProfile memory across repeated route changes, not once (Memory Leaks).
Tested only with a mouseKeyboard and screen-reader users cannot complete the flowSemantics and focus were never exercisedTab through every flow; the keyboard is the cheapest accessibility test there is (Keyboard Operability).

How to build it

Most important first.

  • Learn the pipeline before any framework's API. Every framework ultimately mutates the DOM and reads computed style; the costs of doing so are the browser's, not the framework's (The Rendering Pipeline).
  • Treat the main thread as the scarcest resource in the system. It is the only thread that can touch the DOM, and everything the user feels — input response, animation, scrolling — queues behind whatever is running on it (What the Main Thread Owns).
  • Assume the origin boundary exists and design around it deliberately, rather than discovering it as a console error late in a sprint (CORS).
  • Assume a device roughly an order of magnitude slower than your development machine, on a network with real latency. That is not pessimism; it is the median of the field data for most consumer products (Real User Monitoring).
  • Prefer the platform's own mechanisms — a real button, native form validation, browser caching — before recreating them. They are already correct, already accessible, and already free (Semantics Are Behaviour).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The browser is also an accessibility runtime: it derives an accessibility tree from the DOM and exposes it to screen readers, switch devices, voice control and braille displays (The Accessibility Tree).
  • That tree is computed from semantics you either provide or fail to provide. A div with a click handler produces a node with no role, no name and no keyboard behaviour — the runtime cannot infer intent you did not express.
  • Blocking the main thread degrades assistive technology exactly as it degrades everything else: focus moves late, announcements queue, and a screen-reader user experiences the freeze without any visual cue that the page is busy.
  • Browser defaults — focus rings, scroll behaviour, text selection, zoom, reduced-motion preferences — are accessibility features. Overriding them without replacing them is a regression, not a design choice.

What can go wrong

Failure modes
  • Long synchronous work on the main thread. The page stops responding to input entirely; the browser cannot even produce a frame to show a spinner (Long Tasks).
  • Unbounded memory growth from retained DOM, listeners or caches. It presents as "the app gets slower the longer you use it", which is the hardest symptom to reproduce (Memory Leaks).
  • Assuming a browser API exists. Availability varies by browser, by version, by secure context and sometimes by user setting; a feature that throws on construction takes the whole bundle down with it.
  • Assuming the browser will not throw away your work. Tabs are discarded under memory pressure, service workers are killed between events, and background timers are throttled aggressively.
Security
  • The browser executes untrusted code from arbitrary origins safely, which is the entire reason its security model is as restrictive as it is. Every frustrating limitation is that guarantee's cost.
  • Everything shipped to the browser is public: source, comments, API endpoints, and every value in a bundled config object. There is no such thing as a secret in a frontend bundle, minified or not.
  • The browser enforces some things absolutely (origin isolation, cookie attributes, CSP) and nothing else. Any rule that matters must also be enforced server-side, because the client is fully under the user's control (What the Frontend Is Responsible For in Auth).
  • Third-party script included in your page runs with your page's full authority. There is no partial trust for a plain script tag (Third-Party Scripts and the Supply Chain).
Misreads
  • "The browser just runs my JavaScript." It runs your JavaScript on one thread that it also needs for rendering, and it does far more work between your lines than inside them.
  • "Frameworks abstract the browser away." They abstract the DOM API. Scheduling, layout, paint, memory and the origin boundary are not abstracted by anything, and they are where the hard bugs live.
  • "It is fast on my machine, so it is fast." Your machine is close to the fastest device in your user population and is sitting next to your router.
  • "The browser is a rendering target like a canvas." It is a scheduler, a security boundary, a cache, a storage system and an accessibility runtime that also happens to render.

Measuring it, and what changes in the field

How you would see this
  • The Performance panel is the runtime made visible: a main-thread flame chart, frames, network, and the rendering stages each frame spent time in (A Mental Model of the Devtools).
  • The Memory panel shows what is retained and by what — the only reliable way to distinguish a leak from a cache that is doing its job (Debugging Memory).
  • Field data from real users is the only measurement that reflects real devices and real networks. A local profile is a hypothesis (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a slow device, everything on the main thread costs proportionally more — parse, compile, execute, style, layout. The gap between a fast laptop and a mid-range phone is routinely 5–10x on JavaScript-heavy work.
  • On a slow network, the pipeline stalls waiting for bytes, and which bytes block matters far more than how many there are in total (The Critical Rendering Path).
  • In a long-lived tab, small per-interaction costs compound: retained nodes, growing caches, accumulated listeners. Behaviour after an hour is a different question from behaviour on load (Long-Lived Clients and Version Skew).
What this costs
  • Learning the runtime is slower than learning a framework, and it does not produce a demo on day one. It pays back the first time production does something the framework documentation does not describe — which is every serious performance or correctness bug.
  • Using the platform directly can mean more code than a library call, and libraries exist because some platform APIs are genuinely awkward. The rule is to know what the library is doing, not to refuse libraries.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe runtime properties — one main thread per page, a staged rendering pipeline, origin isolation, a derived accessibility tree — hold across Blink, Gecko and WebKit. They come from the specifications, not from any one implementation.
  • ENGINE-SPECIFICHow the work is divided across processes and threads is an implementation choice: Chromium isolates each site in its own renderer process by default, while Firefox uses a bounded pool of content processes shared across sites, so the memory and crash-isolation characteristics differ even though the programming model does not.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Securitycors
Concurrencyevent-loops
Domains that do not exist yet
  • Programming Languages & Runtime Internals — how a JavaScript engine parses, compiles, optimises and deoptimises the code you ship, and what the garbage collector does between your lines.