Lazy-loading images

Look at creating a fast website with lots of images, some loaded without JavaScript & more loaded with JavaScript.

Goal

We’re going to look at making a basic gallery user experience without JavaScript then enhance it with some JavaScript to substantially improve the loading and rendering performance.

This lesson is completely optional—do it only if you’re interested in making your portfolio much faster.

The idea is to make the website load really quickly—by default only showing a few images.

  • When JavaScript isn’t enabled we use the <noscript> tag to show more images.
  • When JavaScript is enabled we go and download more images to the page after it has already loaded.
  1. Type it, type it real good

    Remember the purpose of this lesson is to type the code out yourself—build up that muscle memory in your fingers!

Fork & clone

Start the lesson by forking and cloning the lazy-loading-images repository.

Fork & clone the “lazy-loading-images” repo.

The repository will have some starter files to get you on your way.

  1. Fork & clone

    This includes some starter code that you can get by forking and cloning the repository.

1 Set up the project

The basic starter repo that has the CSS files & images we need inside it—we’re going to work from that.

  1. lazy-loading-images
  2. _data
  3. images.yml
  4. css Lots of CSS in this folder—we’re not going to touch it
  5. _config.yml
  6. images Lots of images in this folder
  7. index.html

Start Jekyll in this folder. Open the lazy-loading-images into your code editor.

  1. Naming conventions

    Don’t forget to follow the naming conventions.

2 Prioritize the images

To start we need to group the images into 3 categories:

  1. Critical images — these images will always load
  2. Must-have images — these images should always load
  3. Non-critical images — it’d be nice if these images loaded

Of course—the images themselves may just fail to load because of slow Internet—there’s nothing we can do to mitigate that.

For this exercise, we’ll say the first 2 images are critical, the next 2 are must-have, and the rest are non-critical images.

Let’s adjust our HTML to group the images into our three categories.

⋮
</head>
<body>

   <div class="grid">
    {% for img in site.data.images limit:2 %}
      <div class="critical-img unit xs-1 m-1-2">
        <div class="embed embed-16by9">
          <img class="embed-item" src="images/{{img}}" alt="">
        </div>
      </div>
    {% endfor %}
    {% for img in site.data.images offset:2 limit:2 %}
      <div class="non-critical-img unit xs-1 m-1-2">
        <div class="embed embed-16by9">
          <img class="embed-item" data-src="images/{{img}}" alt="" hidden>
          <noscript>
            <img class="embed-item" src="images/{{img}}" alt="">
          </noscript>
        </div>
      </div>
    {% endfor %}
    {% for img in site.data.images offset:4 %}
      <div class="non-critical-img unit xs-1 m-1-2" hidden>
        <div class="embed embed-16by9">
          <img class="embed-item" data-src="images/{{img}}" alt="">
        </div>
      </div>
    {% endfor %}
  </div>

</body>
</html>

In the code above we have three loops, each has a specific purpose and ties to the our image categories:

  1. The images in the first loop will always show.
  2. The images in the second loop will show with or without JavaScript, essentially they will also always show.
  3. The images in the third loop will only show when the JavaScript is triggered.

Why bother having the second loop at all if they’re always going to show? To help the page load faster. Most (almost all) browsers have JavaScript enabled, so these images can be triggered later with JavaScript. The page will load super quick, showing only the images in the first loop, then the JavaScript will kick in and start downloading the rest—but our user will already have a nice, complete page.

  1. Line F

    In this loop we’re limiting the output to the first two images.

    These are the “critical” images—they will always be shown.

  2. Line G

    There’s a new class here, .critical-img, to help us remember the importance of this image.

  3. Line M

    This is a duplicate of the previous loop with a few small modifications:

    • It has offset:2, meaning it will start at the item with the index of 2, aka the 3rd item.
    • It has limit:2, again, here we only want to spit two images out—these are our “must-have” images.
  4. Line N

    There’s new class here, .non-critical-img, to denote these as being “non critical” images—we’ll be using this in JavaScript later.

  5. Line P

    Notice how the src="…" attribute doesn’t exist any more, now it’s data-src="…", this is to prevent the image from downloading. It can’t download unless it has an src="…" attribute.

    It also has the hidden attribute so that it doesn’t accidentally get shown.

  6. Lines Q–S

    The <noscript> tag is here to force the image to be visible if JavaScript isn’t available.

    Inside the <noscript> tag is a standard <img src="…"> tag that will only get triggered if JavaScript is disabled in the browser.

  7. Line W

    We’ll start outputting the images with the fifth one.

  8. Line X

    Here we have a hidden attribute so this whole grid unit isn’t shown until the JavaScript executes.

  9. Line Z

    Again we’re using the data-src="…" attribute to prevent these images from loading.

3 Lazy load non-critical images with JavaScript

Now for a little titch of JavaScript to make the whole thing work together.

First make a new JS file: js/image-loader.js

We’re not going to use jQuery for this code because it will just slow our website down. There’s so little JavaScript that jQuery adds a massive, unnecessary overhead.

window.addEventListener('load', function (e) {
  var imgs = document.querySelectorAll('.non-critical-img');

  [].forEach.call(imgs, function (img) {
    var imgTag = img.querySelector('img');

    imgTag.src = imgTag.dataset.src;
    imgTag.removeAttribute('hidden');
    img.removeAttribute('hidden');
  });
});

Now we need to connect the JavaScript to our HTML file.

Instead of using the standard <script src="…"> tag, we’re going to embed the JavaScript right on the page. This will help mitigate the possibility of the external JavaScript file not downloading.

⋮
    {% endfor %}
  </div>

  <script>{% include_relative js/image-loader.js %}</script>
</body>
</html>

That’s it. Give it a try in your browser with JavaScript on & off and with the network speed throttle to see how it significantly improves performance.

  1. js/image-loader.js — Line A

    Wait for the website to finish loading before this JavaScript is triggered.

  2. js/image-loader.js — Line B

    Find all the elements on the page with the class .non-critical-img

  3. js/image-loader.js — Line D

    Start a loop, that will traverse over all those non-critical images that JavaScript found.

  4. js/image-loader.js — Line E

    Inside the “non-critical” element, find the <img> tag itself.

  5. js/image-loader.js — Line G

    Convert the data-src="…" attribute into a src="…" attribute so the image will start downloading.

  6. js/image-loader.js — Lines H–I

    Remove the hidden attributes from the <img> and the surrounding <div> tags.

  7. index.html — Line E

    We’ll use Jekyll’s include_relative function to read all the JavaScript from our file and output it into the page itself.