Project background

The project is a library for internal company use, packaged using WebPack. (Webpack version: 5.40.0) Used to use the form of NPM for package management, but due to the large volume of the packaged project, affecting the loading speed of the page, so we decided to give up using NPM package management, package it into JS files, and reference it through script tag. Both NPM packages and JS files have their advantages and disadvantages.

type advantages disadvantages
NPM package Easy to do version management; Simple reference When the package volume is too large, it cannot be split. Difficult to do load optimization
Js file Easy to split modules, do load optimization; High reference flexibility Version management is cumbersome; The caller needs to choose the right time to load the JS file

Since the package is for internal use and has higher load performance requirements, js can be used

Optimization steps

I. Dynamic import of some modules

Some modules that are not necessarily used and relatively large in the project are referenced in the way of dynamic import in Webpack to reduce the volume of packages loaded for the first time. For example, before modification:

import module1 from './module1';
Copy the code

Revised:

Const module1 = await import(/* webpackChunkName: "module1" */ './module1');Copy the code

Webpack will then package module1 into a separate JS file. The file is only loaded when you import it.

Webpack also supports prefetch modules, which are loaded when the browser is idle, rather than when you introduce them. This actually uses the preload and prefetch values supported by the tag rel attribute.

Two, module split

The previous approach only works for modules that are not immediately available, but it does not work for some core modules. For example, in our project, three.js is applied, and the module has a large volume. We can package it into a separate JS file through webpack configuration Optimization.

// ...
optimization: {
    // ...
    splitChunks: {
      cacheGroups: {
        three: {
          test: /[\\/]node_modules[\\/](three)[\\/]/,
          name: 'three',
          chunks: 'all',
          priority: 2,
        },
      },
    },
  },
Copy the code

Other larger modules can also be separated.

Modify the module export mode

The first two steps are mainly for module splitting, the purpose is to achieve lazy loading and parallel loading, which basically can greatly improve the loading speed

Now we need to modify the module export method. Module.exports = myLibrary and require(‘myLibrary’). Now packaged as a JS file and imported directly into the browser, you need to expose myLibrary to global objects. (of course you can use the module syntax supported by AMD or other browsers, but not currently.) change webpack output.library.type from commonJS to umD:

library: {
  name: 'myLibrary',
  type: 'umd',
},

Copy the code
Deploy the JS file

We need to deploy the packaged artifacts to a static server. To configure the publicPath of WebPack:

// ...
output: {
  path: path.resolve(__dirname, 'dist'),
  publicPath: `https://cdn.xxx.com/your/server/path`,
},
Copy the code

After each pack will be uploaded to the server dist directory path under https://cdn.xxx.com/your/server/path. We currently use GitLab’s CI/CD for automated deployment.

Five, use a new way to call

To load a script dynamically, here are the loading functions:

function loadScript(src) { return new Promise((resolve, reject) => { const scriptEle = document.createElement('script');  scriptEle.type = 'text/javascript'; // Dynamic script async defaults to true, set to false to execute sequentially (parallel loading) scriptele. async = false; if (scriptEle.readyState) { // IE scriptEle.onreadystatechange = () => { if (scriptEle.readyState == 'loaded' || scriptEle.readyState == 'complete') { resolve(); }}; } else { scriptEle.onload = () => { resolve(); }; scriptEle.onerror = () => { reject(`The script ${src} is not accessible`); }; } scriptEle.src = src; document.currentScript.parentNode.insertBefore(scriptEle, document.currentScript); }); }Copy the code

Parallel loading is used and executed in order:

Runtime files -> dependencies to be split out -> main files

6. Make better use of caching

Use the hash value as the packaged file name to ensure that the cache is invalidated if the file is modified.

 output: {
    // ...
    filename: '[name].[contenthash].js',
  },
Copy the code

Note that when we modify one of the files, the hash values of the packaged products of the other files can also be changed.

For example, if there are three modules in the project and they are all packaged into three separate JS files, the module dependencies are as follows:

graph TD
module1 --> module2 --> module3

When we modify module1, we only expect the js filename hash for module1 to change. In fact, the names of all three files have been changed. The reason is that we don’t package the runtime files separately, and a change in the name of one file changes the contents of another, creating a chain reaction. Use the following configuration:

optimization: {
    runtimeChunk: 'single',
}
Copy the code

This packaging generates a separate runtime file that also needs to be deployed and loaded (before the other files can be executed).

In this way, when we modify module1, only js files corresponding to module1 and runtime file names are changed to optimize the cache.