Skip to main content

Lazy loading images with Docusaurus

· 3 min read
John Reilly
OSS Engineer - TypeScript, Azure, React, Node.js, .NET

If you'd like to improve the performance of a Docusaurus website by implementing native lazy-loading of images, you can. This post shows you how you too can have <img loading="lazy" on your images by writing a Rehype plugin.

title image reading &quot;Lazy loading images with Docusaurus&quot; with a Docusaurus logo and an image that reads &lt;img loading=&quot;lazy&quot;

Updated 26/02/2022

You don't need this anymore. As of Docusaurus v2.0.0-beta.16 Docusaurus lazy loads markdown images by default. You can see the commit where it was added here. Isn't that wonderful?

✅cumulative no of network requests for Docusaurus sites will go 👇 ✅perceived performance will go ☝️ ✅hosting costs will go 👇

Lazy loading images

Native browser lazy loading for images is a relatively recent innovation. To read more on the topic, do look at this post. The TL;DR is this though: by adding the loading="lazy" attribute to an img element, modern browsers will delay loading the image until it is needed. This provides better performance to your users: when it comes to loading, less is more.


If you're using Docusaurus then you're likely writing Markdown. I am. This blog is written using Markdown, and converted, using MDX plugins into JSX. This handles images as well as we can see here:

jsxNode.value = `<img ${alt}src={${src}}${title}${width}${height} />`;

The crucial thing to note about the above, is the lack of the loading="lazy" attribute. Can we add that somehow? Yes we can!

Rehype plugin

To do this, we're going to write our own mini rehype plugin that will take the HTML being pumped out of Docusaurus and add the loading="lazy" attribute.

Alongside our docusaurus.config.js we're going to create a image-lazy-remark-plugin.js file:

const visit = require('unist-util-visit');

/** @type {import('unified').Plugin<[], import('hast').Root>} */
function lazyLoadImagesPlugin() {
return (tree) => {
visit(tree, ['element', 'jsx'], (node) => {
if (node.type === 'element' && node.tagName === 'img') {
// handles nodes like this:

// {
// type: 'element',
// tagName: 'img',
// properties: {
// src: '',
// alt: null
// },
// ...
// } = 'lazy';
} else if (node.type === 'jsx' && node.value.includes('<img ')) {
// handles nodes like this:

// {
// type: 'jsx',
// value: '<img src={require("!/workspaces/[name]-[hash].[ext]&fallback=/workspaces/!./bower-with-the-long-paths.png").default} width="640" height="497" />'
// }

node.value = node.value.replace(/<img /g, '<img loading="lazy" ');

module.exports = lazyLoadImagesPlugin;

As the code above suggests, it looks for img elements, whether they be in HTML or JSX, and adds in the loading="lazy" attribute.

To apply this to our blog, we simply tweak the docusaurus.config.js file to make use of our plugin:

const imageLazyRemarkPlugin = require('./image-lazy-remark-plugin');

// ...

/** @type {import('@docusaurus/types').Config} */
const config = {
// ...

presets: [
/** @type {import('@docusaurus/preset-classic').Options} */
// ...
blog: {
// ...
rehypePlugins: [imageLazyRemarkPlugin],
// ...
// ...

What's the result?

With this in place, next time we run a build, we can see the attribute being applied to our image elements:

screenshot of an img element with the loading=&quot;lazy&quot; attribute set

Consequently, when we fire up devtools we can see that only the images onscreen are being loaded. In the example below we're not seeing five other images being loaded because they're offscreen and haven't been scrolled to as yet:

screenshot of chrome devtools showing only two images being loaded - the ones that are on the screen

Amazing! It works! It's possible that this could land directly in Docusaurus one day. Go here to follow the discussion on this.