Webpack’s image optimization is less about magic and more about making trade-offs between file size and quality, with the goal of either reducing transfer time or avoiding network requests altogether.

Here’s how you can make Webpack compress and inline your images:

Let’s say you have a small icon, logo.png, that you want to include in your application.

// src/index.js
import logo from './logo.png';

const img = document.createElement('img');
img.src = logo;
document.body.appendChild(img);

To get Webpack to process this, you’ll need file-loader (or asset/resource in Webpack 5+) and image-webpack-loader for compression. For inlining, url-loader (or asset/inline in Webpack 5+) is key.

Here’s a Webpack configuration snippet to achieve this:

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192, // Inline images smaller than 8KB
              name: 'assets/images/[name].[hash].[ext]',
            },
          },
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                quality: 75, // Compress JPEG images to 75% quality
              },
              optipng: {
                optimizationLevel: 5, // Optimize PNGs with level 5
              },
              pngquant: {
                quality: [0.65, 0.90], // Quantize PNGs to a quality range
              },
              gifsicle: {
                interlaced: false,
              },
            },
          },
        ],
      },
    ],
  },
};

When Webpack builds this, url-loader will check the limit. If logo.png is less than 8192 bytes (8KB), it will be Base64 encoded and inlined directly into your bundle.js. If it’s larger, url-loader will fall back to file-loader behavior, outputting a separate file to dist/assets/images/logo.[hash].png and referencing it. image-webpack-loader runs before url-loader (due to the order in the use array, it’s processed from bottom to top), compressing the image before it’s potentially inlined or copied.

The limit option in url-loader is your primary lever for deciding when to inline. A common value is 8192 (8KB), but you might adjust this based on your project’s needs and the typical size of your assets. Inlining small assets reduces the number of HTTP requests, which can significantly speed up initial page load, especially on mobile networks. However, inlining too many or too large assets can bloat your JavaScript bundle, leading to longer parse times.

image-webpack-loader uses various plugins to optimize images. For JPEGs, mozjpeg with a quality setting between 0-100 is common. For PNGs, optipng offers different optimization levels (0-7), and pngquant allows for color quantization, which can drastically reduce file size at the cost of some color fidelity. You can tailor these options to find the sweet spot between file size and visual quality for your specific use case.

The most counterintuitive part of this setup is that image-webpack-loader is typically placed after url-loader (or asset/inline) in the use array, meaning it gets executed first. This is because Webpack processes loaders from bottom to top. So, the image is compressed before url-loader decides whether to inline it based on its (now potentially smaller) size or emit it as a separate file.

The next hurdle you’ll likely face is handling different image types and ensuring consistent optimization across all of them, especially when dealing with SVGs, which often require different tooling.

Want structured learning?

Take the full Webpack course →