Fast Inline Images With React and Webpack

toddhgardner

Todd H. Gardner

Posted on June 7, 2022

Fast Inline Images With React and Webpack

Webpack is great for building React applications, but did you know it can help you optimize app performance too? Webpack can automatically inline image data, improving performance by reducing the number of requests your page needs to make. Let’s learn how.

Image Inlining

Normally, every image on a webpage is a unique file that the browser must make a HTTP request to display. The more requests the browser needs to make, the longer page loading will take. Conversely, reducing the total number of requests will generally improve performance.

Image inlining reduces the number of additional requests needed for page load by embedding image data directly into the HTML or Javascript bundle. As with anything, this optimization doesn’t come for free: The total number of image requests is reduced at the price of a larger initial payload. This results in a performance sweet spot where small images are inlined, but larger images are loaded normally with an additional HTTP requests.

Hey! Don’t want to read all the in’s and outs of bending Webpack into shape? Jump to the final webpack configuration.

A Simple React App

To test image inlining we’ve created a simple React app:

Files in a base React app
Basic React App Source Directory

A index.html file is used to bootstrap the single (compiled) JSX file:

<html>
    <header>
        <title>React With Inline Images</title>
    </header>
    <body>
        <div class="images-container"></div>
    </body>
    <script src="index.js"></script>
</html>
Enter fullscreen mode Exit fullscreen mode
import React from "react"
import ReactDOM from "react-dom/client"

// Just to convince webpack to copy the file
import indexHtml from "./index.html"

function SomeImages(props) {
    return (
        <div>
            <h2>{props.title}</h2>
            <p>
                <h3>Some small images:</h3>
                <img src="images/small-bee.png" />
                <img src="images/small-chick.png" />
                <img src="images/small-puppy.png" />
                <img src="images/small-tree.png" />
            </p>
            <p>
                <h3>Some larger images:</h3>
                <img src="images/medium-duckling.jpg" /><br />
                <img src="images/medium-squirrel.jpg" />
            </p>
        </div>
    )
}

var containerDiv = document.querySelector(".images-container");
var root = ReactDOM.createRoot(containerDiv);
root.render(SomeImages({ title: "React with Inline Images" }));
Enter fullscreen mode Exit fullscreen mode

Building React JSX Files with Webpack

First, Webpack and React dependencies need to be installed with NPM:

npm install react react-dom
npm install --save-dev webpack webpack-cli babel-loader @babel/preset-react
Enter fullscreen mode Exit fullscreen mode

Webpack doesn’t compile JSX out of the box. Adding a module rule to webpack.config.js tells Webpack to use Babel when compiling JSX files. There is an additional rule for copying our bootstrap html to the output folder. More on “asset modules” later:

var path = require("path");

module.exports = {
    mode: "development",
    entry: "./src/index.jsx",
    output: {
        filename: "index.js",
        path: path.resolve("dist/"),
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: "babel-loader",
                options: {
                    "presets": ["@babel/preset-react"]
                }
            },
            {
                test: /\.html$/i,
                type: "asset/resource",
                generator: {
                    filename: "[name][ext]"
                }
            }
        ]
    }
};
Enter fullscreen mode Exit fullscreen mode

Running webpack from the command line compiles our JSX into an output folder named dist/, but there are some issues that need to be fixed.

Importing/Requiring Images

Well, things ALMOST work. All our image tags are broken when we load the compiled app:

Webpack Images Not Working

And no images were output to our dist/ folder:

No images in Webpack output directory

Images aren’t displaying because Webpack doesn’t read the urls in src attributes. None of our image files are copied to the dist/ folder because Webpack assumed we are referencing an external dependency that it doesn’t need to worry about. The JSX needs to import or require the images so that Webpack knows we need those images:

// BEFORE:
<p>
    <h3>Some small images:</h3>
    <img src="images/small-bee.png" />
    <img src="images/small-chick.png" />
    <img src="images/small-puppy.png" />
    <img src="images/small-tree.png" />
</p>
<p>
    <h3>Some larger images:</h3>
    <img src="images/medium-duckling.jpg" /><br />
    <img src="images/medium-squirrel.jpg" />
</p>

// AFTER:
<p>
    <h3>Some small images:</h3>
    <img src={require("./images/small-bee.png")} />
    <img src={require("./images/small-chick.png")} />
    <img src={require("./images/small-puppy.png")} />
    <img src={require("./images/small-tree.png")} />
</p>
<p>
    <h3>Some larger images:</h3>
    <img src={require("./images/medium-duckling.jpg")} /><br />
    <img src={require("./images/medium-squirrel.jpg")} />
</p>
Enter fullscreen mode Exit fullscreen mode

Using Asset Modules For Image Files

And, things are still broken. Webpack knows about our images now, but is throwing errors:

ERROR in ./src/images/medium-duckling.jpg 1:0
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, /
    currently no loaders are configured to process this file. /
    See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)
 @ ./src/index.jsx 16:9-48
Enter fullscreen mode Exit fullscreen mode

Webpack is failing because it doesn’t know what to do with our image files. Just as with JSX, we need a module rule telling Webpack what to do when it encounters an image.

Webpack 5 has a new feature called Asset Modules which is meant to replace the url-loader, file-loader, and raw-loader’s used in Webpack 4 for this situation. Just to get things working, we’ll tell Webpack to always copy image files to the output folder:

// Added to webpack.config.js:
module: {
    rules: [
        // ...snip...
        {
            test: /\.(png|jpg)$/i,
            type: 'asset/resource'
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Finally, webpack is including images in the compiled output:

Webpack output directory with images

And our page is working:

React app with images

Place All Images in Their Own Folder

Webpack is copying our image files, but all the images are in the root of the output directory with un-intelligible hashes for names. Any more images and the dist/ folder is going to be a mess. We can tell the asset module to name our images better and place them in their own folder:

{
    test: /\.(png|jpg)$/i,
    type: 'asset/resource'
    // Added:
    generator: {
        filename: 'images/[name]-[hash][ext]'
    }
}
Enter fullscreen mode Exit fullscreen mode

Now images are all in a separate folder with understandable names. Keeping the hash helps with cache busting:

Webpack output directory with all images in their own folder

Automatically Clean Webpack’s Output Directory

Why is my dist directory so cluttered? As the webpack configuration has changed, we’ve manually cleaned up old files from the dist/ directory. By default, Webpack never removes old files that are no longer neeed. We can configure Webpack to automatically clean the dist folder each build:

output: {
    // ...snip...
    clean: true
},
Enter fullscreen mode Exit fullscreen mode

Inline Small Images

Finally, images are working and we can do what we came here for: inline small images! Webpack’s base asset Asset Module automatically handles inlining for us. Webpack will inline anything under 8KB by default, but we can also explicitly set the size threshold. Images over the limit will be output into the dist/ folder as they were previously:

module: {
        rules: [
            // ...snip...
            {
                test: /\.(png|jpg)$/i,
                // Previously we had disabled inlining by using 'asset/resource'
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 10 * 1024 // Inline images under 10KB
                    }
                },
                generator: {
                    filename: 'images/[name]-[hash][ext]'
                }
            }
        ]
    }
Enter fullscreen mode Exit fullscreen mode

The smaller images are inlined and the output directory only contains larger images:

Webpack output directory with only large images

We can see the Base64 encoded images if we inspect the rendered page:

Base64 images in Chrome Dev Tools inspector

TLDR: Final Webpack Configuration

Now we’ve got Webpack automatically inlining images along with a few quality of life improvements. With everything working, our webpack configuration looks like this:

var path = require("path");

module.exports = {
    mode: "development",
    entry: "./src/index.jsx",
    output: {
        filename: "index.js",
        path: path.resolve("dist/"),
        clean: true
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: "babel-loader",
                options: {
                    "presets": ["@babel/preset-react"]
                }
            },
            {
                test: /\.html$/i,
                type: "asset/resource",
                generator: {
                    filename: "[name][ext]"
                }
            },
            {
                test: /\.(png|jpg)$/i,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        maxSize: 10 * 1024 // Inline images under 10KB
                    }
                },
                generator: {
                    filename: 'images/[name]-[hash][ext]'
                }
            }
        ]
    }
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

We successfully convinced Webpack to automatically inline our images. This reduced the number of network requests our page needs to make, but did it make our page faster? That’s the kind of question Request Metrics was built to answer. Try it out today to measure how your site performs for real users in production.

We’ve covered just one way to optimize images here, but there are many other ways to optimize image performance.

💖 💪 🙅 🚩
toddhgardner
Todd H. Gardner

Posted on June 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related