Build a predictable persistent cache scheme based on WebPack4 [.3+]

This article deals with the 'IMmutable Content + Long max-age' type of Web cache. The verification cache and service worker processing scheme will be updated later.Copy the code

The benefits of Web caching go far enough. Since webpack became a Predictable long term caching operation, configuration engineers have been busy with the task.

Before webpack4.3, there were quite a few articles on how to handle it (see resources), but I want to explore it a little more thoroughly.

The problem

When the business is developed and ready to go live, the problem comes 🤡 :

  1. How to ensure that resources with different contents have unique hash values?
  2. Will changing the business code and repackaging cause the identity values of all resources to change?
  3. If you want to stabilize hash values, how can you ensure that changing file names are kept to a minimum?
  4. Does the change of CSS/WASM resources affect the hash value of chunk?
  5. Does a change in the order of references in a business change the hash value of chunk? Should we?
  6. Are the dynamic import files well supported?
  7. Does adding or deleting multiple entry files affect existing hashes?

Don’t give up on treatment 🍷 Some versions of this article when tested:

Node. Js: v10.8.0 Webpack: v4.17.1Copy the code

TL; DR

  • After the [email protected]contenthashVery cool very comfortable 🌈
  • useHashedModuleIdsPluginStable moduleId. The plugin generates a four-digit hash as the module ID based on the relative path of the module. It is recommended for the production environment 🎁
  • useHashedModuleIdsPluginStable chunkId.
  • webpack@5 will have out-of-the-box persistent caching (officially envisioned as 😅, Webapck4 boasts zero configuration and still has plenty of advanced configuration engineers)

Resources that require long-term caching

  • Media resources, such as images and fonts, can use file-loader to generate hash values for media resources and use urL-loader to internalize them into Base64 format as required.

  • CSS If CSS resources are not processed, they are directly added to the JS file. In production environments we usually use mini-CSs-extract-plugin to extract in separate files or inline.

  • Js files are much more cumbersome to handle. As the only source of entry, JS manages other modules and introduces endless questions, which is what we will focus on next.

Webpack4 hash type

Hash type describe
hash The hash of the module identifier
chunkhash The hash of the chunk
Contenthash (WebPack > 4.3.0) The hash of the content(only)

Contenthash should be an important feature and the webpack core developers think it can completely replace chunkhash (see issue#2096) and will probably change contenthash to [hash] in webpack5.

So what’s the difference?

To put it simply, when chunk contains CSS and WASM, if CSS changes, chunkhash will also change, resulting in the change of the hash value of chunk. If contenthash is used, the CSS changes do not affect the chunk hash because it is generated from the CHUNK’s JS content.

It is enough to know that there are several, so let’s start with the most basic examples 🚴♂️.

The chestnuts

This will be tested in Production Mode (check out the WebPack Mode documentation if you don’t know the new mode for Webpack 4).

The unpacking strategy involved will be briefly discussed, and we will talk about unpacking in detail later

1. Simple hash

The simplest configuration file is 👇,

// webapck.config.js
const path = require('path'); 
const webpack = require('webpack'); 
module.exports = { 
    mode:'production'.entry: { 
        index: './src/index.js',},output: { 
        path: path.join(__dirname, 'dist'), 
        filename: '[name].[hash].js',}};Copy the code

The entry file index.js is simple:

// index.js
console.log('hello webapck 🐸')
Copy the code

Packing results:

This example uses name + hash to name the file, because the hash is generated by the Module Identifier, which means that the hash value will change whenever there is a small change in the business, as shown in the following example.

2. Add a vendor

Let’s add a little complexity.

@greyhound showed an interesting example of Webpack’s hash stability in our initial exploration, so let’s try it out.

Now let’s add an A.js module to the entry file:

// index.js
import './a';
console.log('hello webpack 🐸');
Copy the code

Module A introduces the identity method in LoDash:

// a.js
import {identity} from 'lodash';
identity();
Copy the code

Then modify the Webpack configuration file to extract the vendors file and the MANIFEST. As an additional note, runtimeChunk is very small and does not expect to change much in size, so consider inlining HTML.

// webapck.config.js. module.exports = { ...// Unpack the chunks using the splitChunks default policy and extract the Runtime
   optimization: {
        runtimeChunk: true.splitChunks: {
            chunks: 'all'}}};Copy the code

Packing results:

[hash]

As you’ve probably noticed, all files have the same hash value when packaged. What does that mean?

With each iteration, the client needs to re-receive the static resource because the hash value changes each time and all previous caches are invalidated 😬.

So, if we want to do persistent caching, we definitely won’t use [hash].

3. Chunkhash?

Prior to webpack4.3, chunkhash was the only option for module identification, but if it wasn’t very stable, configuration engineers went through a lot of hacks to make hash values as stable as possible.

How different is the new Contenthash from Chunkhash 😳?

Let’s look at a couple of examples.

Using chunkhash

Let’s change [hash] to [chunkhash] and see the packing result:

Indexes, vendors, and Vendors Runtime all have different hashes, so far so Good.

Let’s continue with the big example and add the b.js module to index.js. The b module has only one line of code:

// index.js
import './b';  // Add b.js
import './a';

console.log('hello webpack 🐸');
Copy the code
// b.js
console.log('no can no bb');
Copy the code

Packing results:

The hashes of the index files changed as expected, but the vendors essence was still the Identity method of the Lodash package, and this, too, was changing.

The reason is that webpack4 by default uses the increment ID for module identification in accordance with the considerations Order, so the insertion of B. js causes the vendors ID to be mistaken by a number. We can see this from the two vendors files, and the only difference between the two files is as follows:

HashedModuleIdsPlugin is a built-in plugin that generates the module ID based on the module path, and the problem is solved:

(At first, I was worried that the module path would be hashed and named according to the method. After all, I have already suffered a loss once. I have seen the problem of inconsistent path path in Windows/Linux. Don’t worry about it)

// webpack.config.js. Plugins:new webpack.HashedModuleIdsPlugin({
        // Replace base64 to reduce the drop time
        hashDigest: 'hex'}),]...Copy the code

(Setting optimization.moduleids :’hash’ can achieve the same effect, but above [email protected])

Packing results:

// With module B:
        index.a169ecea96a59afbb472.js  243 bytes       0  [emitted]  index
vendors~index6.b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.ec8eb4cb2ebdc83c76ed.js   1.42 KiB       2  [emitted]  runtime~index

// Without b module:
        index8296.fb0301ada4a021b1.js  185 bytes       0  [emitted]  index
vendors~index6.b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.ec8eb4cb2ebdc83c76ed.js   1.42 KiB       2  [emitted]  runtime~index
Copy the code

4. Add a CSS module

C ss👇, the content of c is not important:

// index.js
import './c.css';
import './b';
import './a'; .Copy the code

Configure the mini-CSS-extract-plugin to extract the CSS module:

// webpack.config.js. module: {rules: [{test: /\.css$/.include: [
                    path.resolve(__dirname, 'src')].use: [{loader: MiniCssExtractPlugin.loader},
                    {loader: 'css-loader'}]}]}, plugins: [new webpack.HashedModuleIdsPlugin(),
    // Add CSS extraction
    new MiniCssExtractPlugin({
        filename: '[name].[contenthash].css'.chunkFilename: '[name].[contenthash].css'})].Copy the code

Then pack. Change the contents of C. CSS a little and package again.

During these two packaging processes, we only changed the C.CSS file. What is expected? Hopefully, only the CSS file’s hash value will change, but things don’t go as expected:

// add c.css
                                Asset       Size  Chunks             Chunk Names
       index90.d7b62bebabc8f078cd.css   59 bytes       0  [emitted]  index
        index.e5d6f6e2219665941029.js  276 bytes       0  [emitted]  index
vendors~index6.b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.de3e5c92fb3035ae4940.js   1.42 KiB       2  [emitted]  runtime~index

// change the code in c. CSS
                                Asset       Size  Chunks             Chunk Names
       index22.b9c488a93511dc43ba.css   94 bytes       0  [emitted]  index
        index704.b09118c28427d4e8f.js  276 bytes       0  [emitted]  index
vendors~index6.b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.de3e5c92fb3035ae4940.js   1.42 KiB       2  [emitted]  runtime~index
Copy the code

Note that the index. Js hash value 📌 after packaging, the entry file hash value actually changed, which is very annoying.

5. Contenthash cures everything?

Contenthash does not solve the problem of the moduleId increment

What is the difference between the behavior of the vendors files above, using Contenthash and Chunkhash? Can you solve the problem of module change?

The answer is no 😅. The HashedModuleIdsPlugin plugin is needed, after all, because the file content contains changes.

Contenthash’s power

Contenthash can solve the problem of CHANGING the JS hash value after CSS module modification.

Modify the configuration file 👇 :

. output: {path: path.resolve(__dirname, './dist'),
        / / to contenthash
        filename: '[name].[contenthash].js'},...Copy the code

To get straight to the comparison:

// add c.css
                                Asset       Size  Chunks             Chunk Names
       index22.b9c488a93511dc43ba.css   94 bytes       0  [emitted]  index
        index.41e5e160a222e08ed18d.js  276 bytes       0  [emitted]  index
vendors~index.ec19a3033220507df6ac.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.d25723c2af2e039a9728.js   1.42 KiB       2  [emitted]  runtime~index

// change the code in c. CSS
                                Asset       Size  Chunks             Chunk Names
       index.a4afb491e06f1bb91750.css   60 bytes       0  [emitted]  index
        index.41e5e160a222e08ed18d.js  276 bytes       0  [emitted]  index
vendors~index.ec19a3033220507df6ac.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.d25723c2af2e039a9728.js   1.42 KiB       2  [emitted]  runtime~index
Copy the code

As you can see, the chunk hash of index.js is exactly the same as 💯 before and after the change.

6. Add asynchronous modules

In order to optimize the performance of the first screen or when the business becomes bloated, it is inevitable to extract and load some asynchronous modules, and it is comfortable to use dynamic import.

However, what is the hash value of an asynchronous module as a new chunk?

Let’s try adding an asynchronous module.

// webpack.config.js. output: {path: path.resolve(__dirname, './dist'),
        filename: '[name].[contenthash].js'./ / chunkFilename increase
        chunkFilename: '[name].[contenthash].js'},...Copy the code
// async-module.js
export default {
    content: 'async-module'
};


// index.js
import './c.css';
import './b';
import './a';
// Add this module
import('./async-module').then(a= > console.log(a));

console.log('hello webpack 🐸');
Copy the code

The contents of async-Module are also not important, the important thing is that the hashes of the async-Module have changed a lot before and after adding the module! No asynchronous modules:

Add asynchronous module:

Add a second asynchronous module:

The comparison above is like a night back to pre-liberation… Everything has changed except the CSS file’s hash value.

The reason is that although we stabilized the moduleId, we could not do anything about chunkId, and the asynchronous module was named with increment because it did not have chunk.name.

Fortunately we also have NamedChunksPlugin for chunkId stabilization 👇 :

// webapck.config.js. plugin:{new webpack.NamedChunksPlugin(
            chunk= > chunk.name || Array.from(chunk.modulesIterable, m => m.id).join("_")),... }...Copy the code

There are other ways to stabilize chunkId, but I won’t go into detail here due to more or less drawbacks, so let’s look at the result of packing now:

As you can see, the asynchronous modules also have name values, and the vendors’ hashes revert as well.

7. Add a second entry file

During business iterations, pages are often added or deleted. How does the hash value change in such scenarios?

// webpack.config.js. entry: {index: './src/index.js'.index2: './src/index2.js'},...Copy the code

Console. log(‘ I am index2~’) to see the result of packing:

The reason for this is that we have stabilized ChunkId and the chunks are no longer numeric increment based on the considerations Order.

In a real production environment, when a new chunk is introduced that relies on other common modules, the hash value of some files can still change, but this can be addressed by a unpacking strategy, which I won’t go into here.

conclusion

Through some examples, this paper summarizes the principle and practice of webpack4 to do long-term caching, and these have been used in our actual business, for frequent iteration of the business, there is a considerable performance improvement.

Webpack4’s long-acting cache is a big improvement over the previous version and has some shortcomings, but we believe that these will be solved in webapck5 🙆♀️~

reference

  • An initial exploration of Webpack’s hash stability
  • Github.com/pigcan/blog…
  • Using webpack4 with proper posture
  • Medium.com/webpack/pre…
  • Github.com/webpack/web…
  • Webpack.js.org/guides/cach…