Learn how to lazy load images properly with simple HTML and JavaScript tips to improve page speed, reduce waste, and avoid common UX mistakes.
A page packed with full-size images loading all at once is a bit like making every guest in the building use the lift at the same time. Things get slow, awkward, and completely avoidable. If you’re wondering how to lazy load images, the short version is this: only load the images a user is likely to see, and wait on the rest until they are actually needed.
That sounds simple because, thankfully, it mostly is. The trick is knowing when native lazy loading is enough, when JavaScript is worth the effort, and where developers accidentally make the experience worse instead of better.
Lazy loading images means deferring off-screen image requests until the user gets close to them in the viewport. Instead of downloading every image during the initial page load, the browser prioritises what is visible first.
That helps with performance in a few obvious ways. The page downloads fewer assets up front, rendering can happen sooner, and users on weaker connections are not forced to fetch images they may never scroll down to see. It is one of those rare performance wins that is both practical and easy to explain to non-developers.
It is not magic, though. Lazy loading reduces unnecessary work, but it does not make huge, badly compressed images acceptable. If your homepage contains a 4MB hero image, lazy loading the screenshots further down the page will not save you from that decision.
For most websites, the easiest answer to how to lazy load images is the browser’s native loading attribute. Add `loading=”lazy”` to an `img` element and modern browsers will defer that image if it is off screen.
“`html
“`
That is the version most developers should start with. It is clean, readable, and does not need a library or custom script trying to feel clever.
There are a couple of details worth getting right. Always include meaningful `alt` text when the image adds content. Also include `width` and `height` attributes where possible. That gives the browser enough information to reserve space before the image appears, which helps prevent layout shifts. No one enjoys trying to tap a button that suddenly teleports because an image finally loaded above it.
Native lazy loading is ideal for content images, gallery items, article thumbnails, and product listings below the fold. It is less ideal for images that are critical to the first impression of the page.
This is where lazy loading gets misused.
You should usually avoid lazy loading above-the-fold images, especially hero images or anything likely to contribute to Largest Contentful Paint. If the image is one of the first things the user sees, delaying it is not optimisation. It is self-sabotage with good intentions.
Logos, key banners, and primary article images near the top of the page are often better loaded normally. The same goes for UI icons that affect usability, though many of those should not be raster images in the first place.
In other words, lazy load the images a user might see later, not the ones they came for in the first place.
If native lazy loading does the job, stop there. If you need more control, use JavaScript with the Intersection Observer API.
This approach is helpful when you want to swap placeholder sources, fade images in, or support cases where you are storing the real image URL in a data attribute until the image approaches the viewport.
“`html
“`
“`javascript const images = document.querySelectorAll(‘.lazy-image’);
const observer = new IntersectionObserver((entries, obs) => { entries.forEach(entry => { if (!entry.isIntersecting) return;
const img = entry.target; img.src = img.dataset.src; img.removeAttribute(‘data-src’); obs.unobserve(img); }); });
images.forEach(image => observer.observe(image)); “`
This gives you finer control than `loading=”lazy”`, but it comes with more moving parts. More moving parts means more things to forget, more bugs to introduce, and more future-you annoyance. Use it when the added control is genuinely useful, not because writing 15 lines of JavaScript feels productive.
A lazy-loaded image should not appear broken while waiting to load. That is where placeholders help.
The simplest option is reserving the image space with width and height and letting the browser show empty space until the image arrives. That is often enough. If you want something more polished, you can use a low-quality placeholder, a blurred preview, or a neutral background colour matching the image area.
The main goal is stability, not theatre. A tasteful placeholder can improve perceived performance. A spinner on every image on the page can make your site look like it is panicking.
One common mistake is lazy loading every image by default. That includes the images at the top of the page, which can hurt perceived speed and Core Web Vitals.
Another is forgetting responsive images. If you use `srcset` and `sizes`, keep using them. Lazy loading decides when to fetch an image. Responsive image markup helps the browser decide which version to fetch. Those jobs are related, but they are not the same.
“`html
“`
A third issue is layout shift. If image dimensions are missing, the browser cannot reserve space properly, so the page jumps around as images load. That is bad for usability and bad for metrics.
A fourth is relying on lazy loading instead of fixing image optimisation. Compress your images, choose sensible formats, and serve the right dimensions. Lazy loading is not a licence to chuck giant files at the browser and hope for the best.
Do not just add the attribute, nod at your screen, and call it performance work.
Open DevTools, throttle the network, and reload the page. Watch which image requests fire immediately and which are deferred until you scroll. If off-screen images load straight away, your implementation may not be working as expected.
You should also test on real devices if you can, especially lower-powered mobiles. Desktop testing is useful, but a page that feels fine on your machine may behave very differently on a budget handset on patchy 4G.
Pay attention to the visual experience as well. Are images appearing too late? Is there obvious jank? Are placeholders stretching oddly? Good lazy loading should feel boring. That is a compliment.
For most projects, native lazy loading is the sensible default. It is easy to implement, widely supported, and good enough for common use cases.
JavaScript is worth considering when you need custom transitions, placeholder swapping, analytics hooks, or specific loading thresholds. Even then, keep it lightweight. If your lazy loading solution needs its own mini architecture, it may be time for a quiet word with yourself.
At Front Runner, we would usually recommend starting simple and only adding complexity when there is a clear reason. Performance work has a funny way of becoming less performant when it turns into a hobby project.
If an image is not needed immediately, lazy load it. If it is central to the first screen, do not. Add image dimensions, keep your responsive markup intact, and test the result under realistic conditions.
That is really the whole thing. Lazy loading works best when it is treated as one sensible part of a broader performance approach, not a silver bullet. Start with the browser feature, stay suspicious of overengineering, and make the page feel fast for actual humans rather than just your lighthouse score.