Along with the many performance-enhancing features built into Drupal, lazy loading is one of the most effective (and most noticeable). It greatly improves the one thing your users feel immediately when they land on your site - and that is how fast the page loads.

Now, nobody enjoys waiting for a page to load. When you click on a link, you're already considering pressing the back button if nothing appears right away. The reason why the web page takes so long to load is because the browser is attempting to retrieve dozens of high-resolution images at once, causing serious delays in loading the page.  

That’s where lazy loading comes in. It sounds complex, but it’s actually pretty straightforward. In Drupal 9.1 and above, it’s built right in, so you get the benefits without extra effort. And if you want more control, there are ways to fine-tune how it works. Read the full article to see how you can make the most of it. 

What is Lazy Loading?

Like the name suggests, lazy loading will not load content until someone needs it in their viewport.

Lazy loading waits rather than immediately loading every single image, video, and iframe onto the page. As the user scrolls down, it retrieves the remaining content after loading what is now visible on the screen.

Imagine ten huge photographs in a blog article. Without lazy loading, the page sits there as the browser attempts to grab all ten at once. Lazy loading loads the remaining ones in the background while you read, getting the first couple that are visible. The whole concept is that you're unlikely to even notice it happening. 

Here’s a live example for you:

Take a look at Specbee’s blog listing page: https://www.specbee.com/blogs 

As you scroll, notice how the images don’t load all at once. The ones in your view appear first, and the rest load just in time as you move down the page. Even the next image is quietly fetched in the background right before you reach it.

That’s lazy loading in action. Very subtle, but it makes the entire experience feel much faster and smoother, don’t you think?

Why Lazy Loading is worth it

  1. Your pages load more quickly. This one should go without saying. There will be less waiting if there are fewer items to load up front. Simply put, the website feels faster.
  2. Improves Core Web Vitals - which is a criteria Google uses to gauge user experience. And as you must know, faster websites typically rank higher in search results. (Read more about it here.)
  3. You conserve bandwidth. Not everyone has a lightning-fast internet connection. A major benefit for mobile users or anyone on a slower network is that lazy loading ensures that visitors only download what they actually view.
  4. Individuals can immediately begin reading. Users receive material instantly rather than having to wait for the browser to retrieve a picture that is buried at the bottom of the page as they stare at a loading spinner. That's just a better experience all around.

The good news: Drupal already does this

Yes, if you're working with Drupal 9.1 or higher you should know that lazy loading is enabled by default for standard image fields, views, and media entities. Drupal automatically adds the loading="lazy" to your image tags:

<img src="/sites/default/files/2025-01/my-image.jpg" loading="lazy" alt="Example Image">

Modern browsers only need that one single attribute to handle the rest. It just functions without any additional modules or unique code.

When built-in isn’t good enough for your site

If you need more customization and flexibility in addition to simply lazily loading your images, we’ve got you covered.

Adding it manually in Twig

If you're writing custom Twig templates or overriding views, you can add lazy loading yourself. It's as simple as dropping in the attribute:

<img 
  loading="lazy" 
  width="830" 
  height="835" 
  src="{{ file_url(row.content['#row']._entity|translation.field_image.entity.uri.value) }}" 
  alt="{{ row.content['#row']._entity.field_image.alt }}" 
/>

A couple of things worth noting here: the |translation filter makes sure you're pulling the right image for multilingual sites, and this approach works especially well in grid or list views where you've got a bunch of images on screen at once.

Using the Image Lazyloader module

The Image Lazyloader contrib Drupal module is worth looking at if you're using an earlier version of Drupal (pre-9.1) or if you want more advanced capabilities like loading animations or custom placeholder images.

composer require drupal/image_lazyloader
drush en image_lazyloader

Next, select Configuration → Media → Image Lazyloader, where you may exclude particular routes or views, set up placeholders, and choose which image styles are lazily loaded. In the background, it uses JavaScript to load the actual images as necessary and replaces image sources with placeholders. If your theme contains a lot of JS or complicated markup, this is incredibly helpful.

Using JavaScript to Lazy Load

Drupal's built-in lazy loading feature may not always work, such as when you're using a JS framework or AJAX to generate images dynamically. In certain situations, a simple, contemporary method of determining when anything enters the viewport is to use the Intersection Observer API:

document.addEventListener("DOMContentLoaded", function() {
  const lazyImages = document.querySelectorAll("img[data-src]");
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute("data-src");
        observer.unobserve(img);
      }
    });
  });

  lazyImages.forEach(img => observer.observe(img));
});

And in your Twig template, use data-src instead of src:

<img data-src="{{ file_url(content.field_image.entity.uri.value) }}" alt="{{ content.field_image.alt }}" />

If you want complete control over dynamic or AJAX-driven pages, this provides that.

Tackling sliders and carousels

Image sliders are a problem that I've encountered numerous times. Even if only the first slide image is visible, libraries such as Slick, Swiper, or Owl Carousel frequently load all of the slide images up front. That's a lot of bandwidth spent on a product carousel with a dozen high-resolution banners.

How to fix it? You can use data-src for all other images and load the first one normally:

{% for item in items %}
  <div class="slide">
    {% if loop.first %}
      <img src="{{ file_url(item.entity.field_image.entity.uri.value) }}" alt="{{ item.entity.field_image.alt }}">
    {% else %}
      <img data-src="{{ file_url(item.entity.field_image.entity.uri.value) }}" class="lazy-slide" alt="{{ item.entity.field_image.alt }}">
    {% endif %}
  </div>
{% endfor %}

Then you can hook into the slider's events to swap in the real image right before it's needed.

For Slick:

$('.your-slider').on('beforeChange', function(event, slick, currentSlide, nextSlide){
  var $nextSlide = $(slick.$slides[nextSlide]);
  $nextSlide.find('img[data-src]').each(function() {
    $(this).attr('src', $(this).data('src')).removeAttr('data-src');
  });
});

For Swiper:

var swiper = new Swiper('.swiper-container', {
  on: {
    slideChangeTransitionStart: function () {
      var activeSlide = this.slides[this.activeIndex];
      $(activeSlide).find('img[data-src]').each(function() {
        $(this).attr('src', $(this).data('src')).removeAttr('data-src');
      });
    }
  }
});

Now, only the active and upcoming slides will load their images. Your carousel still looks seamless, and your page is way lighter.

What about those background images?

CSS background images, such as large hero banners and section backgrounds that are set by background-image, is something Drupal does not automatically lazy load. However, a minor Drupal behavior can resolve it:

(function ($, Drupal) {
  Drupal.behaviors.lazyBackground = {
    attach: function (context) {
      $('.lazy-bg', context).once('lazy-bg').each(function () {
        var bg = $(this).data('bg');
        $(this).css('background-image', 'url(' + bg + ')');
      });
    }
  };
})(jQuery, Drupal);

And in Twig:

<div class="lazy-bg" data-bg="{{ file_url(content.field_banner_image.0['#item'].entity.uri.value) }}"></div>

Although it's a minor detail, it can significantly reduce the initial load time for pages with large hero images.

Final thoughts

One of those infrequent victories where the return is genuine, and the work is minimal, is lazy loading. Faster pages, stronger Core Web Vitals, and a smoother experience are becoming the baseline. And with modern Drupal, you’re already a step ahead.

Even in more complex scenarios like background images, AJAX-driven content, or dynamic sliders, the fixes remain lightweight. A small tweak in Twig here, a bit of JavaScript there, and the system continues to scale seamlessly.

Most users won’t notice lazy loading at all. They’ll just feel that your site works better, loads quicker, and responds the way it should. 

If you want to make sure your website’s performance stays invisible, but keeps delivering, you might want to reach out to Specbee’s Drupal experts who can implement customized and cost-effective Drupal solutions.

Contact us

LET'S DISCUSS YOUR IDEAS. 
WE'D LOVE TO HEAR FROM YOU.

CONTACT US SUBMIT RFP