Background:

The project was optimized for Webpack packaging before it went live, but during network optimization, the plugin Webpack-bundle-Analyzer found that some common JS files were repeatedly packaged into the JS of the business code. The code is small, but I want to optimize it for maximum optimization. The biggest gain of this process is to make myself more familiar with webpack4.x related configuration items, and to be able to use WebPack to easily achieve their desired packaging method.

I remember a former colleague said a word about front-end optimization: front-end optimization is a compromise made after weighing all kinds of advantages and disadvantages.

Optimization results:

Here is a look at the optimization results, because the project is multi-entry packaged mode (project scaffolding click here) one link per page and each page will have its own JS file.

The results are as follows:

  • Js code size reduced: 20KB +
  • Shortened network connection duration: 500ms+

The 20KB reduction in mobile projects is not a small amount, and this 20KB is a result of repeated packaging. In terms of network optimization, the data we can see from our company’s internal monitoring platform is also very obvious. The average data within 3 days after the project went online was counted, and the page loading time was saved by nearly 800ms (500ms above is a conservative writing method, because there will be irresistible factors such as network jashing).

Data statistics before optimization

Optimized data statistics

Optimized the network resolution duration and execution duration

1. Add DNS preresolution

In the HTML head tag, use the meta tag to specify the domain name for enabling DNS pre-resolution and adding DNS pre-resolution. The example code is as follows:

    <! -- Tell browser to enable DNS preresolution -->
    <meta http-equiv="x-dns-prefetch-control" content="on" />
    <! Add a domain name that needs to be resolved -->
    <link rel="dns-prefetch" href="//tracker.didiglobal.com">
    <link rel="dns-prefetch" href="//omgup.didiglobal.com">
    <link rel="dns-prefetch" href="//static.didiglobal.com">
Copy the code

Let’s take a look at the parsing time for static resources on the page before adding the above code

After adding the DNS pre-resolution code and comparing it with the above picture, it is clear that the domain name tracker.didiglobal.com has been pre-resolved before loading. If this js file affects page rendering (such as pages loaded on demand), it can improve page rendering performance.

2. Delayed execution of code that affects page rendering

We usually refer to some third-party JS dependencies during mobile development, such as client.js, which calls the jsbridge method of the client, and console.log.js, which accesses the log service.

The usual and more violent approach is to add these dependent third-party JJS to the head tag, download these JJS in advance when the DOM is parsed into the head tag and call them whenever needed later. However, the download time and execution time of these JS placed in the head tag will undoubtedly affect the rendering time of the page.

The image below shows the current state of our project before optimization. The light green vertical line is the time to start rendering. As you can see from the diagram below, the fusion.js we refer to the client method and the omega. Js dot (both of which are placed in the head tag) affect the start time of the page rendering.

In fact, our business scenario does not need these JS execution results until the page is rendered, so why can’t we load these JS asynchronously?

Asynchronous loading of JS is to select the appropriate time, using the dynamic creation of script tags, to load the required JS to the page. Our project uses VUE. We choose to load the JS that needs to be clicked in the vue lifecycle, and load fusion.js where we need to call the client method through the asynchronous loading library callback.

Here is a simple example code:


export default function executeOmegaFn(fn) {
    // Dynamically load the js to the current page, and execute the callback when the JS is finished loading, note that you need to check whether the JS has been loaded in the current environment
    scriptLoader('/ / xxx.xxxx.com/static/tracker_global/2.2.2/xxx.min.js'.function () {
        fn && fn();
    });
}

// Asynchronously load the js to be loaded
mounted() {
    executeOmegaFn();
},
Copy the code

As you can see in the image below, the page starts rendering earlier and the page finishes rendering about 2s earlier than in the image above. It is obvious that page rendering and download execution time of omega are independent of each other.

Conclusion:

  1. Add a domain name DNS pre-resolution code block to the HTML template file to enable the browser to pre-resolve the domain name of the static file to be loaded. When static resources need to be downloaded, speed up the download of static files.
  2. By delaying the loading and execution of js files that are not needed for the first screen rendering, the time of page rendering is advanced to improve the speed of the first screen rendering.

Optimize the Webpack output

1. Optimize repeated code packaging

The default Webpack4. x does tree shaking for code in Production mode. But in most cases, tree Shaking doesn’t have a way to remove duplicate code after reading this article. Your tree-shaking is not good for eggs

In my project, there is a lib directory that contains a library of functions written according to business needs, which is repeatedly packaged into the JS files of our business code through webpack-bundle-Analyzer. The following diagram shows the js files contained in the packaged business code. You can see that the contents of the lib directory are repeatedly packaged

When you see this, talk about your own optimization:

  • Bundle the dependencies in the node_modules directory into onevendorRely on;
  • Package your own libraries in lib and common into a single common.
  • Package dependent third-party component libraries as needed, if large components from the library are used, such as the Date-Picer and Scroll components in Cube-UI. If used only once, it is packaged into the JS file that references its own page, and if referenced by multiple pages it is packaged into common. The optimization and packaging strategies for this section will be described in more detail later in optimizing third-party dependencies.

Take a look at my webpack configuration file for unpacking.

splitChunks: {
    chunks: 'all'.automaticNameDelimiter: '. '.name: undefined.cacheGroups: {
        default: false.vendors: false.common: {
            test: function (module, chunks) {
                // Only the better- Scroll dependencies of the Common lib cube- UI and cube- UI components are packaged into common by configuring rules
                if (/src\/common\//.test(module.context) ||
                    /src\/lib/.test(module.context) ||
                    /cube-ui/.test(module.context) ||
                    /better-scroll/.test(module.context)) {
                    return true; }},chunks: 'all'.name: 'common'.// The minchunks here are very important. Components that control cube UI usage are referenced by more than a few chunks before they are packaged into the common or not packaged into the JS
            minChunks: 2.priority: 20
        },
        vendor: {
            chunks: 'all'.test: (module, chunks) = > {
                // Package all dependencies in node_modules into vendor
                if (/node_modules/.test(module.context)) {
                    return true; }},name: 'vendor'.minChunks: 2.// Set the chunk packaging priority. The value here determines that cube-UI under node_modules will not be packaged into vendor
            priority: 10.enforce: true}}}Copy the code

A JS is written in the project to unify the Cube-UI components that need to be used throughout the project. See here for the code and here for calling the method.

It is important not to include bulky components that are used infrequently in this file, as you can see in the code comments below.


It is recommended to introduce only the basic components for each page. For complex components such as the Scroll datepicer component *, introduce them separately in the page. Then use the Webpack with minChunk to specify that these larger components should be called into common only when they exceed x references or else packaged separately into the page's JS * @date 2019/04/02 * @author [email protected] */

/* eslint-disable */
import Vue from 'vue';
import {
    Style,
    Toast,
    Loading,
    // Remove scroll here is introduced separately in the page so that webpack can choose whether to package this component into the JS of the page or into the common according to the reference chunk
    // Scroll,
    createAPI
} from 'cube-ui';


export default function initCubeComponent() {
    Vue.use(Loading);
    // Vue.use(Scroll);
    createAPI(Vue, Toast, ['timeout'].true);
}

Copy the code

Currently, only the pay_history page uses the Scroll component of Cube-UI in the project, which is packaged separately into the JS of the business code, so the JS of the page is large.

When more than two pages use the Scroll component, it is automatically packaged into common according to webPack configuration. Below is the result of packaging. The js size of the page has been reduced and the commonJS file has been increased in size.

Conclusion:

  • Optimize the loading of third-party dependent components to reduce unnecessary loading and execution time consumption.

2. Remove unnecessary imports

Sometimes I don’t notice when I write code that references a third party dependency through import but doesn’t end up using it or that I don’t need to comment out the expression I’m executing. Without commenting out the import statement, the package result will include the js of that import. For example:


import VConsole from 'vconsole';
// We may have opened the following comment during the test, but only commented the following code at launch, webpack will still pack the vConsole into the target JS.
// var vConsole = new VConsole();

Copy the code

Conclusion:

  • Identify invalid import statements and comment out or delete import statements without importing functions or expressions using import.

3, Babel-Preset -env and autoprefix configurations are optimized

Babel-polyfill is rarely used today to write front-end code using the Babel + ES6 combination. The main reason is that it will pollute global variables and introduce polyfill in its full form, resulting in a very large packed target JS file.

In most cases now, babel-preset-env is used to polyfill. A smarter or more advanced approach is to use the online Polyfill service, refer to the link

When used with preth-env, are mostly ignored to configure the compatible browsers list. Or search for the configuration directly from the network, and copy and paste the output results directly. In fact, this configuration will affect the size of our JS file.

F you use Autoprefix to automatically add a vendor prefix to the CSS, you also need to configure a browsers list. This configuration list also affects the CSS file size. Browserslist official document

For example, Windows Phone is almost gone now, and there is no need to consider compatibility with PC and WP phones for current mobile terminal projects. Can we remove the -MS – prefix when adding polyfill or CSS manufacturer prefix? Then how should we configure it?

My configuration is as follows:

"browsers": [
    "1%" >."last 2 versions"."IOS > = 6.0"."not ie > 0"."not ie_mob > 0"."not dead"
]
Copy the code

Here’s a quick mention of the importance of using new CSS features correctly. The code below is one I saw on one of our older projects. It doesn’t look like a problem at first glance, but is it a problem in modern browsers?

.example {
    display: flex;
    display: -webkit-box;
}

.test {
   flex:1
}

Copy the code

This general notation is the result of inconsistent resolution of flex layouts. In Chrome,.example is used to display: -webkit-box. Flex :1 is in effect in.test and this is the new standard. The layout display is faulty.

Code after Autoprefix

.example {
    display: -ms-flexbox;
    display: flex;
    display: -webkit-box;
}

.test {
   -webkit-box-flex:1;
       -ms-flex:1;
           flex:1
}

Copy the code

This will also result in a layout error without autoprefix.

Conclusion:

  1. Add specific Polyfill configurations based on your business scenario.
  2. If you use cSS3’s new features and use Autoprefix for auto-adding vendor prefixes, you only need to use the latest standard writing in the original code.

4, webPack Runtime file inline

Compiling code with Webpack, when the code generates multiple chunks, how does WebPack load those chunks?

Webpack itself implements a module loader to maintain relationships between different modules (referred to as the Runtime module in the WebPack article). Modules are identified by a string of numbers (you can write a simple demo to see the result).

This string of numbers will change in the Runtime module when a file’s code is modified, and if this code is not processed when webPack is packaged, it will default to our vendor code. As a result, the hash of the generated vendor file changes whenever the code is modified, making it impossible to take full advantage of the browser cache.

Webpack already provides configurations to extract this code separately to generate a file. Because this part of the code changes frequently and is small, you can choose to inline this part of the code in the HTML template file when packaging to reduce HTTP requests.

In webpack4. X can be implemented through the following configuration optimization. RuntimeChunk: ‘single’. If you want to inline the produced Runtime code into HTML, you can use the webpack plug-in inline-manifest-webpack-plugin.

5. Remove unnecessary async statements

Async and await syntactic candy can solve asynchronous programming problems well. You can also use this syntactic sugar when writing front-end code. Compiling both Babel and typescript code is actually compiling async and await to generator.

I would not recommend using async and await in front-end code if there is an extreme need for code size. Since many third-party dependencies now use promises to handle asyncism, the node_modules dependencies we use are typically compiled ES5 source files that Polyfill promises. Also, our own Babel configuration polyfills promises, and if we mix async and await, Babel adds the associated Generator Run time code.

Look at a real code example: an async expression appears in the following code, but no await is used anywhere the method is called, and reading the code confirms that async expression is not needed

After adding an async expression, the result is shown below. Generaotr Runtime code in the output object file, and this part of the code is relatively large

This is the file size before compilation

After removing this unnecessary async expression, you can see the compiled file size below, reducing the code size by nearly 3KB.

Optimize third-party dependencies

The packaging method for optimizing third party dependencies was briefly introduced in section 1, so here’s a summary:

  • If third-party dependencies support post-compilation, use post-compilation and load on demand with third-party dependencies rather than importing the entire component library.
  • If a third party relies on a component that is large in size and used less times in the project, and the page is loaded on demand, you can select configuration rules to package the component into the common when the number of references exceeds the number of times. Otherwise, the component is packaged directly into the business code.
  • When importing third-party dependencies through script tags and links, these links should not be written into the head tag, but can be loaded as needed.

Post-compilation, which is compilation at the point of use, solves the problem of repackaging code. Import on demand means that if I use a Cube-UI with 20 components but I only use one of them, avoid importing all of them and increase the volume of the output file;

7. Lodash is introduced on demand

Lodash is a nice library to use, but it has the disadvantage of being bulky when packaged in full. So can Lodash be introduced on demand?

Of course you can. You can search for lodash-es on NPM and export LoDash to ES6 modules by executing commands from the documentation. It can then be used as a separate import of a function.

In fact, there are some disputes about how to optimize LoDash and whether it is necessary to optimize loDash. For details, you can read this article on Baidu Gao T Grey Da about loDash’s optimization attempts in Webpack. This article of Grey University also demonstrates what it said at the beginning of the article, that optimization is a compromise after making various trade-offs according to business requirements.

Summary of important knowledge of Webpack

1. Hash, Contenthash, chunkhash

Hash is related to the construction of the entire project. Whenever a file changes in the project, the hash value of the entire project will change, and all files share the same hash value.

Chunkhash hashes generate different hashes after each build, even if the contents of the file never change. There is no way to cache this way, so we need to change the hash method to chunkhash. Chunkhash is different from hash. It parses dependent files based on different Entry files, constructs corresponding chunks, and generates corresponding hash values. We built some public libraries separately from the program entry files in production, and then we used chunkhash to generate the hash, so as long as we didn’t change the code of the public library, we could guarantee that the hash value would not be affected.

When contenthash compiles code using Webpack, we can reference CSS files in js files. So the two files should share the same chunkhash value. The problem is that if the JS changes the code, the CSS file will be built repeatedly, even if the content of the CSS file does not change. In this case, we can use the contenthash value in extra-text-webpack-plugin to ensure that even if the CSS file is in a module where the content of other files changes, as long as the CSS file content does not change, the CSS file will not be built again.

2. SplitChunks

Currently, there is no complete Chinese translation of splitChunks for webpack4.x documents available on the Internet. If you have no difficulty in reading English, you can read the official documents directly. If you are not good at English, you can refer to the following parameters and Chinese definitions:

First, Webpack4.x automatically splits code blocks according to the following conditions:

  • New code blocks can be shared references or modules can come from the node_modules folder
  • New code block larger than 30KB (size before min + gziped)
  • The maximum number of code blocks loaded on demand should be less than or equal to 5
  • The maximum number of code blocks initially loaded should be less than or equal to 3

// The configuration items are explained as follows
splitChunks: {
    // Applies to asynchronous chunks by default. The value is all
    // Initial optimizes the packaging of asynchronous and non-asynchronous modules separately. All optimizes both asynchronous and non-asynchronous packaging. That is, moduleA is introduced asynchronously in indexA and synchronously in indexB, and the lower moduleA will appear in two packaged blocks for INITIAL and only one for ALL.
    // all The common parts of all chunk code (both synchronously and asynchronously loaded modules can be used) are separated into a single file
    // async pulls out a single file from the common part of the asynchronous loading module code
    chunks: 'async'.// The default value is 30KB. If the file size is larger than = minsize, the file will be split into two files and no new chunk will be generated
    minSize: 30000.// The minimum number of chunks to share with this module (new chunks will be split if >= minchunks)
    minChunks: 1.// Up to 5 asynchronous load requests to this module
    maxAsyncRequests: 5.// A maximum of three requests can be made to the module during the initial session
    maxInitialRequests: 3.// The interval between the name
    automaticNameDelimiter: '~'.If set to truw, the name of chunk is separated by ~ by default. For example, vendor~ can also be specified manually
    name: true.// Set a cache group to extract chunks that meet different rules. Each new chunk is a cache group
    cacheGroups: {
        common: {
            // The name of chunk extracted
            name: 'common'.// In the same way as in the outer layer, chunks are overwritten and extracted in the dimension of chunk
            chunks: 'all'.// Can be string, regular expression, function, module as dimension extract,
            // All modules that meet the conditions will be extracted into the chunk of the common, which is the first argument in the function
            // is for each module traversed, and the second parameter is the chunks array for each reference to that module
            test(module, chunks) {
                // module.context Directory where the current file module resides This directory contains multiple files
                // module.resource The absolute path to the current module file

                if (/scroll/.test(module.context)) {
                    let chunkName = ' '; // The name of the module that references the chunk

                    chunks.forEach(item= > {
                        chunkName += item.name + ', ';
                    });
                    console.log(`module-scroll`.module.context, chunkName, chunks.length); }},// Priority. A chunk is likely to meet multiple cache groups and will be extracted to the cache group with the highest priority
            priority: 10.// Be referenced by at least a few chunks
            minChunks: 2.// If the chunk refers to the chunk that has been extracted, the chunk is directly referenced without repeated packaging code (whether to use the previous module when the module has not changed).
            reuseExistingChunk: true.// If minSize is not set in cacheGroup, it is used to determine whether to use upper-layer minSize. True: 0; false: upper-layer minSize is used
            enforce: true}}};Copy the code

Refer to the article

  • Use Webpack to implement persistent caching