preface
How to do persistent caching based on Webpack feels like there has never been a very good solution to practice. There are a lot of articles on the Internet, but there are very few that are really useful, and there are no articles that are really deeply researched and summarized. Now, relying on the online project of Early Education Treasure and my own practice, I have a complete plan.
The body of the
1. Two ways to compute Webpack hash
To do persistent caching, you rely on two hashes provided by WebPack itself: hash and chunkhash.
Let’s look at what these two values mean and what the difference is:
Hash: Webpack generates a Compilation object every time it builds, and this hash value is calculated from the contents of the compilation.
Chunkhash: This value is calculated based on the contents of each chunk.
So based solely on the above description, chunkhash is most effective for persistent caching.
2. Hash and chunkhash tests
entry | Entrance to the file | Entrance to rely on |
---|---|---|
pageA | a.js | a.less->a.css, common.js->common.css |
pageB | b.js | b.less->b.css, common.js->common.css |
- Using hash
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
entry: {
pageA: './src/a.js'.pageB: './src/b.js'
},
output: {
filename: '[name]-[hash].js'.path: path.resolve(__dirname, 'dist')},module: {
rules: [{test: /\.css$/.use: ExtractTextPlugin.extract({
fallback: 'style-loader'.use: ['css-loader? minimize']})}]},plugins: [new ExtractTextPlugin('[name]-[hash].css')]}Copy the code
The build results
Hash: 80c922b349f516e79fb5
Version: webpack 3.81.
Time: 1014ms
Asset Size Chunks Chunk Names
pageB- 80.c922b349f516e79fb5.js 2.86 kB 0 [emitted] pageB
pageA- 80.c922b349f516e79fb5.js 2.84 kB 1 [emitted] pageA
pageA- 80.c922b349f516e79fb5.css 21 bytes 1 [emitted] pageA
pageB- 80.c922b349f516e79fb5.css 21 bytes 0 [emitted] pageBCopy the code
conclusion
You can see that the hash of all the files is the same, but the hash of how many times you build it will be different. The reason is that we use the ExtractTextPlugin. The ExtractTextPlugin itself involves an asynchronous extraction process, so there is uncertainty (sequencing) when generating assets, and updateHash is sensitive to it. Therefore, the hash mutation described above occurs. In addition, the hash values of all assets remain the same, which has no profound significance for the persistent cache of all assets.
- Use chunkhash to calculate
“`js
const path = require(‘path’)
const ExtractTextPlugin = require(‘extract-text-webpack-plugin’)
module.exports = {
entry: {
pageA: ‘./src/a.js’,
pageB: ‘./src/b.js’
},
output: {
filename: ‘[name]-[chunkhash].js’,
path: path.resolve(__dirname, ‘dist’)
},
module: {
rules: [{ test: /\.css$/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: ['css-loader? minimize']})}Copy the code
]
},
plugins: [new ExtractTextPlugin(‘[name]-[chunkhash].css’)]
}
** Result ** ' 'js Hash: 810904F973CC0CF41992 Version: webpack 3.8.1 Time: 1038ms Asset Size Chunks Names pageB-eCurrently, 9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB pagea-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA pageA-3a2e5ef3d4506fce8d93.css 21 bytes 1 [emitted] pageA pageB-e9ed5150262ba39827d4.css 21 bytes 0 [emitted] pageBCopy the code
conclusion
Each entry has its own hash value. If you are careful, you may find that the hash value of the style resource is consistent with that of the entry script. This does not seem to conform to our idea, which tells us that something bad has happened.
3. Explore the relationship between the CSS file hash and the entry file hash
The hash value of the CSS is the same as the hash value of the entry file. Modify the contents of the B.css file to produce the build result:
Hash: 3d95035f096f3ca08761
Version: webpack 3.81.
Time: 1028ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA- 3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA- 3a2e5ef3d4506fce8d93.css 21 bytes 1 [emitted] pageA
pageB-e9ed5150262ba39827d4.css 41 bytes 0 [emitted] pageBCopy the code
Nani?? If you change the content of the CSS file, why does the HASH of the CSS file not change? Unscientific, the hash of the entry file has not changed. Come to think of it, Webpack treats everything as part of a JS file. In the process of building, the ExtractTextPlugin is used to extract styles out of the Entry chunk. At this time, the Entry chunk itself is not changed, but the CSS part that has been removed is changed. Chunkunhash is calculated based on chunk, so it should be normal not to change. But again, this doesn’t meet the requirements of what we want to do with persistent caching, because the change would be to change the hash.
Happily, the ExtractTextPlugin provides us with a Contenthash to change:
plugins: [new ExtractTextPlugin('[name]-[contenthash].css')]Copy the code
After modifying the two building results of B.C. SS:
Hash: 3d95035f096f3ca08761
Version: webpack 3.81.
Time: 1091ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA- 3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA- 9783744431577.cdcfea658734b7db20f.css 21 bytes 1 [emitted] pageA
pageB2 -d03aa12ae45c64dedd7f66bb88dd3db.css 41 bytes 0 [emitted] pageBCopy the code
Hash: 7a96bcf1ef668a49c9d8
Version: webpack 3.81.
Time: 1193ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA- 3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA- 9783744431577.cdcfea658734b7db20f.css 21 bytes 1 [emitted] pageA
pageB-7e05e00e24f795b674df5701f6a38bd9.css 42 bytes 0 [emitted] pageBCopy the code
The comparison shows that only the hash of the style file changes after the modification, which is what we want it to be.
4. Module ID uncontrollable and correction
After the above tests, we can take it for granted that I have achieved hash stabilization of the persistent cache. Then we accidentally delete the A.ess file in a.js and build it twice:
Hash: 88ab71080c53db9d9f70
Version: webpack 3.81.
Time: 1279ms
Asset Size Chunks Chunk Names
pageB-a2d1e1d73336f17e2dc4.js 3.82 kB 0 [emitted] pageB
pageA- 96.c9f5afea30e7e09628.js 3.8 kB 1 [emitted] pageA
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 1 [emitted] pageA
pageB- 56185455.ea60f01155a65497e9bf6c85.css 108 bytes 0 [emitted] pageBCopy the code
Hash: 172153ea2b39c2046a92
Version: webpack 3.81.
Time: 1260ms
Asset Size Chunks Chunk Names
pageB- 884.da67fe2322246ab28.js 3.81 kB 0 [emitted] pageB
pageA4 -c0dfb634722c556ffa0.js 3.68 kB 1 [emitted] pageA
pageA- 35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 1 [emitted] pageA
pageB- 56185455.ea60f01155a65497e9bf6c85.css 108 bytes 0 [emitted] pageBCopy the code
Something strange happened when I removed the A.ess file and found that the hash of the pageB entry file had changed. I would understand if only the file hash associated with pageA changed. But ???? Why has it all changed?? No, I need to see why everything’s changed.
Then we look at the information about the two building blocks:
[3] ./src/a.js 284 bytes {1} [built]
[4] ./src/a.less 41 bytes {1} [built]
[5] ./src/b.js 284 bytes {0} [built]
[6] ./src/b.less 41 bytes {0} [built]Copy the code
[3] ./src/a.js 264 bytes {1} [built]
[4] ./src/b.js 284 bytes {0} [built]
[5] ./src/b.less 41 bytes {0} [built]Copy the code
Through comparison, it is found that the previous sequence number hides pageA related information in the constructed pageB, which is very inconvenient for us to do persistent cache. We expect pageB to contain only information that is relevant to us and not other information that is not relevant to us.
5. Change the Module ID
Exclude irrelevant Module ids or content
People who use Webpack probably have one feature they all agree on: Code Splitting, which is essentially a process of Splitting and regrouping chunks. So how do you do that?
The answer is CommonsChunkPlugin, add to plugin:
plugins: [
new ExtractTextPlugin('[name]-[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'})]Copy the code
Remove a.less from pageA
Hash: 697b36118920d991364a
Version: webpack 3.81.
Time: 1488ms
Asset Size Chunks Chunk Names
pageB9 -b2eb6768499c911a728.js 491 bytes 0 [emitted] pageB
pageA-c342383ca09604e8e7b8.js 495 bytes 1 [emitted] pageA
runtime-b6ec3c0d350aef6cbf3e.js 6.8 kB 2 [emitted] runtime
pageA-b812cf5b72744af29181f642fe4dbf38.css 43 bytes 1 [emitted] pageA
pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css 59 bytes 0 [emitted] pageB
runtime- 35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] runtimeCopy the code
Hash: 7ddaf109d5aa67c43ce2
Version: webpack 3.81.
Time: 1793ms
Asset Size Chunks Chunk Names
pageB- 613.cc5a6a90adfb635f4.js 491 bytes 0 [emitted] pageB
pageA0b72f85fda69a9442076.js 375 bytes 1 [emitted] pageA
runtime-a41b8b8bfe7ec70fd058.js 6.79 kB 2 [emitted] runtime
pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css 59 bytes 0 [emitted] pageB
runtime- 35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] runtimeCopy the code
Then look at the pageB comparison between the two builds:
By comparison, we find that pageB contains only self-relevant content. So using the CommonsChunkPlugin met our expectations. The code pulled out is the runtime code for WebPack. The runtime code also stores webpack information about modules and chunks. In addition, we found that the file sizes of pageA and pageB also changed. The reason for this change is that by default the CommonsChunkPlugin extracts entry chunk modules into the normal chunk we call Runtime.
Let’s say we use some library for every page in development, like LoDash. Since the default behavior of the CommonsChunkPlugin is to extract the common parts, it is possible that LoDash has not changed, but is removed from the runtime code to request a new one each time. This does not meet the minimum renewal principle we require. So we’re going to have to manually interfere with some of the code.
plugins: [
new ExtractTextPlugin('[name]-[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'.minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})Copy the code
Logs built twice before and after the second opposite side:
Hash: a703a57c828ec32b24e1
Version: webpack 3.81.
Time: 1493ms
Asset Size Chunks Chunk Names
vendor-f11f58b8150930590a10.js 541 kB 0 [emitted] [big] vendor
pageB7 -d065cd319176f44c605.js 938 bytes 1 [emitted] pageB
pageA2 -b7e3707314e7ec4d770.js 910 bytes 2 [emitted] pageA
runtime-e68dec8bcad8a5870f0c.js 5.88 kB 3 [emitted] runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 2 [emitted] pageA
pageB- 56185455.ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageBCopy the code
Hash: 26fc9ad18554b28cd8e1
Version: webpack 3.81.
Time: 1806ms
Asset Size Chunks Chunk Names
vendor-d9bad56677b04b803651.js 541 kB 0 [emitted] [big] vendor
pageB-a55dadfbf25a45856d6a.js 929 bytes 1 [emitted] pageB
pageA7 -cbd77a502262ddcdd19.js 790 bytes 2 [emitted] pageA
runtime-fa8eba6e81ed41f50d6f.js 5.88 kB 3 [emitted] runtime
pageA- 35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] pageA
pageB- 56185455.ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageBCopy the code
So far we have solved the problem of excluding irrelevant Module IDS or content.
Stabilize the Module ID, keeping it as constant as possible
A Module ID is the unique identifier of a module and will appear in the code after the corresponding Entry Chunk is built. Look at an example of post-build code for pageB:
__webpack_require__(7)
const sum = __webpack_require__(0)
const _ = __webpack_require__(3)Copy the code
According to the previous experiment, the increase or decrease of modules will cause the change of Module ID. Therefore, in order not to cause the change of Module ID, we can only find something to replace the Module ID as an identifier. Alternate identifiers will be found to replace the Module ID during the build process.
So the above statement can be translated into two steps for action.
- Find a way to replace module ID
- Find an opportunity to replace the Module ID
6. Stabilize operations related to module ID
Find an alternative to the Module ID. In our daily development, we often refer to modules by address. Can we replace all module ids with paths? Finally, we learned that we can definitely get the resource path in the WebPack Resolve Module phase. At the beginning we were worried about platform path differences. Luckily the webpack source code has a difference fix for the module path in ContextModule#74 and ContextModule#35. This means that we can safely access the module’s path through the module’s libIdent.
There are three hooks involved in the Module ID throughout webpack execution:
before-module-ids
-> optimize-module-ids
-> after-optimize-module-ids
So we just need to make changes in before-module-IDS.
Write plug-ins:
'use strict'
class moduleIDsByFilePath {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin("before-module-ids", (modules) => {
modules.forEach((module) = > {
if(module.id === null && module.libIdent) {
module.id = module.libIdent({
context: this.options.context || compiler.options.context
})
}
})
})
})
}
}
module.exports = moduleIDsByFilePathCopy the code
The above is already a plugin for Webpack:
NamedModulesPluginCopy the code
So you just need to add it in the plugins section
new webpack.NamedModulesPlugin()Copy the code
Here’s how the file changes before and after the two builds:
Hash: e5bc78237ca9a3ad31f8
Version: webpack 3.81.
Time: 1508ms
Asset Size Chunks Chunk Names
vendor-ebd9bfc583f45a344630.js 541 kB 0 [emitted] [big] vendor
pageB- 432105.effc229524c683.js 1.09 kB 1 [emitted] pageB
pageA- 158.bf2a923c98ab49be2.js 1.09 kB 2 [emitted] pageA
runtime9 -ca4cebe90e444e723b9.js 5.88 kB 3 [emitted] runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 2 [emitted] pageA
pageB- 56185455.ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageBCopy the code
Hash: 7dce5d9dc88f619522fe
Version: webpack 3.81.
Time: 1422ms
Asset Size Chunks Chunk Names
vendor-ebd9bfc583f45a344630.js 541 kB 0 [emitted] [big] vendor
pageB- 432105.effc229524c683.js 1.09 kB 1 [emitted] pageB
pageA-dae883ddaeff861761da.js 940 bytes 2 [emitted] pageA
runtime-c874a0c304fa03493296.js 5.88 kB 3 [emitted] runtime
pageA- 35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] pageA
pageB- 56185455.ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageBCopy the code
Wow, we found that only the related files and runtime code were changed. Vendor and pageB were not changed. Delectable ~ ~
Now that we have achieved our goal, we can go and look at our built code:
__webpack_require__("./src/b.less")
const sum = __webpack_require__("./src/common.js")
const _ = __webpack_require__("./node_modules/lodash/lodash.js")Copy the code
It really became a path, success. However, a new problem seems to arise. Comparing our files with the previous ones, we found that our files are generally larger than the previous ones. Well, it was when we changed the file path. Can we use hash instead of file path at this point? The answer is yes, there are official plug-ins available for us to use:
new webpack.HashedModuleIdsPlugin()Copy the code
The NamedModulesPlugin is suitable for development environments, while HashedModuleIdsPlugin should be used in production environments. We’ve reached the point of using hash instead of the original Module ID to stabilize it. And the code doesn’t change that much after you build it.
I thought we were done here. But careful observers will notice that the Runtime file changes every time it is compiled. What causes it? Check it out:
- Find a way to stabilize the chunk ID
- Find the time to change the chunk ID
7. Stabilize the operations related to chunk IDS
Find a way to stabilize the chunk ID
Since we know that webpack has a unique entry when it is packaged, can we simply use the name corresponding to the entry? So it’s a little bit easier here to replace the chunk ID with our entry name.
Find the time to change the chunk ID
Based on experience, Module has the above process, so CHUNK also has it, I think.
before-chunk-ids
-> optimize-chunk-ids
-> after-optimize-chunk-ids
So write a plug-in:
'use strict'
class chunkIDsByFilePath {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('before-chunk-ids', chunks => {
chunks.forEach(chunk= > {
chunk.id = chunk.name
})
})
})
}
}
module.exports = chunkIDsByFilePathCopy the code
Unfortunately, this plugin is also available so we don’t have to write it.
NamedChunksPluginCopy the code
We can see it in the built code:
/ * * * * * * / script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"ed00d7222262ac99e510"."pageA":"b5b4e2893bce99fd5c57"."pageB":"34be879b3374ac9b2072"}[chunkId] + ".js";Copy the code
All the original chunk ids are now entry names, which reduces the risk of change. Delectable ~ ~
After we change the name then the problem above and the module id for the name and the same problem, the file will be big. At this point, I still want to hash the same way as above. This is when you really need to write a plug-in. Amway has a wave of our own webpack-hashed-chunk-id-plugin.
The core challenges encountered in this persistent cache have been dealt with.
The last
If you want to build a project quickly, welcome to use this project architecture. Webpack-project-seed is already wired for the project. Star, by the way.
Thanks: @ pigcan