In the modern front-end engineering, modularization has become the standard configuration of the front-end project organization file, the website will be pre-packaged and processed before the launch of relevant modules. However, there are many ways to package. What is the most elegant way to separate business code from dependent libraries, and what is the most efficient way to use caches? This article will share the pros and cons of each solution, the pitfalls and the final solution summarized by the front-end team of Ele. me.

As we all know, the load time of a site is always an important metric for a site. The length of page load time has a direct impact on the number of visitors to your site. Just think, you are reading this article, will have how much patience to wait for a web page leisurely open?

For the front end, common ways to shorten web page load times are:

  • Merge files to reduce the number of network requests.
  • Cache static files for up to a year, allowing browsers to read files directly from the cache.

To make the changes work, we also add a hash to the file name of each file based on the contents of the file. The hash changes every time the contents of the file change, so the browser downloads the updated file over the network, but unupdated files are still read from the cache, shortening load times.

Similarly, when developing a single-page application, we usually package the js code of the application into two files: one is used to store a third-party dependent library whose content rarely changes. This part of the code is usually relatively large; The other holds business logic code that changes more frequently, but is generally smaller than third-party dependencies. For the sake of description, we can call these two files vendor.js and app.js respectively.

With the optimization solution in place, it’s time to choose the packaging tool. There is no doubt that the most popular thing is Webpack. Webpack provides a straightforward configuration in the documentation for packaging the js code in the project into vendor.js and app.js files, and adding a hash generated from the file contents to each file name, as mentioned above:

const webpack = require('webpack')
module.exports = {
  entry: {
    vendor: ['jquery', 'other-lib'],
    app: './entry'
  },
  output: {
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    })
  ]
}
Copy the code

However, almost everyone who uses a similar configuration runs into a problem: Every time you change the business logic code, it causes vendor.js’s hash to change. This means that users still have to re-download Vendor.js, even if the code hasn’t changed.

The problem was pointed out to Webpack by people in the open source community, and it got a lot of people talking about it. There were a lot of solutions, but some people said they worked, some people said they didn’t work, and there was no official decision.

To get an accurate answer, we tried almost every scheme in the community. Next, this paper will introduce the various methods we tried in turn, and give effective solutions at the end of the article.

Use the webpack-md5-hash plugin

The community provided this plugin to replace webPack-generated chunkhash:

const webpack = require('webpack')
const WebpackMd5Hash = require('webpack-md5-hash')
module.exports = {
  entry: {
    vendor: ['jquery', 'other-lib'],
    app: './entry'
  },
  output: {
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new WebpackMd5Hash(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    })
  ]
}
Copy the code

It works by generating hashes from the content of the code before the module was packed, rather than from the content after it was packed, as Webpack does. After a brief test, after modifying the business code, it did ensure that vendor.js’s hash was not changed, so we were happy to use it in the official environment, but the site went blank after launch.

Later, we compared vendor.js generated by two compilations and found that the module ID in the code had changed, but since hash had not been updated, the browser directly read the old vendor.js file from the cache after the project went online. However, at this time, the id of module 41 referenced in the new version of app.js is actually 40 in the old version, so the wrong module was referenced, which caused an error and interrupted the operation of the code.

It wasn’t long before the question was raised in the community.

Remove the Webpack runtime code from Vendor.js

It was pointed out that Webpack’s CommonsChunkPlugin injects some runtime code into the first entry. Based on module dependencies, the first entry is of course Vendor.js. This runtime code contains the file name of the final compiled app.js, and the hash contained in the file name of app.js will change every time the business code is changed, so the content of Vendor.js containing this code will also change, which leads to its hash always changing. Therefore, we need to extract the runtime code from Vendor.js to prevent the vendor.js hash from being affected.

Additionally, we need to OccurenceOrderPlugin to sort the modules in a certain order to ensure that the module ID is the same every time the module ID is compiled, otherwise a change in the module ID will cause the file contents to change and affect the hash.

The final Webpack configuration looks like this:

const webpack = require('webpack') module.exports = { entry: { vendor: ['jquery', 'other-lib'], app: './entry' }, output: { filename: '[name].[chunkhash].js' }, plugins: [ new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor'}), / / pull away a Webpack runtime code new webpack.optimize.Com monsChunkPlugin ({name: 'the manifest, chunks: [' vendor']})]}Copy the code

This works, but we found that vendor.js’s hash was occasionally affected when modules were removed or added to the business code. The author of Webpack touched on this as well, which reads as follows:

By default, the module ID is the index of the module in the module array. OccurenceOrderPlugin will place the most frequently referenced modules first and the order of the modules will be the same at each compilation… If you add or remove modules when you modify the code, this will affect all module ids.

Therefore, this scheme does not completely guarantee that Vendor.js’s hash will not be affected by business code.

Use the NamedModulesPlugin

After trying the second solution, we realized that the root of the problem was that Webpack used the reference order of the modules as the ids of the modules, so that adding or removing modules could not avoid affecting the ids of other modules.

However, Webpack provides the NamedModulesPlugin, which uses the relative path of the module as the module ID, so as long as we don’t rename a module file, its ID doesn’t change and doesn’t affect other modules:

const webpack = require('webpack')
module.exports = {
  entry: {
    vendor: ['jquery', 'other-lib'],
    app: './entry'
  },
  output: {
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    })
  ]
}
Copy the code

However, the relative path is much longer than the numeric ID.

The community compared the size of the files with the plugin and concluded that the files were not much larger after gzip compression. However, after actual use in the project, although Vendor.js is only 1KB larger than before, app.js is nearly 15% larger.

So, we’re still not very happy with this solution.

4. Use DllPlugin

Is there a way to use numbers as module ids without having vendor.js and app.js hash each other? You might have to compile the third-party dependencies and business code separately.

Webpack provides the DllPlugin to help us do this, but it’s a bit cumbersome to use: you need to prepare two Webpack configurations.

One copy is used to compile Vendor.js:

const webpack = require('webpack')
module.exports = {
  entry: {
    vendor: ['jquery', 'other-lib']
  },
  output: {
    filename: '[name].[chunkhash].js',
    library: 'vendor_libs',
  },
  plugins: [
    new webpack.DllPlugin({
      name: 'vendor_libs',
      path: './vendor-manifest.json',
    })
  ]
}
Copy the code

The other is for compiling app.js:

const webpack = require('webpack')
module.exports = {
  entry: {
    app: './entry'
  },
  output: {
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.DllReferencePlugin({
      context: '.',
      manifest: require('./vendor-manifest.json')
    })
  ]
}
Copy the code

The plugin generates a mapping table of module relative path to module ID after compiling the third-party dependency library, and then imports this mapping table using the DllReferencePlugin when compiling the business code so that the business code can find the corresponding module in the third-party dependency. In this way, even if the module ID in app.js changes, it will not affect the module ID of vendor.js, and thus will not change the hash of vendor.js. However, it is important to note that if a module is added or removed from vendor.js, its internal module ID will change, so app.js should also change the module ID in vendor.js accordingly — that is, At this point, vendor.js will affect the hash of app.js. However, in most cases, the business code has also been changed when you upgrade a third-party dependent version, so this problem is minor.

We thought we had come up with the perfect solution — until we came across Webpack’s Code Splitting feature.

One of the pages in our project uses a large third-party dependency library, but it’s not worth packaging it in Vendor.js because not all users will open the page. Therefore, we used the Code Splitting function provided by Webpack to asynchronously load the module only when the user entered the page, which not only reduced the size of the file to be downloaded when the website was opened for the first time, but also ensured that the function was not affected.

However, the asynchronously loaded file’s hash will change when the business code is changed — and we can tell at a glance that the module’s numeric ID is the trick.

5. Final solution

After the above attempts, we concluded the following two conclusions:

  • You need to replace the default numeric module ID with a plug-in to avoid adding or removing modules that affect the ids of other modules, such as NamedModulesPlugin
  • The Webpack runtime code needs to be removed from Vendor.js to ensure that vendor.js hash is not affected.

At the same time, we noticed that the authors of Webpack introduced a new plug-in: HashedModuleIdsPlugin. The plugin generates a four-digit string for the module ID based on the relative path of the module, both hiding the module path information and reducing the length of the module ID. Although this plug-in is included with Webpack 2.x, it is also available directly in Webpack 1.x.

Const webpack = require('webpack') So we need to copy the file to our project const HashedModuleIdsPlugin = the require ('/path/to/HashedModuleIdsPlugin '). The module exports = {entry: { vendor: ['jquery', 'other-lib'], app: './entry' }, output: { filename: '[name].[chunkhash].js', chunkFilename: '[chunkhash].js' }, plugins: [ new HashedModuleIdsPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] }) ] }Copy the code

To ensure the feasibility of this solution, we tested various scenarios such as modifying, adding, or removing business code modules in a real project and compared the hashes of each compiled file. The results were as follows: Only app.js and manifest.js (the run-time code of the extracted Webpack) change the hash after each compilation, while vendor.js and asynchronously loaded files do not.

With this problem resolved, we can now safely use the Lazy Loading Routes feature provided by vue-Router. Imagine if you only changed the code in one of the asynchronously loaded routes, then only the asynchronously loaded file’s hash would change, so the next time the project went live, the browser would still read the other files directly from the cache, and the first load time would not be affected at all.

Finally, we provided a minimal sample project to demonstrate the solution, which you can use to adjust your Webpack configuration and check hash changes.