The previous article tree-shaking performance optimization practice-Principles introduced the principles of tree-shaking, this article mainly introduces the practice of Tree-shaking





Tree -shaking practice

Webpack2 release, announced support for tree-shaking, WebPack 3 release, improved scope and smaller bundle generated files. Before we upgrade WebPack, we are expecting another big performance boost and are looking forward to the upgrade. In fact, this is the truth

After the upgrade, the bundle file size was not significantly reduced, and there was a big psychological gap at that time. Then I studied why the effect was not ideal. See tree-Shaking Performance Optimization Practice-Principles.

The optimization still needs to continue, and while the tree-shaking that comes with the tool can’t remove too much code, there are things that can be done to remove it. Let’s do some optimization in three areas.



(1) Optimization of component library references

Let’s start with a problem

When we use component libraries, import {Button} from ‘element-ui’, as opposed to vue.use (elementUI), is already performance-conscious and recommended, but if we write it in the right-hand form, specific to the file reference, The difference after packaging is very large. In the case of ANTD, the right form of bundle reduces the volume by about 80%.

This reference also has the side effect that WebPack cannot tree-shaking other components. Since the tool itself can’t do it, we can make the tool to automatically change the code on the left to the code on the right. The tool, the ANTD library itself, is also provided. I made a few modifications to the antD tools, no configuration, and native support for our own component libraries, WUI and Xcui, as well as some other commonly used libraries

Babel-plugin-import-fix, narrowing the reference

lin-xi/babel-plugin-import-fix


Here’s how it works

Babel converts ES6 code to an AST abstract syntax tree using core Babylon, then the plugin traverses the syntax tree to find statements like import {Button} from ‘elements-ui’, converts them, and regenerates the code.

Babel-plugin-import-fix supports ANTD, Element, Meterial-UI, WuI, xcui and D3 by default. You just need to configure the plugin itself in babelrc.

.babelrc

{
  "presets": [["es2015", { "modules": false}]."react"]."plugins": ["import-fix"]}Copy the code

The idea is to have all the common libraries supported by default, but many common libraries do not support narrowing the reference. Because there is no independent output for each submodule, you cannot change the reference to a single submodule.



(2) CSS tree-shaking

All of the tree-shaking we talked about was for JS files, using static analysis to eliminate as much useless code as possible. Can we do tree-shaking for CSS?

With the popularity of CSS3, LESS, SASS and other CSS preprocessing languages, the proportion of CSS files in the whole project can not be ignored. As large project functionality continues to iterate, there may be useless code in CSS. I implemented a WebPack plugin to solve this problem and find out where the CSS code is useless.

webpack-css-treeshaking-pluginFor tree-shaking of CSS




webpack-css-treeshaking-plugin


Here’s how it works

The whole idea is to iterate through all the selectors in the CSS file, and then match them in all the JS code. If the selector doesn’t appear in the code, it’s considered useless.

The first question is, how do you gracefully traverse all the selectors? Is it painful to use regular expressions to match the segmentation?

Babel is a blessing in the JS world, but there is also a weapon in the CSS world: postCss.

PostCSS provides a parser that parses CSS into an AST abstract syntax tree. Then we can write various plug-ins, process the abstract syntax tree, and finally generate a new CSS file to precisely modify the CSS.

The whole is another webPack plug-in, the architecture diagram is as follows:

Main process:

  • The plugin listens for webapck compilation and finds all CSS and JS files from the Compilation process after webPack compilation
apply (compiler) {
    compiler.plugin('after-emit', (compilation, callback) => {

      let styleFiles = Object.keys(compilation.assets).filter(asset => {
        return /\.css$/.test(asset)
      })

      let jsFiles = Object.keys(compilation.assets).filter(asset => {
        return /\.(js|jsx)$/.test(asset)
      })

     ....
}
Copy the code

  • Send all CSS files to postCss for processing and find the code that is not useful
   let tasks = []
    styleFiles.forEach((filename) => {
        const source = compilation.assets[filename].source()
        let listOpts = {
          include: ' '.source} tasks.push(postcss(treeShakingPlugin(listOpts)).process(postcss(treeShakingPlugin(listOpts)).process(source).then(result => {       
          letCSS = result.tostring () // postCss AST // replace webpack compilation productsource: () => css,
            size: () => css.length
          }
          return result
        }))
    })
Copy the code

  • PostCss traversal, matching, and deletion processes
 module.exports = postcss.plugin('list-selectors'.function(options) {// iterate from the root node to cssroot.walkrules (function (rule) {
      // Ignore keyframes, which can log e.g. 10%, 20% as selectors
      if (rule.parent.type === 'atrule' && /keyframes/.test(rule.parent.name)) returnCheckRule (rule).then(result => {if(result.selectors. Length === 0) {// All selectors are removedlet log = ' ✂️ [' + rule.selector + '] shaked, [1]'
          console.log(log)
          if (config.remove) {
            rule.remove()
          }
        } else{// Selector partially deletedlet shaked = rule.selectors.filter(item => {
            return result.selectors.indexOf(item) === -1
          })
          if (shaked && shaked.length > 0) {
            let log = ' ✂️ [' + shaked.join(' ') + '] shaked, [2]'
            console.log(log)}if(config.remove) {// modify the AST abstract syntax tree rule-.selectors = result.selectors}})})Copy the code

CheckRule handles every rule core code

let checkRule = (rule) => {
      return new Promise(resolve => {
        ...
        let secs = rule.selectors.filter(function (selector) {
          let result = true
          let processor = parser(function (selectors) {
            for (let i = 0, len = selectors.nodes.length; i < len; i++) {
              let node = selectors.nodes[i]
              if (_.includes(['comment'.'combinator'.'pseudo'], node.type)) continue
              for (let j = 0, len2 = node.nodes.length; j < len2; j++) {
                let n = node.nodes[j]
                if(! notCache[n.value]) { switch (n.type) {case 'tag':
                      // nothing
                      break
                    case 'id':
                    case 'class':
                      if(! ClassInJs (n.value)) {// Call classInJs to determine if notCache[n.value] = in JStrue
                        result = false
                        break
                      }
                      break
                    default:
                      // nothing
                      break}}else {
                  result = false
                  break}}}})... })... })}Copy the code

You can see that I’m actually only dealing with id selectors and class selectors, so ID and class have relatively few side effects and are less likely to cause style exceptions.

Determine whether the CSS appears in JS again, is to use regular match.

For example, the tag class selector can be configured to see if it appears in HTML, JSX, or template. If it does, it can be considered useless code.

Of course, there are certain conditions and constraints that make plug-ins work. We can change the CSS dynamically in our code, like react and vue, and we can write it like this

This is the recommended approach, and the selector appears in the code as a character or variable name. Dynamically generating the selector in the following case will cause the match to fail

render(){
  this.stateClass = 'state-' + this.state == 2 ? 'open' : 'close'
  return <div class={this.stateClass}></div>
}
Copy the code

This is easily avoided

render(){
  this.stateClass = this.state == 2 ? 'state-open' : 'state-close'
  return <div class={this.stateClass}></div>
}
Copy the code

So plug-ins work better with the constraints of a good coding specification.


(3) Delete the webpack bundle file

If the same module exists in the webPack bundle, this is also a form of useless code. Should also be removed

The first thing we need is a tool that can do qualitative analysis of bundle files, that can find problems, that can see optimizations.

Webpack-bundle-analyzer is a plugin that provides a graphical representation of the size of the components of all the modules in the bundle.

Second, requirements extract common modules, and CommonsChunkPlugin is the best known plug-in for providing common modules. In the early days, I didn’t fully understand his function and didn’t get the most out of it.

The correct use of CommonsChunkPlugin is described below

Automatically extract all node_moudles or modules that are referenced more than twice

MinChunks can accept either a numeric value or a function, and if it is a function, you can customize the packing rules

But with the configuration documented above, you can’t rest easy. This is because this configuration can only extract generic modules in all the entry packaged files. In reality, with improved performance, we will load on demand, via webPack’s import (…) Method, the on-demand file does not exist in the entry, so the generic module in the on-demand asynchronous module is not extracted.

How do I extract generic modules from an on-demand asynchronous module?

Configure another CommonsChunkPlugin and add the async property, which can accept Boolean values or strings. When it is a string, the default is the name of the output file.

Names is the name of all asynchronous modules

There is also a bit of a name for asynchronous modules. Here’s how I did it:

const Edit = resolve => { import( /* webpackChunkName: "EditPage"* /'./pages/Edit/Edit').then((mod) => { resolve(mod.default); })}; const PublishPage = resolve => { import( /* webpackChunkName:"Publish"* /'./pages/Publish/Publish').then((mod) => { resolve(mod); })}; const Models = resolve => { import( /* webpackChunkName:"Models"* /'./pages/Models/Models').then((mod) => { resolve(mod.default); })}; const MediaUpload = resolve => { import( /* webpackChunkName:"MediaUpload"* /'./pages/Media/MediaUpload').then((mod) => { resolve(mod); })}; const RealTime = resolve => { import( /* webpackChunkName:"RealTime"* /'./pages/RealTime/RealTime').then((mod) => { resolve(mod.default); })};Copy the code

Yes, add comments to the import. /* webpackChunkName: “EditPage” */

Post a project optimization comparison chart

The optimization effect is still quite obvious.

Optimize the bundle before

The optimized bundles



One final question to consider:

Do I need to extract generic modules for different entry modules or asynchronous modules loaded on demand?

This depends on the scene, such as modules are loaded online, if the general module extraction granularity is too small, it will lead to the first screen of the home page needs more files, many may be the first screen is not used, resulting in the first screen is too slow, the second or third level page loading will be greatly improved. So this is a tradeoff to control the granularity of the generic module extraction based on the business scenario.

The mobile application scenario of Baidu Takeout is like this. All of our mobile pages have been processed offline. After offline, load the local JS file, has nothing to do with the network, basically can ignore the file size, so pay more attention to the size of the whole offline package. The smaller the offline package, the less traffic the user consumes and the better user experience. Therefore, the offline scenario is very suitable for the extraction of universal modules with minimum granularity, that is, all the entry modules and asynchronous loading modules whose references are greater than 2 are extracted, so as to obtain the smallest output file and the smallest offline package.

On January 20, I will share “Baidu Takeout front-end offline practice” in Nuggets, interested can pay attention to it.


All the plugins mentioned in the text are open source, link summary, welcome communication, welcome stamp ❤



lin-xi/babel-plugin-import-fix

lin-xi/webpack-css-treeshaking-plugin