Author: Wang Nan

Summary: Webpack is the most popular front-end resource modular management and packaging tool. How to improve packaging efficiency has become one of the points of concern. Below is a list of practical optimizations we developed to go live.

At present, the most popular packaging tool is Webpack, about the optimization of Webpack, there are many articles on the Internet for your reference. Refer to the previous data, summarize their own problems encountered in the project, and finally come to some practical optimization schemes, this article will share with you one by one. Since it’s package optimization, we need to keep the following points in mind:

  • Reduced compile time
  • Reduce the compile output file size
  • Improve page performance

Scope promotion in Webpackage 3.0

One of the biggest new features of Webpackage 3.0 is scopepromotion. Webpack used to bundle every module (every code that was imported or required) into a separate closure function. This will result in special closures around each module in the bundle’s files, resulting in larger files. This will also make compiled JS files less efficient to execute in the browser.

Although the theoretical knowledge has been understood, but we still use the actual operation to verify:

Open ScopeHoisting is very simple, because webpack has built-in this functionality, at the entrance of plug-ins add new webpack. Optimize. ModuleConcatenationPlugin ().

Plugins: [new webpack. Optimize. ModuleConcatenationPlugin ()] we simply write two files app. Js and timer. Js. import timer from './timer' var time = (new Date()).getTime(); timer(); console.log('hello webpack'+time); export default function bar(){ console.log('pig'); } var path = require('path'); var webpack = require('webpack'); var config = { entry:{ app:'./src/app.js' }, output:{ filename:'bundle.js', path: path.resolve(__dirname,'./build') }, plugins:[ //new webpack.optimize.ModuleConcatenationPlugin() ] } module.exports = config;Copy the code

After compiling, the result is as follows: bundle.js is 3.03KB in size

We put the new webpack. Optimize. ModuleConcatenationPlugin open (), compile the results are as follows:

In bundle.js, Webpack2.0 has more code than Webpack3.0:

(function(module, __webpack_exports__, __webpack_require__) {
...
});Copy the code

Timer.js and app.js are both in one function; timer.js is not compiled in a closure function. A module has one less closure function, so many references can be much less. You can really see a marked reduction in volume. In addition to this effect, this built-in optimization makes compiled code execute significantly more efficiently in the browser. In the article of Aaron Hardy’s Optimizing Javascript Through Scope [1], it is shown that in a real Tubine[2] project, The author compares the package size and JS execution efficiency in the browser using Scopereactive and not using it. Turbine reduced the size of gzip files compressed by about 41% and improved initialization execution time by about 12%. Is it exciting to see this? If it can be used in our project, it will be perfect. Unfortunately, reality is harsh, and here’s a look at its limitations. I changed the reference module of the demo example to CommonJs syntax, and the execution effect is as follows:

//import bar from './timer'
var bar = require('./timer');
var time = (new Date()).getTime();
bar();
console.log('hello webpack'+time);
// export default function bar(){
// console.log('pig');
// }
exports.bar = function(){
console.log('big');
}Copy the code

You will notice that compiling the packaged bundle does not change when scopereactive is enabled using CommonJS module syntax. Because currently webpack3.0 only supports ESModule module syntax. Imagine if you were in your own scaffolding and most of the NPM dependencies were still CommonJS syntax, Webpack would fall back to the original packaging mode. You can use –display-optimization-bailout to check the cause of a downgrade when performing an upgrade.

In addition to the syntax currently supporting ESModlue, you may have the following in your old code:

  • Use the ProvidePlugin [3]
  • The Eva () function is used
  • A project has multiple entries

As far as the front-end ecological environment is concerned, the scopereactive new feature of Webpackage 3.0 can not be used temporarily, but ESModule is the trend. In the future, the way of reference of modules will be replaced by ESModule.

The use of CommonsChunkPlugin

Webpackage 3.0 scopereactive can not be used in actual projects, so let’s take a look at the use of CommonsChunkPlugin. The CommonChunkPlugin is an optional feature for creating a separate file (aka chunk), This file contains a common module for multiple entry chunks (CommonsChunkPlugin has been removed from WebPack V4 LEGato, and to see how chunks are handled in the latest version, see SplitChunksPlugin [4]. The CommonsChunkPlugin optimization idea is that by removing the common modules, the resulting file can be loaded once at the beginning, so that the rest of the page can be accessed directly and the common code in the browser cache can be used, which will no doubt make the experience better. Theoretical knowledge, so we start to try, is not good effect. Let’s build a simple Vue scaffolding based on Webpack2.7.0:

const path = require('path');
const webpack = require('webpack');
const configw = require('./package.json');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader')
var config = {
entry:{
app:'./src/app.js'
},
output:{
path: path.resolve(__dirname, 'build'),
publicPath: configw.publicPath + '/',
filename: 'js/[name].[chunkhash].js'
},
plugins:[
new CleanWebpackPlugin('build'),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new ExtractTextPlugin({
filename: 'css/app.css'
}),
+ new webpack.optimize.CommonsChunkPlugin({
+ name:'vender',
+ minChunks: function(module) {
+ return (
+ module.resource &&
+ /\.js$/.test(module.resource) &&
+ module.resource.indexOf(
+ path.join(__dirname, './node_modules')
+ ) === 0
+ )
+ }
+
+ }),
new VueLoaderPlugin(),
],
module:{
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['css-loader']
}),
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader']
})
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
presets: ['env']
}
}
]
}
}
module.exports = config;Copy the code

For ease of view, there are no compression plugins or optimizations introduced. Currently, third-party public libraries such as Vue, vue-Router, axios, etc. that depend on node_modules in business JS have been pulled out of app.js and packaged in vender.js. In order to achieve the separation of business JS and third-party library JS, it is the first step to achieve the browser side cache js that will not be updated frequently. Unfortunately, if we change the business code, such as app.js, app.vue, index.vue, etc., you will find that in addition to app.js, the hash of vender.js is changed.

If the hash value changes, the contents of the file have changed. This will definitely frustrate you, as Webpack’s ability to build persistent caching JS is almost impossible. If you don’t change the dependency file, why only change the business js and vender.js? Because each build, Webpack generates Webpack Runtime code to help Webpack do its job, such as linking the loading and parsing logic required by modules when they interact. As shown in the figure below, in our scaffolding, the compiled result is compared with only one run-time hash value.

So since the difference is so small, let’s pull the Runtime out. This will enable persistent caching of vender.js. Let’s try it out.

new webpack.optimize.CommonsChunkPlugin({
name:'vender',
minChunks: function(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, './node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name:'manifest',
minChunks:Infinity
})Copy the code

Modified the following code in app.vue, and the compiled results are as follows:

The hash value of vender.js has not changed as expected. Because the code that changes in the file has been pulled out of the manifest file. The manifest stores the chunks map, and with the chunks map, we know the real address of the chunk to load. Then every time you modify the business JS, you don’t need to deploy vender.js. This brings us to the third dependency library that implements persistent client and server caching. Smaller app.js and manifest.js are deployed with each live update. Note, however, that the manifest must be loaded first. Use this deployment strategy directly in production. No, not yet. Our goal is to implement persistent caching of third-party libraries without risking our going live. To ensure that you do not launch third-party libraries, users directly access the third-party libraries in the local browser cache, and js services can still run normally even if they are online. CommonsChunkPlugin this kind of packaging method is compiled at runtime code, if we add or remove dependencies in business code, imagine that after your project adopts this method, you may optimize the project or add functional modules, the module-dependent part of the business JS code will inevitably change. Let’s compare the results of keeping a dependency in index.vue with those of removing it:

It was obvious that I had changed the module dependencies in the business code, causing the vender.js library to change as well, which was unavoidable. Because vender.js and app.js are tightly coupled, you can pull runtime code out of the manifest. The removal and addition of imported modules causes the id dependencies of modules compiled at run time to change. I compared the differences between the two vender.js, as shown below, mainly because the referenced module ID has changed.

Seeing this, the CommonsChunkPlugin solves the problem, but the risk of going online is unavoidable. It is not worth doing so in order to take advantage of the browser cache and thus only line app.js files. It’s hard to guarantee a trouble-free launch. The main problem is that we still commit vender.js after building the commit test. Of course, some people would say that you can look at the hash value to know whether you need to live verder.js. However, the scenarios I mentioned above are too common to change in business code to justify a second vender.js update. The final solution is to use the DllPlugin, see below.

DllPlugin and DllReferencePlugin

While the two Webpacks package the plug-in, Dllplugin packages a DLL file and a mapping file referenced by the manifest.json module. DLL file to put what, is our third party library dependencies. It’s like a dynamic link library for Windows. The idea behind the Dllplugin is to package the common third-party libraries in our project into a DLL file. It is static unless you manually modify the libraries you need to introduce in your project. A manifest.json mapping file is also compiled. It is also static, which stores the library JS found by mapping the id value to the DLL file. The DllReferencePlugin packages the mapping values into our business JS. This completely removes third-party dependencies in advance. After that, only the business part of the code will be packaged and compiled, and there is no need to repeatedly build third-party library JS. Build compilation time is greatly reduced.

Let’s prove it in practice:

First we configure a config to generate the DLL.

const path = require("path"); const webpack = require("webpack"); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const config = require('./package.json'); const curDate = new Date(); const curTime = curDate.getFullYear() + '/' + (curDate.getMonth() + 1) + '/' + curDate.getDate() + ' ' + curDate.getHours() + ':' + curDate.getMinutes() + ':' + curDate.getSeconds() const bannerTxt = config.name + ' ' + config.version + ' ' + curTime; Module. exports = {// Array of modules you want to package :{vendor:['vue','axios','vue-router','qs']}, output:{ path:path.join(__dirname,'/static/'), filename:'[name].dll.js', Library :'[name]_library' // vendor.dl.js is used for name in DllPlugin // so we need to use 'name 'in webpack.dllplugin Plugins :[+ new webpack.dllplugin ({+ path:path.join(__dirname,'.','[name]-manifest.json'), + name:'[name]_library', + context:__dirname + }), new UglifyJsPlugin({ cache:true, sourceMap:false, parallel:4, uglifyOptions: { ecma:8, warnings:false, compress:{ drop_console:true, }, output:{ comments:false, beautify:false, } } }), new webpack.BannerPlugin(bannerTxt) ] }Copy the code

Entry configures the common Vue family bucket series. Since almost every page needs them, it’s nice to put them in the public vender.js. Let’s look at the results. I configured the NPM script execution code NPM run DLL:

"scripts": {
"dev": "webpack-dev-server -d --open --progress",
"build": "cross-env NODE_ENV=production webpack --hide-modules --progress",
"upload": "cross-env NODE_ENV=upload webpack --hide-modules --progress",
"dll": "webpack --config ./webpack.dll.config.js"
}Copy the code

The compressed DLL. Js size is acceptable. Let’s take a look at what’s stored in the generated manifest.json.

As expected, it stores the reference mapping path and the corresponding ID value. DLL. Js and manifest.json need to be compiled only once. After that, we don’t need to compile the vender.dll. Js package again for business code development and online packaging. Let’s take a look at how webpack.config.js is configured.

const webpack = require('webpack'); const config = require('./package.json'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const autoprefixer = require('autoprefixer'); const htmlwebpackincludeassetsplugin = require('html-webpack-include-assets-plugin'); const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); const webpackConfig = module.exports = {}; const isProduction = process.env.NODE_ENV === 'production'; const isUpload = process.env.NODE_ENV === 'upload'; const curDate = new Date(); const curTime = curDate.getFullYear() + '/' + (curDate.getMonth() + 1) + '/' + curDate.getDate() + ' ' + curDate.getHours() + ':' + curDate.getMinutes() + ':' + curDate.getSeconds(); const bannerTxt = config.name + ' ' + config.version + ' ' + curTime; Webpackconfig.entry = {app: './ SRC /app.js',}; webpackConfig.output = { path: path.resolve(__dirname, 'build' + '/' + config.version), publicPath: config.publicPath + '/'+config.version+'/', filename: 'js/[name].js' }; webpackConfig.module = { rules: [{ test: /\.css$/, use: ExtractTextPlugin.extract({ fallback: "style-loader", use: ['css-loader', 'postcss-loader'] }), }, { test: /\.scss$/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: ['css-loader', 'sass-loader', 'postcss-loader'] }) }, { test: /\.vue$/, loader: 'vue-loader', options: { extractCSS: true, postcss: [require('autoprefixer')()] } }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, }, { test: /\.(png|jpg|gif|webp)$/, loader: 'url-loader', options: { limit: 3000, name: 'img/[name].[ext]', } }, ] }; webpackConfig.plugins = [ new webpack.optimize.ModuleConcatenationPlugin(), new CleanWebpackPlugin('build'), new HtmlWebpackPlugin({ template: './src/index.html' }), new ExtractTextPlugin({ filename: 'css/app.css' }), new CopyWebpackPlugin([ { from: path.join(__dirname, "./static/"), to: path.join(__dirname, "./build/lib") } ]), + new webpack.DllReferencePlugin({ + context:__dirname, + manifest:require('./vendor-manifest.json') + }) ]; if (isProduction || isUpload) { webpackConfig.plugins = (webpackConfig.plugins || []).concat([ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), new webpack.LoaderOptionsPlugin({ minimize: true }), new UglifyJsPlugin({ cache:true, sourceMap:false, parallel:4, uglifyOptions: { ecma:8, warnings:false, compress:{ drop_console:true, }, output:{ comments:false, beautify:false, } } }), new htmlwebpackincludeassetsplugin({ assets:['/lib/vendor.dll.js'], publicPath:config.publicPath, append:false }), new webpack.BannerPlugin(bannerTxt) ]); } else { webpackConfig.output.publicPath = '/'; webpackConfig.devtool = '#cheap-module-eval-source-map'; webpackConfig.plugins = (webpackConfig.plugins || []).concat([ new AddAssetHtmlPlugin({ filepath:require.resolve('./static/vendor.dll.js'), includeSourcemap:false, }) ]); Webpackconfig. devServer = {contentBase: path.resolve(__dirname, 'build'), compress: true, //gzip historyApiFallback: true, }; }Copy the code

We use the DllReferencePlugin to introduce the generated manifest.json mapping file into the formal business code package.

App.js is only 7.45KB, vender.dll. Js is copied to build directory lib folder.

All business code is in the version control folder, and vender.dll.js is in the Lib folder. If there is a version change every time online, as long as the online business JS line. There is no need to go online with the lib folder. As long as you don’t manually modify entry for webpack.dll.config.js

entry:{
vendor:['vue','axios','vue-router','qs']
}Copy the code

It will never change. Again, compare before and after optimization:

Before optimization, app.js was 116KB due to the packaging of third-party library. After optimization, the third library app.js was only 7.45KB. But at the beginning of the project, you need to pack a DLL file in advance. Each subsequent compilation took nearly half as long as before. And this is just a scaffolding demo. By the time it is used in real project construction, the effect is more obvious.

conclusion

There are a lot of people who don’t recommend using the DllPlugin, feel that there is no need to pack all the public ones together and load them on the first screen, which makes the first screen load time too long, etc., and feel that an extra config adds to the workload. However, I personally feel that for the whole family bucket series like React and Vue, the technology stack is more holistic. It is necessary to remove the family bucket and place it in vender.js. Because it’s used on almost every page. Moreover, they are third-party libraries that have nothing to do with business logic. Implementing persistent caching for them will greatly improve the experience for both developers and users. A little scaffolding experience, thank you for browsing, any questions welcome to discuss ha ~

Read more:

[1]optimizing Javascript Through Scope Hoisting:https://medium.com/launch-by-adobe/optimizing-javascript-through-scope-hoisting-47c132ef27e4

[2]Tubine:https://github.com/Adobe-Marketing-Cloud/reactor-turbine

[3]ProvidePlugin:https://webpack.js.org/plugins/provide-plugin/

[4]SplitChunksPlugin:https://webpack.js.org/plugins/split-chunks-plugin

The article is transferred from the public account “Full stack Exploration”, welcome to pay attention to: