Webpack 4 Tree Shaking Ultimate Optimization Guide

A few months ago, my task is to build a configuration Webpack 4 Vue.js project to upgrade our group. One of our main objectives is to use tree-shaking advantage that Webpack removed to reduce the size of the package code does not actually use. Now, the benefits of tree-shaking will vary depending on your code base. As several of our architecture decisions, we extract a lot of code from other libraries within the company, and we only use a small part of it.

I wrote this article because properly optimized Webpack not simple. At first I thought it was a simple magic, but then I took a month to search for answers to a series of questions I met online. I hope that through this article, other people will make it easier to deal with similar problems.

Let me talk about benefits

Before discussing the technical details, let me first summarize the benefits. Different applications will see different levels of benefits. The main determining factor is the number of dead in the application code. If you do not have much dead code, then you will not see much benefit in tree-shaking. We project there are a lot of dead code.

In our department, the biggest problem is the number of shared libraries. From simple custom component library, to the enterprise standard component library, and then somehow stuffed into a library a lot of code. Many of which are technical debt, but a big problem is that all of our applications are in import all these libraries, and virtually every application need only a small part of it

In general, once we realized the tree-shaking, our application will be based on different applications, reducing the rate from 25% to 75%. The average reduction was 52%, mainly by driving these massive shared libraries, which are small applications in the main code.

Also, the specific situation will be different, but if you feel you hit the bag may have a lot of unnecessary code, which is how to eliminate their methods.

No sample code repository

For you to continue with the old iron, I am doing the project is the company's property, so I can not share the code to GitHub repositories. However, I will provide a simplified sample code in this article to illustrate my point.

So, without further ado, let's look at how to write the best webpack tree-shaking 4 configuration.

What is dead code

It is simple: Webpack not see the code you use. Webpack tracking the entire application import / export statements, so if it sees something imported ultimately not used, it will think it is "dead code", and will be tree-shaking.

Dead code is not always so clear. Here are some examples dead code and "live" code, hoping to make you more aware. Keep in mind that, in some cases, Webpack will be seen as something dead code, even though it actually is not. See the "side effects" section to learn how to deal with.

// 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
// 这会被看作“活”代码,不会做 tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import Stuff from './stuff';
doSomething();
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import './stuff';
doSomething();
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import 'my-lib';
doSomething();

Write import by way of support for tree-shaking

When writing code to support tree-shaking import way is very important. You should avoid import the entire library into a single JavaScript object. When you do this, you are told you need Webpack entire library, Webpack will not shake it.

To the popular library Lodash example. First import the entire library is a big mistake, but it is much better to import a single module. Of course, Lodash require additional steps to do the tree-shaking, but it is a good starting point.

// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';

The basic configuration of Webpack

The first step in using Webpack be tree-shaking is to write Webpack profile. You can do a lot of custom configuration for your webpack, but if you want the code tree-shaking, you need the following items.

First, you have to be in production mode. Webpack only be tree-shaking when compressed code, and this will only happen in production mode.

Secondly, it must be optimized option "usedExports" set to true. This means Webpack will recognize that it believes the code has not been used, and to mark it in the original packaging step.

Finally, you need to use a support removing dead code compressor. Such compression will recognize that it is a marker Webpack how the code is not being used, and release it. TerserPlugin support this function, it is recommended to use.

The following is the basic configuration of the tree-shaking open Webpack:

// Base Webpack Config for Tree Shaking
const config = {
 mode: 'production',
 optimization: {
  usedExports: true,
  minimizer: [
   new TerserPlugin({...})
  ]
 }
};

What are the side effects

Just because the code can not see Webpack period of use, it does not mean it can be tree-shaking safely. Some imported modules, as long as introduced, will have an important impact on the application. A good example is the global style sheet or JavaScript file to set the global configuration.

Webpack that such documents have "side effects." Files with side effects should not do tree-shaking, as this would undermine the entire application. Risk packaged code, so all of the code by default considered to have side effects in the case Webpack designers clearly recognized not know which files have side effects. This protects you from deleting necessary files, but this is the default behavior means Webpack actually not tree-shaking.

Fortunately, we can configure our project, told Webpack it is no side effects, can be tree-shaking.

How to tell Webpack your code without side effects

package.jsonThere is a special property sideEffects, that is, for this purpose exist. It has three possible values:

trueIt is the default, if you do not specify the value of other words. This means that all files have side effects, which is not a file can be tree-shaking.

false Tell Webpack no files have side effects, all files are tree-shaking.

The third value […]is the file path array. It tells webpack, in addition to the array of files included in your document are not any side effects. Therefore, in addition to the specified file, other files can be safely tree-shaking.

Each project must be sideEffectsproperty to falseor file path array. In our work, we have all the basic applications and shared libraries I mentioned the need to properly configure sideEffectsflag.

The following are sideEffectssome code examples flag. Despite JavaScript comment, but this is JSON Code:

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

CSS and global effects

First, let's define global CSS in this context. Global CSS JavaScript file is imported directly into the style sheet (may be CSS, SCSS, etc.). It has not been converted into CSS module or any similar thing. Basically, import statements like this:

// 导入全局 CSS
import './MyStylesheet.css';

So, if you do a side effect of the above mentioned changes, then when you run webpack building, you will notice a thorny problem immediately. Any style manner described above were imported from the output performance deleted. This is because the import is to be regarded as dead code webpack, and deleted.

Fortunately, there is a simple solution that can solve this problem. Webpack modules using its rules to control the loading system files of various types. Each rule each file type has its own sideEffectslogo. This set of files matching rules will cover all previous sideEffectsmark.

Therefore, in order to retain global CSS file, we only need to set this particular sideEffectsflag true, like this:

// 全局 CSS 副作用规则相关的 Webpack 配置
const config = {
 module: {
  rules: [
   {
    test: /regex/,
    use: [loaders],
    sideEffects: true
   }
  ]
 } 
};

All modules rule Webpack have this property. Rule processing global style sheet must spend it, including but not limited to CSS / SCSS / LESS / and the like.

What is why the modules, important

Now we begin to enter the Fam. On the surface, compile correct module type seems to be a simple step, but, as the following sections will be explained, this is a cause many areas of complex problems. It's taken me a long time to figure out the part.

First, we need to know about the module. Over the years, JavaScript has developed the ability to switch between files as "module" effective import / export code. There are many different modules of JavaScript standard has been around for years, but for the purposes of this article, we will focus on two criteria. One is the "commonjs", another is "es2015". The following is a code form thereof:

// Commonjs
const stuff = require('./stuff');
module.exports = stuff;

// es2015 
import stuff from './stuff';
export default stuff;

By default, Babel assume that we use es2015 module write code and JavaScript code to use commonjs conversion module. This is done for compatibility with a wide range of server-side JavaScript libraries, these libraries are usually built on JavaScript NodeJS (NodeJS only supports commonjs module). However, Webpack does not support the use commonjs module to complete the tree-shaking.

Now, there are some plug-ins (such as common-shake-plugin) claim to have the ability to make Webpack commonjs module tree-shaking, but in my experience, these plug-ins do not work either, or run on es2015 module on the tree -shaking minimal impact. I do not recommend these plug-ins.

Therefore, in order to carry out tree-shaking, we need to compile code to es2015 module.

es2015 module configuration Babel

As far as I know, Babel does not support the other modules of the system will be compiled into a es2015 module. However, if you are a front-end developer, then you probably already using es2015 module to write code, because it is fully recommended method.

Therefore, in order for us to compile code uses es2015 module, we need to do is tell babel do not control them. To achieve this, we simply add the following to our babel.config.js in (In this article, you will see that I prefer the JavaScript configuration rather than JSON configuration):

// es2015 模块的基本 Babel 配置
const config = {
 presets: [
  [
   '[@babel/preset-env](http://twitter.com/babel/preset-env)',
   {
    modules: false
   }
  ]
 ]
};

The modulesset falseis not to tell babel compiled module code. This will make Babel keep our existing es2015 import / export statements.

Plan Key: All needs must be compiled in such a way tree-shaking code. So, if you want to import the library, you must compile these libraries to es2015 module for tree-shaking. If they are compiled as commonjs, then they can not do tree-shaking, and will be packaged into your application. Many library supports partial import, lodash is a good example, which itself is commonjs module, but it has a lodash-es version, using es2015 module.

In addition, if you use the internal library in your application, you must also use es2015 compiled module. In order to reduce the size of the application package, all these internal libraries must be modified to compile in this way.

Sorry, Jest strike

Other testing framework similar situation, we are using Jest.

Anyway, if you come to this point, you will find Jest test begins failed. You will be like I was, like, see the log in all kinds of strange errors, a group of panic. Do not panic, I will take you step by step to resolve.

This result appears simple reason: NodeJS. Jest is based NodeJS development, but does not support NodeJS es2015 module. For this reason there are ways to configure the Node, but does not work in jest. So we stuck in here: Webpack need to be es2015 tree shaking, but Jest not perform tests on these modules.

That's why I say into the modular system "Fam." This whole process is the most time-consuming to figure out my part. I suggest you read this section and section behind a few, because I will give solutions.

Solution has two main parts. The first part of the code for the project itself, which is to run test code. This part is relatively easy. For the second part of the library code, that is, from other projects, it is compiled into es2015 module and introduced into the code for the current project. This part is more complicated.

Building Solutions local Jest Code

For our problem, babel has a useful feature: environmental option. It can be configured to run in different environments. Here, development and production environments we need es2015 module, and test environment needs commonjs module. Fortunately, Babel configuration is very easy:

// 分环境配置Babel 
const config = {
 env: {
  development: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: false
     }
    ]
   ]
  },
  production: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: false
     }
    ]
   ]
  },
  test: {
   presets: [
    [
     '[@babel/preset-env](http://twitter.com/babel/preset-env)',
     {
      modules: 'commonjs'
     }
    ]
   ],
   plugins: [
    'transform-es2015-modules-commonjs' // Not sure this is required, but I had added it anyway
   ]
  }
 }
};

After setting up all the items to compile the normal native code, Jest a test run. However, the use of third-party library code es2015 module still can not run.

Solve the library code Jest

The reason the library code running error is very obvious, look at node_modules directory will understand. Here library code using a syntax es2015 module, to be tree-shaking. These libraries have been compiled in this manner, so Jest when attempting to read the code in the unit test, to explode. Noticed, we've let Babel enabled commonjs module in a test environment Yeah, why these libraries do not work? This is because, when Jest (especially babel-jest) before the test run compiled code, default ignore any code from the node_modules.

This is actually a good thing. If Jest need to recompile all the libraries, it will greatly increase the processing time of the test. However, while we do not want it to recompile all the code, but we want it to be recompiled using the library es2015 module, so as to use the unit tests.

Fortunately, Jest in its configuration provides a solution for us. I want to say, this part does make me think for a long time, and I feel no need to do so complicated, but this is the only solution I can think of.

Configuring Jest recompile the library code

// 重新编译库代码的 Jest 配置 
const path = require('path');
const librariesToRecompile = [
 'Library1',
 'Library2'
].join('|');
const config = {
 transformIgnorePatterns: [
  `[\\\/]node_modules[\\\/](?!(${librariesToRecompile})).*$`
 ],
 transform: {
  '^.+\.jsx?$': path.resolve(__dirname, 'transformer.js')
 }
};

The above configuration is Jest recompile your library needs. There are two main parts, I'll explain everything.

transformIgnorePatternsJest is a functional configuration, it is a regular array of strings. Any of these regular expression matching code will not be babel-jest recompiled. The default is a string "node_modules". That's why Jest not recompile any library code.

When we offer custom configuration, it is to tell Jest re-compile time how to ignore code. That's why you just see the metamorphosis of regular expressions have a negative assertion inside the first, the purpose is to match Everything except the library. In other words, we tell Jest ignore node_modules all the code in addition to the specified library.

This once again proved JavaScript Configuration better than JSON, because I can easily by string manipulation, regular expressions to insert the name of the library splice array.

The second transform is configured, he points babel-jest a custom converter. I'm not 100% sure this is a must, but I added. We set it up for loading configuration at the time of Babel recompile all the code.

// Babel-Jest 转换器
const babelJest = require('babel-jest');
const path = require('path');
const cwd = process.cwd();
const babelConfig = require(path.resolve(cwd, 'babel.config'));
module.exports = babelJest.createTransformer(babelConfig);

These are configured, you can test the code should run. Remember, any use of the library es2015 modules need to configure, or test code can not run.

Then turn to another sore point: the link library. Use npm / yarn linking process is to create a project to a local directory symbolic link. The results showed that, Babel when recompiled to link this way the library will throw a lot of mistakes. The reason I took so long to figure out Jest thing that it exists, is one of the reasons I always link my library in this way, there have been a bunch of errors.

The solution is: Do not use npm / yarn link. Such a tool similar "yalc", which can be connected to the local project, while npm simulate the normal installation process. It not only failed to recompile Babel problem, but also to better deal with transitive dependencies.

Optimized for specific library.

If you have completed all of the above steps, your application basically achieved relatively robust tree shaking. However, in order to further reduce the package size, you can do something. I will list some optimization methods specific library, but this is definitely not all. In particular, it provides inspiration for us to make some cool stuff.

MomentJS is a big volume library name. Fortunately, it can be removed to reduce the volume of multi-language pack. In the following code example, I exclude all momentjs multi-language pack, leaving only the basic parts, volume was significantly smaller lot.

// 用 IgnorePlugin 移除多语言包
const { IgnorePlugin } from 'webpack';
const config = {
 plugins: [
  new IgnorePlugin(/^\.\/locale$/, /moment/)
 ]
};

Moment-Timezone is MomentJS's cousin, also a big man. Large files JSON its volume is essentially a time zone information with the result. I found that as long as the data retention years of this century, it can be 90% smaller in size. This situation need to use a special plug-Webpack.

// MomentTimezone Webpack Plugin
const MomentTimezoneDataPlugin = require('moment-timezone-data-webpack-plugin');
const config = {
 plugins: [
  new MomentTimezoneDataPlugin({
   startYear: 2018,
   endYear: 2100
  })
 ]
};

Lodash is another cause big package expansion. Fortunately, there is an alternative package Lodash-es, it is compiled into es2015 module, and with a sideEffects flag. Alternatively it may be further reduced by Lodash packet size.

In addition, Lodash-es, react-bootstrap and other libraries can achieve weight loss with the help of Babel transform imports plug-ins. The plugin reads the statement from index.js file import library, and the library to point to a specific file. This will make it easier for libraries to do webpack tree shaking when parsing module tree. The following example shows how it works.

// Babel Transform Imports
// Babel config
const config = {
 plugins: [
  [
   'transform-imports',
   {
    'lodash-es': {
     transform: 'lodash/${member}',
     preventFullImport: true
    },
    'react-bootstrap': {
     transform: 'react-bootstrap/es/${member}', // The es folder contains es2015 module versions of the files
     preventFullImport: true
    }
   }
  ]
 ]
};
// 这些库不再支持全量导入,否则会报错
import _ from 'lodash-es';
// 具名导入依然支持
import { debounce } from 'loash-es';
// 不过这些具名导入会被babel编译成这样子
// import debounce from 'lodash-es/debounce';

to sum up

This concludes the paper. Such optimization can greatly reduce the size of the package. With the front-end architecture began to have a new direction (such as micro front-end), to keep the packet size optimization has become more important than ever. I hope this will give those students who are doing tree shaking to the application to bring some help.

communicate with

Welcome scan code concern micro-channel public number "1024 Verses", as you offer more technical dry.
Public number: 1024 Verses

Guess you like

Origin www.cnblogs.com/lzkwin/p/11878509.html