When you open a page, the browser does not paint it instantly. Between the first byte of HTML and the first pixel on screen there is a sequence of steps — that is the Critical Rendering Path. If something slows down at any step, the user sees a white screen or a content jump.
Understanding this sequence is not just academic. Most slow initial loads and visual shifts come from one of a small number of well-known causes — and all of them have concrete fixes.
From bytes to pixels
The browser goes through several stages:
HTML → DOM. The browser reads HTML bytes and builds a tree of objects — the DOM (Document Object Model). Each tag becomes a node in the tree.
CSS → CSSOM. In parallel with building the DOM, the browser reads CSS and builds the CSSOM (CSS Object Model). This is also a tree — rules and computed styles for each node.
Render tree. The DOM and CSSOM are combined into the render tree. Only visible elements are included: display: none elements are excluded, while pseudo-elements like ::before are included.
Layout (Reflow). The browser computes geometry — the size and position of each element. This is an expensive operation: changing the width of one block can shift everything around it.
Paint. The browser draws pixels — fills color, applies shadows, outlines. Each layer is painted separately.
Composite. The finished layers are sent to the GPU, which combines them into the final image on screen.
What blocks the first render
CSS blocks rendering
The browser cannot build the render tree until all CSS is loaded. This is why <link rel="stylesheet"> is called a "render-blocking" resource. Until the stylesheet is loaded, the user sees a white screen.
What to do: load only the critical CSS for the above-the-fold content; non-critical styles — asynchronously or with a media="print" attribute that is later changed to all.
Synchronous <script> blocks parsing
When the browser encounters a <script> with no attributes, it stops building the DOM — it must execute the script because the script might modify the HTML. A slow script means a long wait before the first render.
<!-- blocking: browser waits for download and execution -->
<script src="analytics.js"></script>
<!-- defer: downloads in parallel, executes after HTML is parsed -->
<script src="app.js" defer></script>
<!-- async: downloads in parallel, executes as soon as it is loaded -->
<script src="counter.js" async></script>
<!-- type="module": behaves like defer, plus ESM support -->
<script src="main.js" type="module"></script>
The rule for most scripts: use defer. For independent analytics scripts — async. type="module" — when you need ES modules.
Fonts and FOUT/FOIT
The browser does not show text until the web font is loaded. This leads to two problems:
- FOIT (Flash of Invisible Text) — text is hidden until the font loads.
- FOUT (Flash of Unstyled Text) — text first appears in a system font, then "jumps" to the loaded font.
The font-display property controls this:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* show system font immediately, replace when loaded */
}
font-display: swap is a good choice: the user sees text right away, and a small jump is acceptable. optional — the browser uses the font only if it loaded within the first 100 ms; no jump, but no guarantee of the custom font.
Reflow, repaint, and composite: what costs what
Not all style changes are equally expensive:
| What changes | Trigger | Cost |
|---|---|---|
width, height, margin, top | Layout → Paint → Composite | Expensive |
color, background, box-shadow | Paint → Composite | Moderate |
transform, opacity | Composite only | Cheap |
When the browser does a reflow, it recalculates the geometry of the element and everything affected by it. Changing width once can trigger recalculation for half the page. transform: translateX() moves the element on the GPU only, without touching geometry — it is nearly free.
The same rule applies to animations: animating transform and opacity is fine; animating top or width is not. For a detailed look at building animations correctly, see Animations: CSS and optimizations.
Layout thrashing
Layout thrashing happens when JavaScript alternates between reading and writing geometric properties in a loop. The browser is forced to do a reflow on every iteration.
// bad: reflow on every iteration
const items = document.querySelectorAll('.item');
items.forEach(item => {
const height = item.offsetHeight; // read — browser recalculates geometry
item.style.height = height * 1.5 + 'px'; // write — invalidates geometry cache
});
// good: all reads first, then all writes
const heights = Array.from(items).map(item => item.offsetHeight);
items.forEach((item, i) => {
item.style.height = heights[i] * 1.5 + 'px';
});
The rule: collect all geometric data first, then apply all style changes. Reading offsetHeight, getBoundingClientRect(), or scrollTop after writing a style always causes a reflow.
How to measure
DevTools → Performance tab. Record a page load or a specific interaction. The panel shows the full pipeline: Loading, Scripting, Rendering, Painting — you can see where time is spent and what triggered a reflow.
LCP and CLS metrics. LCP (Largest Contentful Paint) — when the main visible element appeared. CLS (Cumulative Layout Shift) — how much content shifted during load. Both are measured in DevTools → Lighthouse. For a detailed look at React app performance metrics, see Rendering performance.
Practical checklist for a fast first render
<script>withoutdefer/asyncis moved to the end or replaced withdefer.- Critical CSS is inlined or loaded first; non-critical CSS is loaded asynchronously.
- Fonts use
font-display: swaporoptional. - Heavy images have the
loading="lazy"attribute. - Animations use
transform/opacity, nottop/left. - No layout thrashing: geometry reads are not interleaved with style writes.
In short
- The browser builds the DOM from HTML, CSSOM from CSS, combines them into the render tree, computes geometry (layout), draws layers (paint), and composites on the GPU (composite).
- CSS blocks rendering — the page will not appear until all CSS is loaded.
<script>without attributes blocks HTML parsing;defer— downloads in parallel, executes after;async— executes immediately after download.font-display: swapeliminates FOIT at the cost of a small FOUT;optional— no jump, no font guarantee.transformandopacityare cheap — composite only;width/height/topare expensive — they trigger reflow.- Layout thrashing: reading geometry after writing styles in a loop causes a reflow on every iteration. Fix: all reads, then all writes.
- LCP and CLS are the key metrics for first render; measured in DevTools Performance and Lighthouse.
What to read next
- Rendering performance — React component optimization, memo, and code-splitting.
- Animations: CSS and optimizations — how to animate along the composite path without reflow.
- Event loop and async — how JavaScript processes tasks and why this affects rendering.