Page loading performance is decided by a handful of things you can actually change: render-blocking stylesheets, images that arrive late or arrive far too large, fonts that hold text hostage, and long pages that ask the browser to lay out everything before anything is visible.
What happens between receiving a page and seeing it?
The browser parses the HTML into a document, parses the CSS into a rule tree, combines the two into a tree of boxes, calculates where each box goes, paints the pixels, and then composites them onto the screen. Layout and paint cannot begin for any part of the page until the CSS that applies to it has been parsed, because the browser cannot know the box sizes without the rules.
That single dependency is why a stylesheet in the head of the document is render-blocking. The browser will not show the page until it has the CSS, even though the HTML alone would be perfectly readable. Images and fonts have a different relationship: they do not block the first paint by default, but they do cause the layout to change when they arrive, which is visible to the reader as content jumping around.
How do you stop the page waiting on its stylesheet?
Two approaches work, and they suit different situations. The first is to make the stylesheet small enough that waiting for it costs very little: one file, minified, cached, with the rules the first screen needs included and the rest loaded afterwards.
The second is to inline the handful of rules that affect the first screen directly in the head, and load the complete stylesheet without blocking the render. The common pattern preloads the file and swaps it to a stylesheet once it arrives, but it depends on script running, so the page needs a sensible no-script fallback and a small amount of critical rules inlined as well.
<head>
<style>
/* the rules for the first screen only */
body { margin: 0; font-family: system-ui, sans-serif; }
.recipe-header { padding: 2rem 1rem; }
</style>
<link rel="preload" href="/styles/recipe.css" as="style">
<link rel="stylesheet" href="/styles/recipe.css">
</head>
Keeping the ordinary stylesheet link as well means the page still works when the preload approach is unavailable, and it keeps the first paint honest. The point of the exercise is that the browser gets the critical rules without waiting for a network round trip.
What should you do about images?
Images are usually the heaviest thing on a page, and they cause two separate problems: weight and shifting. Weight is solved by serving a file that is no larger than the space it occupies, and by serving a smaller file to a smaller screen. Shifting is solved by telling the browser the dimensions before the file arrives, so it can reserve the right amount of space.
<img
src="sourdough-800.jpg"
srcset="sourdough-400.jpg 400w, sourdough-800.jpg 800w, sourdough-1600.jpg 1600w"
sizes="(min-width: 48rem) 48rem, 100vw"
width="800"
height="533"
alt="A round sourdough loaf with a scored top on a wooden board"
decoding="async">
The width and height attributes are not there to fix the rendered size. They give the browser the intrinsic aspect ratio, so it can calculate the space the image will occupy before the file arrives, which is what stops the text below it from jumping. The srcset and sizes pair let the browser pick the file that matches the space the image will occupy on that screen. decoding="async" asks the browser to decode the image off the main thread.
For images below the fold, add loading="lazy" and the browser will not request them until they approach the viewport. Do not add it to the main image of the page, which is exactly the image you want immediately.
<img src="step-04.jpg" width="1200" height="800" loading="lazy" decoding="async"
alt="Dough folded over itself on a floured surface">
How do you stop web fonts from blocking text?
A font that has not arrived leaves the browser with two choices: show nothing until it loads, or show the fallback and swap when the real font is ready. The font-display descriptor chooses between them, and swap is the practical default because it never hides text.
@font-face {
font-family: "Recipe Sans";
src: url("/fonts/recipe-sans.woff2") format("woff2");
font-display: swap;
size-adjust: 102%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
The override descriptors let you describe a fallback font so it occupies almost the same space as the real one, which is what turns a visible reflow into an almost invisible one. You can also preload the font file so the request starts before the CSS that needs it has finished parsing.
<link rel="preload" href="/fonts/recipe-sans.woff2" as="font" type="font/woff2" crossorigin>
The crossorigin attribute is required because font files are fetched in anonymous mode, and a preload without it is fetched twice: once as a preload and again as the font request.
How do you speed up a very long page?
A page with dozens of sections asks the browser to lay out and paint all of it, even the parts nobody is looking at. The content-visibility property lets the browser skip that work for content outside the viewport, and contain-intrinsic-size tells it how much room to reserve so the scrollbar does not jump as you scroll.
<ol class="method">
<li class="method-step">
<h3>Mix the dough</h3>
<p>Combine the flour, the water and the starter, then leave it covered for an hour.</p>
</li>
<li class="method-step">
<h3>Fold and rest</h3>
<p>Fold the dough over itself four times, resting between each fold.</p>
</li>
</ol>
.method-step {
content-visibility: auto;
contain-intrinsic-size: auto 12rem;
}
Applying it to each step of a recipe is effective because the steps are self-contained blocks that do not need to be laid out until they are close to the screen. Applying it to the whole page does nothing useful, because the top of the page is what is visible.
What should you look at when a page feels slow?
| Symptom | Likely cause | What to change |
|---|---|---|
| Blank screen for a moment | Render-blocking stylesheet | Inline critical rules, shrink the stylesheet |
| Text appears, then jumps | Late web font | font-display: swap with size-adjust overrides |
| Images pop in one by one | Large files, no dimensions | srcset, width and height, lazy loading below the fold |
| Scrolling stutters | Too much content laid out at once | content-visibility with contain-intrinsic-size |
| Everything slow on a phone | Desktop-sized assets | Responsive images and a smaller base font |
| Nothing renders until a script runs | Script in the head without defer | defer or module, and keep the critical path in CSS |
What to remember
- The browser cannot lay out a page until the CSS that applies to it has arrived.
- Give every image a width and a height so space can be reserved in advance.
- Serve an image at the size it will be shown, and let the browser choose with
srcset. - Lazy-load images below the fold, never the main image of the page.
- Give web fonts a swap behaviour and a fallback with matching metrics.
- Skip the layout work for off-screen sections with
content-visibility. - Fix the biggest asset first; the rest of the list is noise until then.
Try it yourself: open the recipe page you built and look at the network panel. Find the largest transferred file and note whether it is an image, a font or a stylesheet. Add width="800" and height="533" to that image if they are missing and watch the layout stop shifting when it loads. Then add loading="lazy" to the images below the fold and to nothing else, and confirm the first screen still loads at full speed.