There's a gap between a page being fast and a page feeling fast & users only care about the second one. You can shave milliseconds off your load time and still ship something that feels cheap — content lurching down the screen as images pop in, text flashing from one font to another, the main image you actually wanted people to see arriving dead last. None of that shows up as "slow" in a naive timing, but all of it is what people mean when they say a site feels janky.
Perceived performance is mostly about stability and order — things not moving once they appear & the important stuff appearing first. Here's where the felt slowness usually comes from and what actually fixes it.
The layout shift that makes everything feel cheap
You've felt this: you go to tap a link, an image finishes loading above it, everything jumps down & you tap the wrong thing. That's cumulative layout shift & it's the single biggest reason a technically-fast page feels unpolished. It happens because the browser laid out the page before it knew how much space something would take, then had to shove everything around once it found out.
The main culprit is images with no dimensions. If you don't tell the browser how big an image is, it reserves zero height, lays out everything below it, then reflows the whole page when the image arrives. The fix is as old as HTML — put the dimensions on the tag:
<!-- No reserved space — everything below jumps when this loads -->
<img src="/hero.jpg" alt="" />
<!-- Space reserved up front — nothing moves -->
<img src="/hero.jpg" alt="" width="1200" height="630" />Giving width and height doesn't lock the image to that pixel size — with height: auto in your CSS the browser uses the ratio to reserve the right amount of space and still scales it responsively. The same principle covers everything that loads late: ad slots, embeds, banners, anything injected by JavaScript. If something will appear later, reserve its space now, with a fixed height or an aspect-ratio, so its arrival doesn't push the page around.
Most "janky" pages aren't slow — they're unstable. Content loads without reserved space, so everything jumps. Reserve the space up front and the same load time suddenly feels calm.
Fonts: the flash you can choose the shape of
Custom fonts create their own little moment of instability. The browser has to download the font file before it can paint text in it & it has to decide what to do in the meantime. There's no free option — only a choice of which annoyance you'd rather have & you make that choice with font-display.
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap;
}swap shows the text immediately in a fallback font, then swaps to your font when it loads. The upside: content is readable instantly. The downside: a visible flash when the swap happens & if your fallback and real font are different widths, the text reflows — that's layout shift again, this time from type. You cut it down two ways: pick a fallback whose metrics are close to your real font (the CSS size-adjust and ascent-override descriptors exist for exactly this) & preload the font files you know you'll need above the fold so the swap happens sooner:
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
/>There's no perfect answer here, but the wrong one is doing nothing and letting the default behavior hide your text for up to a few seconds while the font loads. Choose your trade-off on purpose.
Don't lazy-load the thing people came to see
Lazy-loading images is good advice that gets over-applied. The idea is sound: don't download images that are far down the page until the user scrolls near them. But people add loading="lazy" to every image on the page, including the big hero at the top — and now the most important image, the one that defines when the page feels "there," is deprioritized behind everything else.
<!-- Wrong: the main image is the one thing you want ASAP -->
<img src="/hero.jpg" loading="lazy" width="1200" height="630" alt="" />
<!-- Right: load the hero eagerly, lazy-load what's below the fold -->
<img
src="/hero.jpg"
loading="eager"
fetchpriority="high"
width="1200"
height="630"
alt=""
/>The rule: anything visible when the page first paints should load eagerly & the single biggest one can get fetchpriority="high" to jump the queue. Save loading="lazy" for what's below the fold — that's where it earns its keep, by not spending bandwidth on images nobody has scrolled to yet.
Lazy-loading above the fold backfires
loading="lazy" on your hero or logo tells the browser "this isn't urgent"
— about the most urgent thing on the page. It's one of the few performance
tweaks that reliably makes the felt experience worse. Lazy is for below
the fold, not above it.
Show progress that matches the shape of what's coming
When you genuinely have to wait — a slow request, a heavy list — how you fill the gap changes how long it feels. A centered spinner tells the user nothing except "wait," and worse, it usually gets replaced by content of a different size, causing another layout shift when the real thing lands.
A skeleton — grey blocks in the rough shape of the content that's coming — feels faster for two reasons. It signals what's about to appear & if you size the skeleton like the real content, the swap from skeleton to content causes no jump, because the space was already the right size. You're not making it faster; you're making the wait legible and stable, which is most of what "feels fast" actually means.
The mental model that ties it together
Stop thinking about load time as one number and start thinking about the sequence and stability of what the user sees. Two questions cover most of it:
- Does anything move after it appears? If yes, you have a layout-shift problem — reserve space for images, embeds, fonts and anything injected later. Movement reads as cheapness, no matter how fast the bytes arrived.
- Is the important stuff first? The hero, the headline, the main content should load eagerly and early; everything secondary can wait. Lazy-load below the fold, never above it.
Get those two right and you'll have pages that feel quick even on a middling connection — because "fast" was never really about the total time. It's about things showing up in the right order and then holding still.