How a browser turns a URL into a first paint
A modern browser splits its work across separate processes. A browser process runs the interface and coordinates everything, a network service fetches resources, and sandboxed renderer processes turn HTML, CSS and JavaScript into pixels.
Inside a renderer, the rendering engine parses HTML into the DOM and CSS into the CSSOM, then computes layout and paints. The first paint waits for render-blocking stylesheets and for any scripts that pause the parser, so those decide how soon a page appears. What each later frame costs is covered in the article on the browser rendering lifecycle.
The processes inside a modern browser
Chromium-based browsers such as Chrome and Edge, Firefox and Safari all use several processes. The names and details differ, but the roles are similar:
- The browser process draws the address bar, tabs and menus, handles navigation, and controls access to files and other system resources.
- Renderer processes (content processes in Firefox, web content processes in Safari) run the page itself: parsing, styling, layout, painting and JavaScript.
- The network service handles DNS, connections, the HTTP cache and cookies for the renderers. In Chrome and Safari it usually runs in a process of its own.
- A GPU process talks to the graphics hardware and puts finished frames on screen.
Sandboxing and site isolation
Renderers run in a sandbox with little direct access to the operating system. To read a file or open a connection, a renderer has to ask another process, which can refuse. An attacker who compromises a renderer through a browser bug still needs a second bug to escape the sandbox.
Site isolation goes further by putting pages from different sites in different renderer processes, including cross-site iframes. Desktop Chrome and desktop Firefox enable it by default.
A site here means the scheme plus the registrable domain. So https://shop.example.com and https://example.com are the same site, while https://example.org is a different one.
Process boundaries limit what a compromised renderer can reach. They also reduce exposure to side-channel attacks such as Spectre, which can let a page read memory from another site in the same process. The cost is memory, so browsers may let same-site pages share a renderer.
The rendering engine and the JavaScript engine
Each renderer contains two engines. The rendering engine parses HTML and CSS, builds the DOM, computes styles and layout, and paints: Blink in Chrome and Edge, Gecko in Firefox, WebKit in Safari. The JavaScript engine compiles and runs scripts: V8 in Chrome and Edge, SpiderMonkey in Firefox, JavaScriptCore in Safari.
Both engines share the renderer's main thread. While a script runs, parsing and rendering wait, so long-running JavaScript delays the first paint and every later frame.
V8 and SpiderMonkey are sometimes described as the engine that renders the page. They run JavaScript and WebAssembly; HTML parsing and rendering belong to Blink, Gecko or WebKit.
From HTML bytes to first paint
The critical rendering path is the sequence of steps between receiving HTML and drawing the first pixels:
- The parser decodes the bytes into characters, splits them into tokens and builds the DOM, a tree of nodes. It works incrementally, so the DOM grows while the HTML is still downloading.
- A preload scanner reads ahead in the raw HTML and starts fetching stylesheets, scripts and images early, even while the main parser is paused.
- Stylesheets are fetched and parsed into the CSSOM, the object model of style rules the browser matches against the DOM.
- Style calculation combines the DOM and CSSOM into a render tree (also called the layout tree) of visible elements. Elements with display: none are left out, while visibility: hidden elements stay because they still take up space.
- Layout computes the size and position of each box, paint records the drawing, and compositing puts the result on screen.
Render-blocking CSS and parser-blocking scripts
Stylesheets in the head are render-blocking. The browser will not paint until they have loaded and been parsed, which prevents a flash of unstyled content.
Keep the CSS needed for the first view small, and avoid @import, which makes the browser discover and fetch stylesheets one after another. A stylesheet whose media attribute does not match, such as media="print" on a screen, does not block rendering.
A classic script element without attributes is parser-blocking. The parser stops while the browser fetches and runs the script, and the script itself waits for earlier stylesheets to load. Script attributes change this:
- With defer, the script downloads in parallel and runs after parsing finishes, in document order, before the DOMContentLoaded event.
- With async, the script downloads in parallel and runs as soon as it arrives, pausing the parser briefly. Async scripts can run in any order, so use async for independent scripts such as analytics.
- Module scripts (type="module") are deferred by default.
<head>
<!-- Render-blocking: needed for the first view -->
<link rel="stylesheet" href="/css/main.css">
<!-- Does not block rendering on screens -->
<link rel="stylesheet" href="/css/print.css" media="print">
<!-- Runs after parsing, in document order -->
<script src="/js/app.js" defer></script>
<!-- Runs whenever it arrives -->
<script src="https://analytics.example.com/tag.js" async></script>
</head>