preface

One of the most noticeable aspects of daily development is the amount of time it takes to compile and package a project, especially if the deployment is packaged more frequently.

For example, a previous project I worked on “cold start” took about 86 seconds to compile:

Can you stand it? Obviously not, so I decided to optimize it, otherwise it would be too bad for the development experience. Here are some things I did during the optimization process.

Analysis time module

View vue-CLI built-in configuration

Vue-cli-service inspect can be used to easily view the built-in configuration content of VUe-CLI. Enter: Vue inspect – mode production webpack. Config. Production. Js, will be in the project and the SRC directory to generate webpack. At the same level config. Production. Js file.

Analysis tools

To get time-consuming modules, you need some analysis tools, such as:

  • speed-measure-webpack-plugin
  • webpack-bundle-analyzer

speed-measure-webpack-plugin

This plugin can measure the speed of web package construction and output the compilation time of each module, which can help us find the time-consuming module better.

The vue. Config. Js configuration

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
module.exports = { ... .configureWebpack: (config) = >{... config.plugins.push(newSpeedMeasurePlugin(), ); }};Copy the code

Restart the

From the output below, you can easily see the corresponding time consuming module.


webpack-bundle-analyzer

This plugin visualizes the size of a web package output file and provides an interactive, scalable tree diagram.

The vue. Config. Js configuration

  const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = { ... .configureWebpack: (config) = >{... config.plugins.push(new SpeedMeasurePlugin(),
      + newBundleAnalyzerPlugin() ); }};Copy the code

Restart the

From the output below, you can easily see the size of each module when packed.


To optimize the

Thread-loader – Enables multi-threading optimization

According to the compilation time output of speed-measure-webpack-plugin, the following loaders have a large proportion of compilation time:

  • vue-loader
  • ts-loader
  • babel-loader
  • image-webpack-loader
  • postcss-loader

This section can be optimized with thread-loader because it can put the time-consuming content into a separate thread for execution, but not for all loaders because this process is also expensive.

Note: Use thread-loader only for time-consuming operations. Otherwise, using thread-loader may lead to a longer project build time because each worker is an independent Node.js process, which costs about 600ms. It also restricts cross-process data exchange and so on.

To recap, the “cold start” time of the previous project without using Thread-Loader is about 86 seconds:

After thread-loader is used for babel-loader only, the “cold start” time of the project is about 78 seconds:

It takes longer for other Laoder to use thread-Loader to find, so the best result after trying is obtained

Hard-source-webpack-plugin — Uses cache optimization

There are several ways to cache in Webpack:

  • cache-loader
  • hard-source-webpack-plugin
  • CacheDirectory flag for babel-loader

All of these caching methods have initial startup overhead, meaning they make “cold starts” longer, but secondary starts save a lot of time.

Vue-cli has built-in cacheDirectory flags for cache-loader and babel-loader. The corresponding configurations are as follows:

By default, babel-Laoder, TS-Loader, vue-loader have been cached during the “cold start”.

Configure the hard-source-webpack-plugin first:

  const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
+ const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = { ... .configureWebpack: (config) = >{... config.plugins.push(// Provide an intermediate cache for the module. The cache path is node_modules/. Cache /hard-source
        // Resolve undetected configuration changes
       + new HardSourceWebpackPlugin({
          root: process.cwd(),
          directories: [].environmentHash: {
            root: process.cwd(),
            directories: [].// The main reason for configuring files is to solve the problem that cache does not take effect due to configuration update
            // Plugin will rebuild part of the cache if the packet changes after configuration
            files: ['package.json'.'yarn.lock']}}),new SpeedMeasurePlugin(),
        newBundleAnalyzerPlugin(), ); }};Copy the code

The “cold start” and “secondary start” times after using the hard-source-webpack-plugin are as follows:

Reduce packing volume

Reduce js code size

In the packaged dist directory, look for console.log. The result is as follows:

You can see that the default configuration in vue-CLI does not remove console.log statements from js files, so this is something that can be further optimized.

Here you can use uglifyjs-webpack-plugin or terser-webpack-plugin to delete comments and compress JS code. The specific configuration can be directly linked to refer to.

Compress images

For some image pixel without high requirements of image resources for compression, here can be through image-webpack-plugin or image-minimizer-webpack-plugin image resource compression, specific configuration can be directly linked to refer to.

External extension — externals & CDN

The externals option is used to prevent import packages from being packaged into the bundle, and instead to obtain external dependencies at runtime

In simple terms, the JS should have been packaged in the bundle. Now, by configuring the externals option, we can make it a resource outside the bundle, namely a CDN resource, and request this resource when the code runs.

For example, the following is the configuration of externals in the project:

 chainWebpack: (config) = >{...// Import resources through CDN
      config.externals({
        echarts: 'echarts'.nprogress: 'NProgress'}); }Copy the code

DllPlugin plugin – Optimizes packaging time

The DllPlugin is responsible for packaging and breaking down bundles of libraries that are relatively stable (such as vue/ React family buckets) so that they do not need to be packaged again the next time they are packaged, thus greatly increasing build speed.

In WebPack4, DllPlugin has been integrated, so we only need to configure it

  • Create a DLL. Js file for simple configuration
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['echarts'.'element-ui'.'vue/dist/vue.esm.js'.'vue-router'.'vuex'],},output: {
    path: path.join(__dirname, 'target'),
    filename: '[name].js'.library: '[name]_[hash]',},plugins: [
    new webpack.DllPlugin({
      // The name attribute of DllPlugin needs to be consistent with libary
      name: '[name]_[hash]'.// Specify the current directory
      path: path.join(__dirname, '. '.'[name]-manifest.json'),
      // Context needs to be consistent with webpack.config.js
      context: __dirname,
    }),
  ],
};
Copy the code
  • inpackage.jsonConfiguration in filescriptScript:"dll": "webpack --config ./dll.js"
  • The installationwebpack-cliBecause the dependency was not installed in the original dependency and is required to run the script commandwebpack-cli
  • Executing script commandsnpm run dllTo generate thevendor-manifest.jsonFile. This file is used to makeDllReferencePluginBe able to map to corresponding dependencies

  • invue.config.jsIn the configurationDllReferencePluginPlug-ins that link to packaged dependencies
const { pathResolve } = require('./build/utils.js'); // eslint-disable-line
const devConfig = require('./build/webpack.dev.conf.js'); // eslint-disable-line
const buildConfig = require('./build/webpack.prod.conf.js');
// Analysis tools
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

// Resource cache
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

// Remove stable third-party libraries to avoid repeated packaging
const DllReferencePlugin = require('webpack').DllReferencePlugin;

// Public function
const { versionSet } = require('./build/utils'); // eslint-disable-line

// Whether it is a development environment
const isDevelopment = process.env.NODE_ENV == 'development';

const vueWebpackConfig = () = > {
  let envConfig = {};

  if (isDevelopment) {
    / / development
    envConfig = devConfig;
  } else {
    / / build
    versionSet();
    envConfig = buildConfig;
  }

  const vueConfig = {
    // Environment configuration. envConfig,productionSourceMap: isDevelopment, // Whether sourcdeMap is generated when the production package is built

    // Extend the WebPack configuration
    chainWebpack: (config) = > {
      // ============ Configure the alias ============
      config.resolve.alias
        .set('@build', pathResolve('.. /build')) // Build the directory
        .set(The '@', pathResolve('.. /src'))
        .set('@api', pathResolve('.. /src/api'))
        .set('@utils', pathResolve('.. /src/utils'))
        .set('@views', pathResolve('.. /src/views'));

      // ============ SVG processing ============
      const svgRule = config.module.rule('svg');
      // Clear all existing loaders.
      // If you do not do this, the following loader will be appended to the existing loader for this rule.
      svgRule.uses.clear();

      // Add the loader to be replaced
      svgRule.use('svg-sprite-loader').loader('svg-sprite-loader').options({
        symbolId: 'icon-[name]'});// ============ compressed image ============
      config.module
        .rule('images')
        .use('image-webpack-loader')
        .loader('image-webpack-loader')
        .options({ bypassOnDebug: true })
        .end();

      // ============ package analysis tool ============
      if(! isDevelopment) {if (process.env.npm_config_report) {
          config.plugin('webpack-bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin).end();
          config.plugins.delete('prefetch'); }}// ============ CDN resource import ============
      config.externals({
        // echarts: 'echarts',
        nprogress: 'NProgress'}); },configureWebpack: (config) = > {
      // Try to ensure that the file suffixes in the project are accurate
      config.resolve.extensions = ['.ts'.'.js'.'.vue'.'.json'];

      / / processing Babel - loader
      config.module.rules[12].use.unshift({
        loader: 'thread-loader'}); config.plugins.push(// Provide an intermediate cache for the module. The cache path is node_modules/. Cache /hard-source
        new HardSourceWebpackPlugin({
          root: process.cwd(),
          directories: [].environmentHash: {
            root: process.cwd(),
            directories: [].// The main reason for configuring files is to solve the problem that the cache does not take effect due to configuration updates. Plugin will rebuild part of the cache after configuration changes
            files: ['package.json'.'yarn.lock'],}}),/ / DllReferencePlugin plug-in
          new DllReferencePlugin({
            context: __dirname,
            // Manifest is the JSON file we packaged in step 2
            manifest: require('./vendor-manifest.json'),}),// Analysis tools
        new SpeedMeasurePlugin(),
        newBundleAnalyzerPlugin(), ); }};return vueConfig;
};

module.exports = vueWebpackConfig();
Copy the code

Other optimization

resolve.alias & resolve.extensions

Resolve. Alias is an alias used to create import or require to make module introduction easier

For example, here are some aliases defined in the project:

   chainWebpack: (config) = > {
      // Configure the alias
      config.resolve.alias
        .set('@build', pathResolve('.. /build')) // Build the directory
        .set(The '@', pathResolve('.. /src'))
        .set('@api', pathResolve('.. /src/api'))
        .set('@utils', pathResolve('.. /src/utils'))
        .set('@views', pathResolve('.. /src/views'));
  }
Copy the code

The resolve.extensions are specified as the corresponding file suffix to ensure useless look-ups and recursion when looking for modules, i.e. the file suffix in this configuration should be as few as possible.

Reduce unnecessary parsing — module.noparse

NoParse prevents Webpack from parsing any files that match a given regular expression. Ignored files should not contain import, require, define calls, or any other import mechanism. Build performance can be improved by ignoring large libraries.

For example, the configuration of module.noParse in vue-CLI is as follows:

Code level optimization

According to the contents of Webpack-bundle-Analyzer, it can be found that some JS and CSS modules are relatively large in size. At this time, we can find the corresponding file combing logic and optimize the code, such as encapsulating JS logic and extracting CSS styles. The most basic optimization is to write less repetitive styles and logic, which can also avoid some ineffective double compilations.

The last

These are some of the vue-CLI (Webapck based) optimizations you should try if you haven’t done them before.

Finally, take a look at the changes in production packaging and construction time before and after optimization: