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:
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>
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" }));
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
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]"
}
}
]
}
};
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:
And no images were output to our dist/
folder:
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>
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
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'
}
]
}
Finally, webpack is including images in the compiled output:
And our page is working:
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]'
}
}
Now images are all in a separate folder with understandable names. Keeping the hash helps with cache busting:
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
},
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]'
}
}
]
}
The smaller images are inlined and the output directory only contains larger images:
We can see the Base64 encoded images if we inspect the rendered page:
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]'
}
}
]
}
};
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.
Great work Todd 👌
static website forms