Inside your browser

How web pages actually get rendered

1,629 words9 min read

Your web browser is one of the most complex pieces of software on your computer. It's a networking client, a rendering engine, a JavaScript runtime, a media player, a security sandbox, a database, and much more. When you load a webpage, your browser orchestrates an incredibly sophisticated pipeline to transform HTML, CSS, and JavaScript into the pixels you see on screen - and it does this in milliseconds.

Modern browsers are the product of decades of engineering. Chrome's codebase exceeds 35 million lines of code. They implement hundreds of web standards, maintain compatibility with millions of websites, and run untrusted code from the internet while keeping your computer secure. Let's follow a URL from the moment you hit Enter to the moment the page appears.

The network request

Before any rendering happens, the browser needs to fetch the page. If you type 'example.com', the browser first checks its cache - have we fetched this resource recently? If not, it performs a DNS lookup to translate the domain name to an IP address. This lookup itself might be cached at multiple levels: browser cache, OS cache, router cache, ISP resolver cache.

Then comes connection establishment. For HTTPS (which is most of the web), this means a TCP handshake (SYN, SYN-ACK, ACK) followed by a TLS handshake (key exchange, certificate verification, cipher negotiation). Only after all this can the browser send its HTTP request. Modern protocols like HTTP/2 and HTTP/3 (QUIC) reduce this overhead through connection reuse and multiplexing.

The server responds with HTML, which the browser starts processing immediately - it doesn't wait for the entire file to download. This [[streaming]] approach is crucial for performance. As HTML arrives, the browser can start parsing and even fetching additional resources like CSS, JavaScript, and images. This is why script and stylesheet placement matters so much.

The critical rendering path

The [[critical rendering path]] is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into visible pixels. Understanding and optimizing this path is the key to fast-loading websites. Every step takes time, and several steps must happen sequentially - optimizing the critical path means minimizing what's on it.

Browser Rendering Pipeline

HTML Parsing
DOM Tree
CSS Parsing
CSSOM
Render Tree
Visible Nodes
Layout
Geometry
Paint
Pixels
Composite
Layers
Current Stage

Click "Start Render" to see the pipeline

Output
// Waiting...
Performance Insight
Changing styles triggers different amounts of work:Layout changes → most expensive,Paint only → medium,Composite only → cheapest (use transforms!)
The browser rendering pipeline - watch each stage transform your code into pixels

Step 1: HTML parsing and DOM construction

The browser parses the HTML document and builds the [[DOM]] (Document Object Model) - a tree structure representing every element on the page. Each HTML tag becomes a node in this tree: <html> is the root, <body> is its child, paragraphs and divs are descendants. The DOM is a live data structure that JavaScript can query and modify.

HTML parsing is surprisingly complex. The HTML specification includes detailed algorithms for handling malformed markup - browsers don't just reject invalid HTML, they repair it. They handle missing closing tags, incorrectly nested elements, and all sorts of errors that would break a strict parser. This fault tolerance is why the web is so resilient (and sometimes maddening to debug).

<!-- This HTML... -->
<html>
  <body>
    <div class="container">
      <h1>Hello World</h1>
      <p>Welcome to my site</p>
    </div>
  </body>
</html>

<!-- ...becomes a DOM tree:
document
  └── html
      └── body
          └── div.container
              ├── h1 -> "Hello World"
              └── p  -> "Welcome to my site"
-->

Step 2: CSS parsing and CSSOM construction

Simultaneously, the browser parses CSS and builds the [[CSSOM]] (CSS Object Model). This tree structure contains all the styling rules and how they cascade together. Unlike HTML, CSS parsing is strict - syntax errors cause rules to be skipped. The CSSOM is [[render-blocking]] - the browser won't render anything until it has processed all CSS, because rendering with incomplete styles would cause jarring flashes of unstyled content.

CSS cascade resolution is computationally expensive. For each element, the browser must determine which rules apply (based on selectors), calculate specificity, apply inheritance, resolve conflicts, and compute final values. Optimizations like selector hashing make this fast enough, but complex selectors and deep DOM trees can still hurt performance.

Step 3: Render tree construction

The browser combines DOM and CSSOM into the [[render tree]]. This tree contains only visible elements - elements with 'display: none' are excluded entirely (they don't even get laid out). Pseudo-elements like ::before and ::after are added here. Each node in the render tree contains the element and its computed styles (the final CSS values after cascading, inheritance, and specificity are resolved).

Step 4: Layout (reflow)

With the render tree built, the browser calculates the exact position and size of every element. This process is called [[layout]] or 'reflow.' Starting from the root, the browser walks the tree calculating geometry: 'This div is 500px wide, this paragraph wraps to 3 lines at that width, this image needs space for 200x150px, this flex container distributes remaining space among children.'

Layout is recursive and often requires multiple passes. Percentage widths depend on parent widths. Auto margins depend on available space. Flex and grid layouts involve complex algorithms for distributing space. The browser must handle intrinsic sizing, min/max constraints, and content that overflows its container. Modern CSS layout (flexbox, grid, container queries) is incredibly powerful but adds computational complexity.

Step 5: Paint

[[Paint]] is where the browser fills in actual pixels - text glyphs, colors, images, borders, shadows, gradients. This happens in multiple passes: backgrounds, then borders, then text, then outline. Complex effects like filters, blend modes, and masks add additional passes. Modern browsers often paint to multiple [[layers]], which can be composited separately.

Paint operations are recorded as display lists - sequences of drawing commands. These lists can be cached and replayed for regions that haven't changed. Only 'dirty' regions (where something changed) need to be repainted. This invalidation tracking is crucial for performance - full repaints are expensive.

Step 6: Compositing

Finally, [[compositing]] combines all the painted layers in the correct order and sends the result to the GPU for display. Layers are composited based on stacking context, z-index, and other factors. Elements with 'transform,' 'opacity,' 'will-change,' or certain CSS filters get their own compositing layers, which can be animated efficiently by the GPU without triggering layout or paint.

This is why 'transform: translateX(100px)' is faster than 'left: 100px' for animation. The transform only affects compositing - the element's layer just moves. Changing 'left' triggers layout (recalculating all affected element positions) and paint (redrawing the affected region), which is orders of magnitude slower.

JavaScript: the wildcard

JavaScript complicates everything. By default, when the browser encounters a <script> tag, it stops parsing HTML, downloads the script, and executes it. Why such disruptive behavior? Because JavaScript might call document.write() and inject HTML into the document the browser is still parsing, completely changing what comes next.

This is why you often see scripts at the end of <body> or with the defer attribute - to avoid blocking the parser. The [[async]] attribute downloads scripts without blocking but executes them immediately when ready (potentially out of order). The [[defer]] attribute downloads without blocking and executes in order after HTML parsing completes. For modules, defer is the default behavior.

<!-- Blocks parsing until downloaded and executed (bad for performance) -->
<script src="app.js"></script>

<!-- Downloads async, executes immediately when ready (good for analytics) -->
<script async src="analytics.js"></script>

<!-- Downloads async, executes in order after HTML parsed (best for app code) -->
<script defer src="app.js"></script>

<!-- ES modules are deferred by default -->
<script type="module" src="app.mjs"></script>

JavaScript execution can also trigger reflows and repaints. Reading layout properties forces a synchronous layout calculation. Modifying DOM or styles can invalidate previous layout work. Interleaving reads and writes (the 'forced synchronous layout' anti-pattern) causes repeated layout thrashing. Good performance requires batching DOM operations.

The event loop and rendering

JavaScript runs in a single-threaded event loop. Tasks (macrotasks) like script execution and event handlers run to completion - once started, they can't be interrupted by other JavaScript. Microtasks (Promises, MutationObserver) run at the end of each task. Rendering updates happen between tasks, typically at 60fps (every ~16.67ms).

If JavaScript takes too long (more than ~50ms), the page feels unresponsive - you'll notice laggy scrolling, delayed clicks, and janky animations. Long tasks block rendering updates. This is why heavy computation should be chunked using requestIdleCallback, moved to Web Workers, or offloaded to the server. The browser can't update the screen while JavaScript is running on the main thread.

The browser as an OS

Modern browsers are essentially operating systems. Chrome, for example, runs each tab in a separate process for security and stability (one tab crashing doesn't take down others). It has a [[JavaScript engine]] (V8) that compiles JavaScript to optimized machine code through multiple compilation tiers. It has a [[rendering engine]] (Blink) that handles layout and paint. It has a network stack, a storage system, a GPU process, and extension runtime.

Security is paramount. Websites run in sandboxes that restrict what they can access. The same-origin policy prevents cross-site data access. Content Security Policy restricts which resources can load. CORS controls cross-origin requests. Feature Policy controls access to sensitive APIs. Every new web API is designed with security considerations - browsers are constantly under attack, and a single vulnerability can affect billions of users.

This complexity is why browsers are among the most sophisticated software ever built - and why web performance is such a deep topic. Every millisecond of the critical rendering path has been optimized by thousands of engineers over decades. Understanding this pipeline makes you a better web developer, able to diagnose performance issues and write code that works with the browser rather than against it.

How Things Work - A Visual Guide to Technology