How to Lazy-Load a Bootstrap 5 Image Gallery for Speed

  • Canvas Team
  • 8 min read
How to Lazy-Load a Bootstrap 5 Image Gallery for Speed
8 min read
Share:

lazy loading API with a well-structured Bootstrap 5 image gallery gives you a fast, accessible, and maintainable result with surprisingly little code.

Key Takeaways

  • Native loading="lazy" defers off-screen images with zero JavaScript overhead.
  • The Intersection Observer API gives you fine-grained control when you need custom thresholds or animation callbacks.
  • Bootstrap 5’s grid and utility classes make responsive gallery layouts straightforward without writing custom CSS.
  • Serving correctly sized thumbnails via srcset and sizes compounds the performance gain from lazy loading.
  • A well-optimised gallery improves LCP, reduces TBT, and directly supports better Core Web Vitals scores.

Why Lazy Loading Matters for Image Galleries

An image gallery page can easily contain 30 to 100 images. Without deferral, every one of those assets is requested the moment the browser parses the HTML, regardless of whether the visitor will ever scroll to see them. This creates three concrete problems:

  • Increased Largest Contentful Paint (LCP): bandwidth is split across every image, slowing the render of the hero or above-the-fold content.
  • Wasted data transfer: a significant share of visitors never scroll to the bottom of a long gallery.
  • Higher hosting costs: unnecessary egress adds up at scale.

For a deeper look at deferring more than just images — including scripts — see our guide on how to implement lazy loading and defer scripts in HTML templates.

man in orange and black jacket and black pants standing beside white truck during daytime
Photo by billow926 on Unsplash

Approach 1 — Native Browser Lazy Loading

The simplest implementation requires a single HTML attribute. Every modern browser (Chrome 76+, Firefox 75+, Edge 79+, Safari 15.4+) respects loading="lazy" on <img> elements. The browser decides when to fetch the image based on the user’s scroll position and network conditions.

<!-- Bootstrap 5 gallery grid with native lazy loading -->
<div class="row g-3">
  <div class="col-6 col-md-4 col-lg-3">
    <img
      src="images/gallery/photo-01.jpg"
      srcset="images/gallery/photo-01-400.jpg 400w,
              images/gallery/photo-01-800.jpg 800w"
      sizes="(max-width: 576px) 50vw,
             (max-width: 992px) 33vw,
             25vw"
      alt="Gallery image 1"
      class="img-fluid rounded"
      loading="lazy"
      width="800"
      height="600"
    />
  </div>
  <!-- repeat for additional items -->
</div>

Two points deserve emphasis. First, always include explicit width and height attributes. Without them the browser cannot reserve space for the image before it loads, causing layout shift and hurting your Cumulative Layout Shift (CLS) score. Second, do not apply loading="lazy" to above-the-fold images — the hero or the first row of a gallery. Those should load eagerly so LCP is not penalised.

Approach 2 — Intersection Observer for Custom Control

Native lazy loading is excellent for straightforward use cases, but it offers no callbacks and limited threshold control. If you want to trigger a fade-in animation, load a higher-resolution version on entry, or support older Safari versions, the Intersection Observer API is the right tool.

The pattern is to store the real image URL in a data-src attribute and use a placeholder (a tiny LQIP — Low Quality Image Placeholder — or a CSS background colour) in the actual src.

<!-- Gallery item using data-src for Intersection Observer -->
<div class="col-6 col-md-4 col-lg-3">
  <img
    src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
    data-src="images/gallery/photo-02.jpg"
    data-srcset="images/gallery/photo-02-400.jpg 400w,
                 images/gallery/photo-02-800.jpg 800w"
    sizes="(max-width: 576px) 50vw, 25vw"
    alt="Gallery image 2"
    class="img-fluid rounded lazy-image"
    width="800"
    height="600"
  />
</div>
<script>
  (function () {
    var lazyImages = document.querySelectorAll('img.lazy-image');

    if (!('IntersectionObserver' in window)) {
      // Fallback: load all images immediately
      lazyImages.forEach(function (img) {
        img.src = img.dataset.src;
        if (img.dataset.srcset) img.srcset = img.dataset.srcset;
      });
      return;
    }

    var observer = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        if (!entry.isIntersecting) return;
        var img = entry.target;
        img.src = img.dataset.src;
        if (img.dataset.srcset) img.srcset = img.dataset.srcset;
        img.classList.remove('lazy-image');
        img.classList.add('lazy-loaded');
        observer.unobserve(img);
      });
    }, { rootMargin: '200px 0px' });

    lazyImages.forEach(function (img) { observer.observe(img); });
  })();
</script>

The rootMargin: '200px 0px' setting begins fetching images 200 pixels before they enter the viewport, preventing a visible flash of empty space on fast scrolls.

grayscale photo of woman in white shirt and black pants
Photo by Y M on Unsplash

Adding a CSS Fade-In on Load

A subtle opacity transition prevents the jarring swap from placeholder to full image. Because Bootstrap 5 ships with CSS custom properties, you can hook into the same design token approach used throughout a template like the Canvas HTML Template.

<style>
  .lazy-image {
    opacity: 0;
    transition: opacity 0.4s ease;
  }

  .lazy-loaded {
    opacity: 1;
  }
</style>

This works because the Intersection Observer script adds the lazy-loaded class after assigning src. The browser triggers a repaint once the image decodes, and the CSS transition takes over from there. Keep the duration short — 300 ms to 500 ms — to avoid the animation feeling sluggish on fast connections.

Most Bootstrap 5 gallery implementations include a lightbox. The common mistake is pointing the anchor’s href at the full-size image and letting the lightbox script pre-fetch it. Set the anchor correctly so the lightbox reads from data-src or a dedicated data-lightbox-src attribute instead.

<div class="col-6 col-md-4 col-lg-3">
  <a
    href="images/gallery/photo-03-full.jpg"
    data-fancybox="gallery"
    data-caption="Coastal landscape at dusk"
  >
    <img
      src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
      data-src="images/gallery/photo-03.jpg"
      alt="Coastal landscape at dusk"
      class="img-fluid rounded lazy-image"
      width="800"
      height="600"
      loading="lazy"
    />
  </a>
</div>

Fancybox, GLightbox, and PhotoSwipe all respect the anchor’s href at click time rather than on page load, so the full-resolution file is only fetched when the visitor actually opens the lightbox. This keeps your initial page load lean.

Combining Lazy Loading with srcset for Maximum Gains

Lazy loading defers when an image loads. srcset controls which file the browser downloads. Together they produce the largest performance improvement. Generate at least two thumbnail sizes for gallery images — a 400 px wide version for mobile columns and an 800 px version for desktop columns. Tools like Sharp (Node.js), Squoosh CLI, or a build-step Gulp task make batch resizing straightforward.

Use WebP as your primary format with a JPEG fallback inside a <picture> element:

<picture>
  <source
    type="image/webp"
    data-srcset="images/gallery/photo-04-400.webp 400w,
                 images/gallery/photo-04-800.webp 800w"
    sizes="(max-width: 576px) 50vw, 25vw"
  />
  <img
    src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
    data-src="images/gallery/photo-04-800.jpg"
    data-srcset="images/gallery/photo-04-400.jpg 400w,
                 images/gallery/photo-04-800.jpg 800w"
    sizes="(max-width: 576px) 50vw, 25vw"
    alt="Gallery image 4"
    class="img-fluid rounded lazy-image"
    width="800"
    height="600"
  />
</picture>

When using Intersection Observer with <picture>, update the srcset on each <source> element in addition to the <img>. Many developers forget the <source> elements and wonder why WebP is not being served.

SEO Considerations for Lazy-Loaded Galleries

Googlebot renders JavaScript and supports Intersection Observer, so lazy-loaded images are indexable. However, there are two practices worth following. First, ensure every <img> has a descriptive alt attribute — this is both an accessibility requirement and an image SEO signal. Second, consider adding ImageObject schema to your gallery page to provide Google with structured metadata about each image. Our post on adding schema markup to a Bootstrap 5 HTML template covers that process in detail.

Also verify in Google Search Console’s URL Inspection tool that your gallery images appear in the rendered HTML after JavaScript execution. If Googlebot times out before the observer fires, images may not be indexed. The 200 px rootMargin and a reasonable scroll depth ensure most images load well within the render budget.

Frequently Asked Questions

It is supported in all major browsers released since 2020, including Chrome, Firefox, Edge, and Safari 15.4+. For older Safari, either accept that images load eagerly (the page still works correctly) or add the Intersection Observer fallback shown in this article.

No. Googlebot executes JavaScript and processes Intersection Observer callbacks. As long as images have descriptive alt text and correct structured data, they will be crawled and indexed. Verify this with the URL Inspection tool in Search Console.

No. Images visible in the initial viewport should use loading="eager" (the default) or simply omit the loading attribute. Applying lazy loading to above-the-fold images delays their fetch and will hurt your Largest Contentful Paint score.

Always set explicit width and height attributes on your <img> elements. This allows the browser to calculate the aspect ratio and reserve the correct space before the image loads, eliminating the reflow that causes Cumulative Layout Shift.

Yes, but with a caveat. Bootstrap’s Masonry integration relies on images being loaded to calculate column heights correctly. Trigger Masonry’s layout recalculation inside the Intersection Observer callback after each image loads by calling the Masonry layout() method, or initialise Masonry after all images have loaded using the imagesLoaded library.

Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.

If you’re building with the Canvas HTML Template and want to ship production-ready Bootstrap 5 layouts faster, try Canvas Builder free — the visual builder that exports clean Canvas-ready markup in minutes.

Skip the setup — build it free

Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.

Share:
Canvas Team
Canvas Team

Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.

More from the Canvas Blog