How Browsers Render Web Pages

Photo: Mathews Jumba / Pexels
Typing a URL and seeing a finished page feels instant, but the browser is running a strict, multi-stage pipeline behind that instant: parsing HTML into a tree, parsing CSS into a second tree, merging them, calculating every element's geometry, painting pixels, and compositing layers on the GPU. Chrome's own DevRel documentation calls this sequence the critical rendering path — the minimum set of steps the browser must complete before it can show anything at all (web.dev, 2024). Each stage depends on the one before it, which is why a single blocking script or a late-loading stylesheet can stall the entire chain and leave a visitor staring at a blank tab.
This matters more than it sounds like it should. A rendering delay of even a few hundred milliseconds measurably increases the chance a visitor leaves before the page finishes loading, which is why Core Web Vitals metrics like Largest Contentful Paint exist as a proxy for "did the pipeline finish fast enough to matter." Understanding the pipeline is the difference between guessing at performance fixes and knowing exactly which stage you're fighting.
What the browser actually receives first
Before any of the tree-building begins, the browser has to get bytes off the network. A request goes out, a response comes back as a stream of raw bytes, and the browser's HTML parser starts converting those bytes into characters, then into tokens, then into nodes, and finally into the tree structure known as the Document Object Model, or DOM — a live, in-memory representation of every element on the page (MDN, Critical rendering path documentation). This process is incremental: the browser doesn't wait for the entire HTML file to download before it starts building the DOM. It processes what has arrived and continues as more bytes stream in, which is one reason a server that responds with the first byte of HTML quickly (a metric called Time to First Byte) gives the whole rendering pipeline a head start.
Parallel to the DOM, the browser tracks any external resources referenced in the markup, stylesheets, scripts, fonts, images, and starts requesting them as soon as their tags are discovered, often before the rest of the HTML has even finished parsing. This is why browsers use a preload scanner, a secondary, faster parser that scans ahead in the raw HTML specifically to find resource references early, so downloads can start in the background while the main parser continues building the DOM node by node.
None of this is rendering yet. It's the raw material. The DOM by itself has no visual information at all, no colors, no sizes, no positions. A <div> node in the DOM doesn't know if it's red or blue, ten pixels wide or a thousand. That information comes from a second tree the browser has to build in parallel, and until both trees exist, nothing appears on screen.
Building the DOM and CSSOM
As HTML arrives, each tag becomes a node with parent-child relationships that mirror the nesting in the markup. A <div> containing a <p> becomes a parent node with a child node, and the browser keeps building this structure until it hits the closing </html> tag or runs out of bytes to parse. Malformed HTML, an unclosed tag, a stray character, doesn't stop this process the way a syntax error would in a programming language; browsers are built with error-tolerant parsing rules that silently correct or ignore invalid markup so the page still renders, which is part of why "view source" on a real website often reveals surprisingly messy HTML that still displays fine.
CSS goes through a parallel process, producing a separate tree called the CSS Object Model, or CSSOM, that captures every style rule and every element it applies to, including inherited and computed values. Unlike HTML parsing, CSS parsing is render-blocking by design. Chrome's engineering documentation is explicit about why: a rule at the bottom of a stylesheet can still override a rule the browser already parsed near the top, so painting anything before the full CSSOM exists would risk showing styles that are about to be invalidated, forcing the browser to redraw the whole page a moment later (web.dev, Render-tree Construction, Layout, and Paint). Rather than risk that flash of wrong styling, the browser simply refuses to paint anything until the entire CSSOM is built, which means every stylesheet in the <head> is, by default, standing directly between the user and a visible page.
This is also why the placement of <link rel="stylesheet"> tags matters so much in practice. A stylesheet referenced late in the document, after most of the visible content, still blocks the same way a stylesheet in the <head> does, because the browser has no way of knowing whether more style rules are coming until it reaches the end of that file. Site owners who inline critical, above-the-fold CSS directly in the <head> and defer the rest are exploiting exactly this mechanic: give the browser the minimum CSSOM it needs to paint something correct immediately, then load the rest without blocking.
| Pipeline Stage | Input | Output | What Happens |
|---|---|---|---|
| HTML Parsing | Raw bytes from network | DOM tree | Bytes → characters → tokens → nodes → tree, built incrementally as HTML streams in |
| CSS Parsing | Stylesheet bytes | CSSOM tree | Full stylesheet parsed into a style tree; blocks painting until complete |
| Render Tree Construction | DOM + CSSOM | Render tree | Merges both trees, discards nodes that won't display (display: none, <head> elements) |
| Layout (Reflow) | Render tree | Box geometry | Calculates exact pixel size and position of every visible box |
| Paint | Box geometry | Pixel data | Fills in text, color, borders, shadows, images as raster pixels |
| Composite | Painted layers | Final frame | Combines separately painted GPU layers into the frame shown on screen |
Six dependent stages turn a stream of bytes into a picture on screen — and a delay at any one of them delays every stage after it.
From trees to pixels: render tree, layout, and paint
Once both trees exist, the browser combines them into a render tree, which contains only the nodes that will actually be visible. Elements set to display: none are excluded entirely, along with structural elements like <head> and its children, which never render visually regardless of what they contain. Note that this is different from visibility: hidden, which does occupy layout space even though it isn't drawn, a distinction that trips up a lot of developers debugging why a "hidden" element still affects surrounding spacing.
For every node in the render tree, the browser then runs layout, sometimes still called reflow in older documentation, calculating the exact pixel position and size of every box on the page. This calculation depends on the viewport size, the box model (content, padding, border, margin), and every layout-relevant CSS property, width, height, flexbox and grid rules, font size, line height. Layout is where percentage-based widths get resolved to actual pixel values, where flex items get distributed along a main axis, and where text gets wrapped into lines based on the available inline space. It is, computationally, one of the more expensive stages in the pipeline, because a single change near the root of the tree, resizing a parent container, for example, can cascade into recalculating the position of every descendant node.
The final stage, paint, fills in the actual pixels: text glyphs, background colors, borders, shadows, and images, converting the geometric boxes from layout into a rasterized image. Modern browsers frequently split the page into separate layers for this step, giving elements like fixed headers, video, or anything with a CSS transform or will-change property their own paint layer on the GPU. The final composite stage then combines those layers into the single frame the user actually sees, and because compositing happens on the GPU independently of the main thread, a browser can often re-composite a layer, sliding a fixed header, for instance, without re-running layout or paint at all. That distinction, a full layout-paint-composite cycle versus a composite-only update, is the single biggest lever for animation performance on the web.
Why layer promotion matters for animation
Properties like transform and opacity can be animated purely through re-compositing existing GPU layers, skipping layout and paint entirely on every frame. Properties like width, top, or margin force a full layout recalculation each frame because they change the geometry other elements depend on, which is why the former animate smoothly even on weaker devices and the latter visibly stutter under load.
Why JavaScript can freeze this whole pipeline
By default, a <script> tag without async or defer attributes is render-blocking and parser-blocking: the browser stops building the DOM entirely, downloads the script if it's external, executes it, and only then resumes parsing the rest of the HTML. Web.dev's resource-loading documentation identifies this as the default because a synchronous script might call document.write() or otherwise modify the DOM in ways that would invalidate everything already parsed, so the browser can't safely continue past it without waiting to see what the script does first.
This default made sense when scripts were small, occasional enhancements to mostly-static pages. It makes considerably less sense on a modern site loading a framework bundle, an analytics tag, and three third-party widgets, all as blocking scripts in the <head>, which is exactly the pattern that produces a page that sits blank for a second or more despite a fast network connection.
Two attributes fix this without changing what the script does, just when it runs relative to parsing:
asyncdownloads the script in the background without blocking HTML parsing, then executes it the moment the download finishes, pausing parsing only for that execution. Scripts withasyncrun in whatever order they finish downloading, not the order they appear in the document, so they're appropriate for independent scripts like analytics that don't depend on the DOM being complete or on other scripts.deferalso downloads in the background without blocking parsing, but delays execution until HTML parsing is completely finished, running scripts in document order, immediately before theDOMContentLoadedevent fires. This preserves execution order and guarantees the full DOM exists before the script runs, which is why it's the standard choice for framework and application bundles that need to attach to elements already in the document.
Comparisons from browser performance tooling vendors consistently frame defer as the safer default for most application code, because it guarantees ordering and a complete DOM without sacrificing the parsing speed gained by not blocking (DebugBear, Async vs. Defer). async remains the right call specifically for scripts that are self-contained and order-independent.
Does script placement in the document still matter?
Less than it used to, but yes for legacy blocking scripts. A blocking script placed just before </body> lets the browser parse the entire visible DOM first, so the page's content appears before the script executes, but async and defer make placement mostly moot since neither blocks parsing regardless of where the tag sits in the document.
What actually delays a page appearing
Four categories of delay account for most of the gap between "network request sent" and "user sees content." None of them are mysterious once you know which pipeline stage they're stalling.
- Render-blocking CSS in the
<head>must fully download and parse before any paint occurs, so a large, unminified, or unnecessarily comprehensive stylesheet directly delays first paint, even if the visible content only needs a fraction of the rules it contains. - Synchronous scripts without
asyncordeferpause HTML parsing entirely until they finish downloading and executing, which is why a slow third-party script tag in the<head>can stall an otherwise fast page. - Web fonts can produce a flash of invisible text (FOIT) if the browser withholds text rendering until the custom font file finishes downloading, a behavior controlled by the CSS
font-displayproperty, where a value likeswaptells the browser to paint text in a fallback font immediately and swap in the custom font once it arrives. - Layout thrashing, where JavaScript reads a layout property (like
offsetHeight) and then writes a style change in a tight loop, forces the browser to synchronously recalculate layout on every iteration instead of batching the work, turning what should be one layout pass into dozens or hundreds.
Each of these maps to a specific, addressable fix: minimize and split critical CSS, add defer to non-critical scripts, set font-display: swap on @font-face rules, and batch DOM reads before DOM writes in JavaScript. None of them require exotic tooling, only knowing which pipeline stage is actually the bottleneck for a given page.
Common misunderstandings about rendering
A common assumption is that a browser renders a page in one pass, top to bottom, the way a printer lays ink on paper. In reality, rendering is a repeated cycle. Any DOM or CSSOM change, a hover effect, a class toggle from JavaScript, a window resize, can trigger a partial or full re-run of layout and paint. This is why performance advice so often centers on minimizing the number and cost of these re-runs rather than treating the initial render as the only thing that matters. A page can render fast on first load and still feel sluggish if every scroll or interaction re-triggers an expensive layout pass.
Another misconception is that faster JavaScript execution always translates into a faster-feeling page. In practice, the layout and paint stages, not raw script execution time, are frequently the actual bottleneck for perceived speed on content-heavy pages. A script that finishes computing in two milliseconds but then triggers a full-page layout recalculation can cost far more wall-clock time than a slightly slower script that avoids touching layout-relevant properties. This is part of why minimizing render-blocking resources and avoiding layout thrashing matters as much as, and sometimes more than, optimizing algorithmic efficiency in application code.
A third misunderstanding worth naming directly: people often assume "the page loaded" is a single, well-defined moment. Browsers actually expose several distinct milestones, DOMContentLoaded (HTML fully parsed, DOM ready, but stylesheets and images may still be loading), the load event (all resources including images and stylesheets finished), and paint-timing metrics like First Contentful Paint and Largest Contentful Paint (when meaningful content actually became visible, independent of whether every resource has finished). These milestones can be seconds apart on a real page, and conflating them is a common source of confusion when developers debug why a page "loaded" according to one metric but still felt slow to an actual visitor.
Does a slow server always cause a slow render?
Not necessarily. A fast server response only guarantees a fast start to the pipeline; if the response includes large render-blocking stylesheets, synchronous scripts, or unoptimized web fonts, the browser can still spend a long time in the CSSOM, layout, or paint stages regardless of how quickly the first byte arrived.
Why do some pages flash unstyled content briefly?
This is a Flash of Unstyled Content (FOUC), and it typically happens when the browser paints HTML before the CSSOM is ready, which is rare given CSS is render-blocking by default, but can occur with certain script-injected stylesheets or misconfigured critical-CSS setups that assume styles are present before they actually finish loading.
What this means for real-world performance work
Knowing the pipeline changes how you diagnose a slow page. Instead of guessing at "make it faster," you can use browser DevTools' Performance panel to see exactly which stage is consuming time, whether it's a long "Recalculate Style" block (CSSOM/render tree), a long "Layout" block, or scripting time that's blocking the main thread before parsing can resume. Each of those maps directly back to one of the pipeline stages covered above, which turns performance debugging from guesswork into a targeted fix.
It also explains why some performance advice that sounds contradictory actually isn't. "Inline your critical CSS" and "minimize render-blocking resources" are the same underlying principle applied differently: give the browser the smallest possible CSSOM it needs to paint correctly right away, and load everything else without making the user wait for it.
The takeaway
A browser doesn't display a web page so much as construct it in careful, dependent stages: parse markup into a DOM, parse styles into a CSSOM, merge them into a render tree, calculate every element's geometry in layout, paint the pixels, and composite the final frame. Understanding where each stage can stall, a blocking script here, an unoptimized stylesheet there, a layout-thrashing script loop somewhere else, is what separates a page that feels instant from one that leaves visitors watching a blank tab.
Read next:
- How CDN Caching Speeds Up Websites
- What Server-Side Rendering Solves
- Why Web Performance Affects SEO
FAQ
Q: Why does CSS block rendering but HTML doesn't?
CSS rules can override earlier rules anywhere in a stylesheet, so the browser must finish parsing the entire CSSOM before it can safely paint anything, whereas HTML parsing can proceed incrementally as more markup arrives because each node can be added to the DOM without invalidating nodes already parsed.
Q: What is the difference between the DOM and the render tree?
The DOM contains every HTML node, including hidden ones and structural elements like <head>, while the render tree contains only the nodes that will actually be visible on screen, combining DOM structure with computed CSS styles from the CSSOM and skipping anything set to display: none.
Q: Why do developers add "defer" to script tags?
Without defer, a script blocks HTML parsing until it downloads and executes. Adding defer lets the browser keep parsing HTML in the background, download the script in parallel, and run it only after parsing completes and in document order, which speeds up how quickly the page becomes visible without changing what the script does.
Q: What's the difference between async and defer?
async scripts execute as soon as they finish downloading, in whatever order that happens to be, which can interrupt parsing at an unpredictable moment. defer scripts always wait until HTML parsing is fully complete and always execute in the order they appear in the document, making defer the safer default for application code that depends on the DOM or on script execution order.
Q: Does animating CSS always cause a layout recalculation?
No. Properties like transform and opacity can be animated by the GPU compositor without triggering layout or paint at all, while properties like width, top, or margin force the browser to recalculate layout on every frame, making them significantly more expensive to animate, especially on lower-powered devices.
Q: What causes a Flash of Invisible Text (FOIT)?
FOIT happens when a browser withholds rendering of text until a custom web font finishes downloading, leaving the text area blank in the meantime. Setting font-display: swap on the relevant @font-face rule tells the browser to paint text in a fallback font immediately and swap in the custom font once it's available, avoiding the invisible-text period entirely.
Q: Is DOMContentLoaded the same as the page finishing loading?
No. DOMContentLoaded fires once HTML parsing is complete and the DOM is ready, but stylesheets, images, and other subresources may still be loading. The load event fires later, once every resource on the page has finished, and paint-timing metrics like Largest Contentful Paint measure yet another, separate moment: when the biggest visible content element actually became visible to the user.
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.
Related Articles


Tailwind vs Bootstrap vs shadcn/ui — Styling Stack 2026
Real comparison of Tailwind CSS, Bootstrap, and shadcn/ui in 2026 — when each one wins, bundle size, customization, and the right pick for your project.

How Graphics Cards Render 3D Games: Rasterization vs Ray Tracing
Your GPU processes billions of calculations per second to paint every frame you see. Understanding how graphics cards actually work — from CUDA cores to memory bandwidth — reveals the engineering behind modern game visuals.


How Progressive Web Apps Work
How progressive web apps work: the web app manifest, service worker lifecycle, caching strategies, install rules, and where iOS and Android still differ.
