Ever added loading="lazy" to your <img> or <iframe> and felt like magic: the browser just held off loading until you scrolled near the element? Me too. But then I started noticing images loading earlier than expected, or huge layout jumps when an image finally loaded.
I decided to dig into what browsers really do when you tell them to "lazy load". Spoiler: it's way more than a checkbox. There’s a whole internal orchestration involving viewport intersection, resource prioritization, and layout management.
The moment lazy loading felt less lazy
I was debugging a client’s page with dozens of images and embedded videos. They all had loading="lazy". Yet, performance profiles showed many images loading immediately, even those far below the fold.
Why were these supposed-to-be-lazy images loading eagerly? And why did the page jump so much as images popped in?
Turns out, browsers apply extra smarts and heuristics to decide when exactly to start loading, balancing speed and smoothness.
Intersection Observer: The eyes of lazy loading
Under the hood, modern browsers use something like the Intersection Observer API to detect when an element enters or nears the viewport.
Here's what happens:
- When you mark an image or iframe with
loading="lazy", the browser defers its network request.
- The browser registers a viewport intersection watcher on the element , similar to how you'd use Intersection Observer in your own JS.
- Once the element is close enough (within some margin), the browser kicks off the resource fetch.
This margin isn't necessarily zero. Browsers typically use a threshold to start loading before the element actually enters the viewport, so the content is ready by the time you scroll to it.
This margin varies across browsers and even device conditions (like network speed or memory).
Example
Imagine an image 1000px below the viewport bottom. The browser might start loading it when it’s 200px away, not strictly when visible. This preloading helps avoid flickers.
// Roughly, browsers do something like this internally
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting || entry.boundingClientRect.top < window.innerHeight + preloadMargin) {
loadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
observer.observe(imageElement);
Why some images still load eagerly
Lazy loading isn’t absolute. Browsers override it in some cases:
- Above-the-fold images: If an image is already visible or close enough on initial load, the browser loads it eagerly.
- Network conditions: On fast connections, browsers might be more aggressive loading early.
- Resource hints: If you have
<link rel="preload"> or <link rel="prefetch"> for an image, lazy loading is skipped.
- Low memory or CPU: On constrained devices, browsers might skip lazy loading to avoid complex scheduling.
- Critical images: Browsers may heuristically detect hero images or large banners and load them immediately to avoid layout shift.
This explains why sometimes your lazy images seem to load right away.
Resource prioritization: the browser’s juggling act
Browsers don’t just load everything they spot at once. They maintain a priority queue for resource fetching, balancing:
- Critical CSS and JS
- Visible images
- Lazy images near viewport
- Background images
When a lazy image becomes close enough, it moves up the priority ladder.
This prioritization uses hints from the loading attribute plus internal heuristics based on size, position, and user interaction.
Avoiding layout shifts when lazy loading
One of the biggest UX issues with lazy loading is layout shift , that jarring jump when an image finally loads and pushes content down.
Browsers try to reduce this by:
- Using width and height attributes: If your
<img> declares its intrinsic dimensions, browsers reserve the right amount of space before the image loads.
- CSS aspect ratio boxes: If you set
aspect-ratio on images or containers, the browser can reserve space even without explicit width/height.
- Placeholder images or dominant color placeholders: Some browsers and frameworks insert low-res or color placeholders to hold space.
If you don’t provide dimensions, browsers might guess, but it’s error prone. That’s why many layout shifts happen even with lazy loading.
Debugging unexpected eager loading and layout shifts
If you see images loading too early:
- Check if you have
preload or prefetch hints on those images.
- Inspect if their
loading attribute is really set to lazy (sometimes JS frameworks mutate attributes).
- Audit network conditions in devtools throttling , browsers may behave differently on slow connections.
To debug layout shifts:
- Use Chrome’s Performance panel with Layout Shift Regions enabled.
- Inspect computed styles to verify if width/height or aspect-ratio are set.
- Make sure your images or containers reserve space.
Wrapping up
Lazy loading looks simple on the surface, but browsers juggle a lot behind the scenes: watching viewport intersection with clever margins, balancing resource priority, and trying to keep your page stable.
Next time your images load a bit too eagerly or cause layout jumps, remember it’s not just your code , the browser is trying to find the best balance between snappy loading and smooth UX.
Pro tip: always provide image dimensions or use CSS aspect ratios to help the browser reserve space early.
Now you know what’s really happening under the hood when you add loading="lazy"!
Try scrolling through your next project and open the network tab to watch lazy loading in action. It’s a neat peek into browser resource choreography you usually don’t see.