Webpack’s default caching mechanism is surprisingly naive, relying on simple file modification times, which means even a tiny change in one file can invalidate the entire cache for a large chunk of your application.

Let’s see how long-term caching with hashes saves the day. Imagine this webpack.config.js:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].[contenthash].js', // The magic happens here
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  // ... other configurations
};

Here, [name].[contenthash].js tells Webpack to generate a unique hash for each output file based on its content. If src/index.js changes, its contenthash will change, resulting in a new filename like main.a1b2c3d4e5f6.js. Crucially, if a dependency like lodash (which might be in node_modules and rarely changes) is bundled, its contenthash will remain the same, and its filename will not change. This means browsers, or any other cache, can safely cache lodash.a1b2c3d4e5f6.js indefinitely, only re-downloading it when its content actually changes.

The problem this solves is the "cache busting" issue. Without content hashing, when you update a single JavaScript file, the browser has no way of knowing that the new version is different from the old one it has cached, unless you manually change the filename (which is tedious and error-prone). This leads to users often running outdated code, causing bugs and a frustrating user experience.

Internally, Webpack calculates a hash (like SHA-256) for the content of each module as it’s processed. When modules are bundled into output chunks, the hash for the chunk is derived from the hashes of its constituent modules. This contenthash is then embedded directly into the output filename. This ensures that any change to the content of a module, however small, will alter its hash, and consequently the hash of any chunk it belongs to, leading to a new filename.

The primary lever you control is the output.filename template string. While [contenthash] is the most robust for long-term caching, you might also see [chunkhash] (which is similar but can vary slightly more between builds for the same content) or [hash] (which is a build-wide hash and less granular for caching). For long-term caching of individual assets, [contenthash] is the standard.

When you deploy assets with [contenthash] filenames, your index.html file (which is typically not hashed, or has a separate hash if you’re using HtmlWebpackPlugin with hashing enabled) will reference these uniquely named JavaScript and CSS files. For example:

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
  <link rel="stylesheet" href="styles.a1b2c3d4e5f6.css">
</head>
<body>
  <script src="main.a1b2c3d4e5f6.js"></script>
</body>
</html>

If you only change src/utils.js which is bundled into main.js, the new build might produce main.x7y8z9a0b1c2.js. Your index.html would then be updated (ideally automatically by a plugin) to reference this new file:

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
  <link rel="stylesheet" href="styles.a1b2c3d4e5f6.css"> <!-- This one is unchanged -->
</head>
<body>
  <script src="main.x7y8z9a0b1c2.js"></script> <!-- This one is new -->
</body>
</html>

The browser sees styles.a1b2c3d4e5f6.css as unchanged and keeps it cached. It sees main.x7y8z9a0b1c2.js as a new file and downloads it. This is efficient and reliable.

A common pitfall is not updating the index.html file itself to point to the new hashed assets. Tools like HtmlWebpackPlugin are essential here, as they automatically inject the correct, newly hashed filenames into your HTML. Without it, you’d have to manually manage the references, defeating the purpose of automated caching.

The next hurdle is managing the lifecycle of these hashed assets, specifically knowing when to remove old, unused files from your dist directory.

Want structured learning?

Take the full Webpack course →