Loading every photo at once is the silent killer of page speed, and you can fix it in minutes.
In this tutorial we’ll build a simple image gallery from scratch that only fetches images as you scroll.
I’ll show the exact files to create, how to use HTML’s loading=”lazy”, a responsive CSS Grid layout, and a tiny script if you want tighter control.
You’ll test it in the browser, watch images load in DevTools, and see real PageSpeed improvements.
By the end you’ll have a working gallery that keeps bandwidth low and pages fast, step by step, no magic required.
Step‑by‑Step Build: Your Simple Image Gallery With Lazy Loading

You’re about to build a working image gallery from scratch that only loads images when they’re actually needed. We’ll walk through every step, from setting up your files to checking the final result in the browser.
Here’s what you need in your project folder: an index.html file, a styles.css file, a script.js file (leave it empty for now), and an /images folder with some test photos inside. Keep all three files at the root level and toss a few images into that /images folder so you’ve got something to work with.
Seven steps to get your gallery running:
- Start with an HTML boilerplate and drop a
<div class="gallery-container">wrapper inside the body. - Add your
<img>elements inside that container. Each one getsloading="lazy"in the tag. - Put explicit
widthandheightattributes on every image so the browser knows the dimensions before anything loads. - Set up responsive CSS Grid rules in styles.css to arrange images in rows that adjust to whatever screen size someone’s using.
- Add
height: auto;andwidth: 100%;(or a responsive percentage) in your CSS to stop images from distorting or shifting the layout around. - Open index.html in your browser and scroll down slowly. You’ll see images load as they get close to the viewport.
- Run the page through Google PageSpeed Insights. Check the score. A lot of galleries jump from around 80 to 98 once lazy loading is in place.
Chrome, Firefox, Safari, and Edge all handle native lazy loading pretty much the same way. Your gallery will behave consistently across major browsers. Pop open DevTools and reload. You’ll see only the first few images download right away. The rest show up in the Network tab as you scroll.
The sections ahead cover things like adding blurred placeholders, using Intersection Observer for tighter control, and optimizing image sizes through a CDN. Each layer builds on what we’ve done here.
Core Concepts Behind a Lazy‑Loaded Image Gallery

Lazy loading defers image downloads until each image is near the viewport. The browser skips fetching offscreen content until the user scrolls close enough. This matters a lot in galleries with dozens or hundreds of photos because loading everything upfront wastes bandwidth and slows down the parts people actually see first.
Deferring images improves performance in ways you can measure. It cuts bandwidth use, helps Largest Contentful Paint (LCP) by focusing on visible content, and prevents massive files from blocking interactivity. Testing on a simulated Good 2G connection showed that adding a single 3 MB image caused up to 55 seconds of lost interactivity. When you defer offscreen images, you dodge these delays and let the browser spend network resources on what matters right now.
Native lazy loading is supported in over 95% of browsers globally. You can rely on it for most users without needing a third-party library. The technique fits naturally into beginner workflows because it’s just an HTML attribute. No complex scripts or build tools required. Modern lazy loading turns a performance optimization into a one-line addition.
Structuring HTML and CSS for a Responsive Image Gallery

Use semantic HTML to give your gallery clear structure and make it easier for browsers and assistive tools to understand. Wrap each image in a <figure> element and use <img> with descriptive alt text inside. This keeps your markup clean and signals that each image is a self-contained piece of content.
Build your layout with CSS Grid and add responsive breakpoints so the gallery adapts to different screen sizes without breaking. The important part is setting explicit width and height attributes on every <img> tag. Browsers read these values and compute the aspect ratio before the image loads, which prevents Cumulative Layout Shift (CLS). In your CSS, set height: auto; so the image scales proportionally and width: 100%; (or a responsive value) to fill its container without distortion.
| Technique | Purpose |
|---|---|
| Explicit width/height | Maintain aspect ratio |
| height:auto | Prevent distortion |
| CSS Grid columns | Responsive image distribution |
| Max-width:100% | Keep images within container |
Implementing Lazy Loading Methods: Native Attribute and Intersection Observer

You’ve got two main approaches for lazy loading images: the native HTML loading attribute and JavaScript with Intersection Observer. The native method is simpler and works out of the box in most browsers, but it stops working when JavaScript is disabled (the browser spec ties the feature to scripting). Intersection Observer gives you more control over when images load and raises browser support to around 94.91%, but it requires writing a bit of JavaScript.
Choosing the right approach depends on your project’s needs. If you want the quickest setup and don’t need fine tuned preload distances, go with the native attribute. If you need to preload images at a specific scroll distance or support older browsers, Intersection Observer is the better choice.
Firefox has speculative preloading behavior that can differ slightly from Chrome and Safari, so always test your gallery in multiple browsers to confirm images load at the same scroll points. These differences are usually minor, but they can surprise you if you’re debugging why an image appears earlier or later than expected.
Native HTML Lazy Loading
Add loading="lazy" to any <img> or <iframe> tag and the browser defers fetching until the element is near the viewport. This works on images and embedded iframes but not on CSS background images. Performance improvements are immediate. Pages often see faster LCP and reduced bandwidth because offscreen content stays out of the initial request queue.
The main limitation is that loading="lazy" is disabled when scripting is turned off. This is part of the HTML spec and ties lazy loading behavior to JavaScript availability, even though the attribute itself is pure HTML. If your audience includes users who disable JavaScript for privacy or security, consider adding a fallback or using Intersection Observer instead.
JavaScript with Intersection Observer
Intersection Observer lets you detect when an element enters a defined boundary near the viewport, then trigger image loading with JavaScript. The rootMargin option controls preload distance and accepts values in pixels or percentages, like rootMargin: "100px" to start loading images 100 pixels before they enter the viewport.
This approach increases device support to about 94.91% and gives you precise control over loading behavior. The downside is that rootMargin behavior can be inconsistent across browsers. Some implementations handle percent values differently, and pixel based margins may shift slightly depending on scroll speed. Test your settings thoroughly and pick a rootMargin value that feels smooth on real devices, especially on mobile.
Enhancing the Gallery With Blurred Placeholders and LQIP

Blurred placeholders improve the user experience by showing a low quality preview immediately while the full image loads in the background. A typical workflow reduces a 4.55 MB original image to a 230 KB resized version for display, then creates a 25 KB blurred placeholder that appears instantly. This tiny placeholder eliminates blank white boxes and gives users visual feedback that content is loading.
You can combine blurred placeholders with native lazy loading by using an inline CSS background-image to display the blurred version while the browser lazily loads the real src. Set the placeholder as a background on a wrapper div, then place the <img loading="lazy"> inside. When the image loads, it overlays the blurred background. This avoids extra HTTP requests if you inline the blurred image as a Data URL, and it keeps your HTML practical even though inline styles usually break best practices.
LQIP workflow goes like this: generate a tiny blurred version of each image during your build or server side process, then embed it as a background or Data URL. Inline Base64 encoded placeholders eliminate extra requests but may slightly reduce PageSpeed scores and require compute resources during generation. Inline backgrounds work without JavaScript and pair well with loading="lazy", but they add markup and can complicate dynamic galleries.
Always set explicit dimensions so the browser reserves space before the image loads, avoiding layout shifts even with placeholders. Use placeholders in galleries with large high resolution images, where the visual gap between placeholder and full image is obvious and the delay is noticeable.
Testing, Debugging, and Measuring Lazy Loading Performance

Open your browser’s DevTools and switch to the Network tab, then filter by “Img” and reload your gallery page. You should see only above the fold images download immediately. Scroll down slowly and watch new image requests appear in the Network panel. This confirms that lazy loading is working. Check the Initiator column for entries labeled “lazyload” or similar, which tells you whether the browser or a script triggered the fetch.
Performance measurements show real world impact. Experiments adding loading="lazy" to offscreen images produced a 22% improvement in Largest Contentful Paint and a 26% reduction in page weight. Run your gallery through Google PageSpeed Insights before and after implementing lazy loading. Scores often jump from around 80 to 98 once offscreen images are deferred. These numbers reflect how much bandwidth and main thread work you’ve saved by not loading everything upfront.
Test your gallery in Chrome, Firefox, Safari, and Edge using network throttling (try Good 2G or Slow 3G) to see how lazy loading behaves under constrained bandwidth. Check that images appear smoothly as you scroll and that the Initiator column in DevTools shows the expected trigger. Cross browser testing catches edge cases like Firefox’s speculative preloading and makes sure your gallery performs consistently for all users.
Final Words
You’re in the action: you built a complete image gallery with lazy loading—HTML, CSS, JS, an /images folder, img tags using loading=”lazy”, and explicit width/height to keep layouts stable.
You learned the core ideas: defer offscreen images to save bandwidth and improve LCP, when to use Intersection Observer, and how blurred placeholders (LQIP) polish the UX.
You tested in the browser, tuned the CSS grid for responsiveness, and validated results with PageSpeed. Ready to keep iterating—try to create a simple image gallery with lazy loading step by step in your next mini-project. You’ll see performance wins and better user experience.
FAQ
Q: What will I build in this gallery tutorial?
A: This tutorial builds a complete working image gallery from scratch with native lazy loading, a responsive layout, and minimal JavaScript so images load only when they near the viewport, improving page speed.
Q: What files and folders do I need?
A: The files you need are index.html, styles.css, script.js and an /images folder to store assets, keeping image paths simple so lazy loading and references work cleanly.
Q: How do I add native lazy loading to images?
A: Adding native lazy loading is done by adding loading=”lazy” to each img element; browsers then defer fetching until images are close to the viewport, reducing initial network load.
Q: Why should I include width and height attributes on img tags?
A: Including width and height prevents layout shifts by giving the browser an image aspect ratio; combine this with CSS height:auto and width:100% to keep images responsive and stable.
Q: How do I make the gallery responsive without causing layout shifts?
A: Making the gallery responsive uses CSS Grid with breakpoints, images set to max-width:100%, and explicit width/height attributes so the layout adapts while avoiding CLS.
Q: When should I use Intersection Observer instead of native lazy loading?
A: Use Intersection Observer when you need precise control (rootMargin preload distance), custom behavior, or broader compatibility; otherwise loading=”lazy” is simpler and often enough.
Q: How do blurred placeholders and LQIP improve user experience?
A: Blurred placeholders work by showing a tiny, low-quality image first (often inlined as a Data URL), giving instant visual feedback while the full, lazy-loaded image downloads.
Q: How should I test and measure lazy loading performance?
A: Test by scrolling in DevTools, inspect the network waterfall and filmstrip, then run PageSpeed Insights or Lighthouse; expect LCP improvements (~22%) and page weight reductions (~26%) in typical tests.
Q: What are the basic build steps for a working lazy-loaded gallery?
A: The basic steps are: create HTML boilerplate and container, add img elements with loading=”lazy” plus width/height, write a responsive CSS grid (height:auto, width:100%), then test and validate.

