Loading Images

How to tame the devDependencies of your project?

Image loading and processing can be a concern when developing sites and applications. The problem can be solved by pushing the images to a separate service that then takes care of optimizing them and provides an interface for consuming them.

For smaller scale usage, webpack is a good option as it can both consume and process images. Doing this comes with build overhead depending on the types of operations you are performing.

Starting from webpack 5, the tool supports asset modules. Earlier dealing with assets required using loaders such asurl-loader and file-loader but now the functionality is integrated to webpack. The following options are supported at a loader definition:

  • type: "asset/inline" emits your resources as base64 strings within the emitted assets. The process decreases the number of requests needed while growing the bundle size. The behavior corresponds with url-loader.
  • type: "asset/resource" matches the behavior of file-loader and emits resources as separate files while writing references to them.
  • type: "asset/source" matches raw-loader and returns full source of the matched resource.
  • type: "asset" is a mixture between asset/inline and asset/source and it will alter the behavior depending on the asset size. It's comparable to using the limit option of file-loader earlier.

output.assetModuleFilename field can be used to control where the assets are emitted. You could for example set it to [hash][ext][query] or include a directory to the path before these fragments.

Integrating images to the project#

The syntax above can be wrapped in a small helper that can be incorporated into the book project. Set up a function as below:

webpack.parts.js

exports.loadImages = ({ limit } = {}) => ({
  module: {
    rules: [
      {
        test: /\.(png|jpg)$/,
        type: "asset",
        parser: { dataUrlCondition: { maxSize: limit } },
      },
    ],
  },
});

To attach it to the configuration, adjust as follows:

webpack.config.js

const commonConfig = merge([
  ...
parts.loadImages({ limit: 15000 }),
]);

To test that the setup works, download an image or generate it (convert -size 100x100 gradient:blue logo.png) and refer to it from the project:

src/main.css

body {
  background: cornsilk;
background-image: url("./logo.png"); background-repeat: no-repeat; background-position: center;
}

The behavior changes depending on the limit you set. Below the limit, it should inline the image while above it should emit a separate asset and a path to it.

Using srcsets#

Modern browsers support srcset attribute that lets you define an image in different resolutions. The browser can then choose the one that fits the display the best. The main options are resize-image-loader, html-loader-srcset, and responsive-loader.

Optimizing images#

In case you want to compress your images, use image-webpack-loader, svgo-loader (SVG specific), or imagemin-webpack-plugin. This type of loader should be applied first to the data, so remember to place it as the last within use listing.

Compression is particularly valuable for production builds as it decreases the amount of bandwidth required to download your image assets and speed up your site or application as a result.

Loading SVGs#

Webpack allows a couple ways to load SVGs. However, the easiest way is to set type as follows:

const config = { test: /\.svg$/, type: "asset" };

Assuming you have set up your styling correctly, you can refer to your SVG files as below. The example SVG path below is relative to the CSS file:

.icon {
  background-image: url("../assets/icon.svg");
}

Consider also the following loaders:

  • svg-inline-loader goes a step further and eliminates unnecessary markup from your SVGs.
  • svg-sprite-loader can merge separate SVG files into a single sprite, making it potentially more efficient to load as you avoid request overhead. It supports raster images (.jpg, .png) as well.
  • svg-url-loader loads SVGs as UTF-8 encoded data urls. The result is smaller and faster to parse than base64.
  • @svgr/webpack exposes imported SVGs as React components to consume.

Loading images dynamically#

Webpack allows you to load images dynamically based on a condition. The techniques covered in the Code Splitting and Dynamic Loading chapters are enough for this purpose. Doing this can save bandwidth and load images only when you need them or preload them while you have time.

Loading sprites#

Spriting technique allows you to combine multiple smaller images into a single image. It has been used for games to describe animations and it's valuable for web development as well as you avoid request overhead.

webpack-spritesmith converts provided images into a sprite sheet and Sass/Less/Stylus mixins. You have to set up a SpritesmithPlugin, point it to target images, and set the name of the generated mixin. After that, your styling can pick it up:

@import "~sprite.sass";

.close-button {
  sprite($close);
}

.open-button {
  sprite($open);
}

Using placeholders#

image-trace-loader loads images and exposes the results as image/svg+xml URL encoded data. It can be used in conjunction with file-loader and url-loader for showing a placeholder while the actual image is being loaded.

lqip-loader implements a similar idea. Instead of tracing, it provides a blurred image instead of a traced one.

Referencing to images#

Webpack can pick up images from style sheets through @import and url() assuming css-loader has been configured. You can also refer to your images within the code. In this case, you have to import the files explicitly:

import src from "./avatar.png";

// Use the image in your code somehow now
const Profile = () => <img src={src} />;

Starting from webpack 5, it's possible to achieve the same as below:

const Profile = () => (
  <img src={new URL("./avatar.png", import.meta.url)} />
);

The benefit of using the URL interface is that it lets the code work without using a bundler.

It's also possible to set up dynamic imports as discussed in the Code Splitting chapter. Here's a small example:

const src = require(`./avatars/\${avatar}`);

Conclusion#

Webpack allows you to inline images within your bundles when needed. Figuring out proper inlining limits for your images requires experimentation. You have to balance between bundle sizes and the number of requests.

To recap:

  • Use loader type field to set asset loading behavior. It replaces file-loader and url-loader used before webpack 5.
  • You can find image optimization related loaders and plugins that allow you to tune their size further.
  • It's possible to generate sprite sheets out of smaller images to combine them into a single request.
  • Webpack allows you to load images dynamically based on a given condition.

You'll learn to load fonts using webpack in the next chapter.

Previous chapter
Loader Definitions
Next chapter
Loading Fonts

This book is available through Leanpub (digital), Amazon (paperback), and Kindle (digital). By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of webpack.

Need help?