Rails has made it possible to enable browser built-in lazy-loading of images across your whole app.

Read on to learn about the feature and what to consider before using it.

The background

Until recently web browsers loaded all images on the page eagerly, even when hidden far off the page. That means lots of wasted bytes since the visitor downloads images they never end up seeing.

For supporting browsers, native lazy loading will only load images when they are about to scroll in to the viewport.

This progressive enhancement seems like a big win. Less wasted energy. 🌎❤️

Mathias Bynens’ demo app lets us test see the feature in action

How to setup app-wide lazy image loading in Rails

In Rails you can specify config.action_view.image_loading (in your app or environment specific config) with either lazy or eager.

Rails.application.configure do
  config.action_view.image_loading = "lazy"
end

Setting to "lazy" (or :lazy) will mean all HTML image tags generated by the Rails image tag view helper will have the loading="lazy" attribute. This will tell supporting browsers to lazy-load the images.

Equally setting to "eager" (or :eager) will mean image tags will have the loading="eager" attribute, i.e. the browser to use the current eager loading behaviour (which is the default loading strategy).

The Deep Dive: But why eager as default?

There are some concerns about having the default image loading to be “lazy”, both at browser level and in Rails.

Lets dive deeper…

1. Privacy concerns 🛡

First there are privacy/fingerprinting concerns for browser vendors.

Browser developers have a really tough time. It seems to be a constant fight to stay ahead of tracking attempts.

Eg, the HTML spec on lazy loaded resources suggests browser devs hide the true scroll speed of the user for this reason:

🤯

2. The dreaded Layout shift

Then there is layout shift, the annoying jumping of content on the page when something changes.

In this case, if the user is scrolling and an image is lazy-loaded, but isn’t ready by the time the user scrolls to its position, they may see the layout change when it finally does pop in.

The general recommendation to prevent layout shift is:

  • always ensure images have their width and height attributes specified on the HTML tag (or use CSS).

But it is often the case that these attributes have not been specified.

Setting action_view.image_loading to "lazy" will set the image to lazy load even if there are no dimensions specified.

From web.dev article on the topic:

Why care about Layout Shift (CLS)?

  1. layout shift is annoying for viewers,
  2. lots of layout shift may affect your search engine ranking from this year when CLS is added as a metric for user experience.

3. Lazy loading affects “preload”

The Chromium team recommends to not lazy-load images that will start off in the viewport (ie images high up on the page or “above the fold”) due to the loading attribute preventing “preloading” of images.

Addy Osmani has written up all about preload. Essentially the preload declaration tells the browser to make requests for resources without blocking the document’s onload event.

But as the browser scans for resources it might preload, it will skip any that are marked as loading="lazy".

This may affect the so called “time to interactive” of your page by disabling the preload optimisation on those images.

A higher TTI may affect user experience and ranking in the same way as CLS.

Why care about Time to Interactive (TTI)?

  1. waiting to interact with a page is annoying for viewers,
  2. long TTI may affect your search engine ranking from this year when TTI is added as a metric for user experience.

Conclusions

To enable the built-in browser lazy-loading, specify config.action_view.image_loading with the value "lazy". But note the caveats discussed above.

Personally I will be enabling the feature as soon as possible and evaluating CLS (Cumulative Layout Shift) and TTI (Time to Interactive) to check for any degredation in real life usage. User experiece and search rank is important, but our planet more so.

If you found this article useful, why not follow me on Twitter for more.

Extra notes

You can use native lazy-loading without the config

Of course you can set HTML attributes on images already by passing to image_tag:

<%= image_tag "pic.png", loading: "lazy" %>

The main point of the new config is to allow this to be set application wide.

What about browsers that don’t support native lazy-loading

Browsers which don’t support the loading attribute will safely just ignore it.

You can add a JavaScript polyfill if you wish, for example this polyfill which uses lazysizes by the web.dev team.

The lazy loading attribute is not image tag specific

It is actually defined as part of the “Fetching resources” section of the “Common infrastructure” part of the HTML specification. For example, apart from images it is also defined for use with iframes.

Browsers disable native lazy loading when JavaScript is disabled

If JavaScript is disabled by the user, lazy loading is too. The HTML spec tells us why:

The loading attribute is not enabled by default in Safari

While enabled already in Chrome and Firefox, the feature is still experimental in Safari, both on MacOS and iOS… hopefully it will follow suite and be enabled by default soon.

loading-lazy attribute disabled by default in Safari

loading-lazy attribute disabled by default in Safari

Whats next?

Synchronous / Asynchronous Image Decode support for image tags!