# webpack 高级

优化 webpack:

  1. 提升开发体验
  2. 提升打包构建速度
  3. 减少代码体积
  4. 优化代码运行性能

# 1. 提升开发体验

介绍:

  • 浏览器运行的是 webpack 编译后的代码,很难看懂
  • 一旦出错,报错位置 是 编译后的文件的出错代码位置,定位到源码位置比较困难
  • SourceMap 就是编译后的代码与源代码的映射,运行的代码一旦出错,就可以定位到源代码错误位置的行和列

配置:

module.exports = {
  mode: 'development',
  /*
    优点:打包编译速度快,只包含行映射
    缺点:没有列映射
   */
  devtool: 'cheap-module-source-map',
  // ...
};

module.exports = {
  mode: 'production',
  /*
    优点:包含行/列映射
    缺点:打包编译速度更慢
   */
  devtool: 'source-map',
  // ...
};

参考:devtool (opens new window)

# 2. 提升打包构建速度

# 2.1. HMR

说明:

  • HotModuleReplacement (HMR/热模块替换)
  • 在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。

CSS 热替换:

  • 配置

    devServer = {
      // ...
      hot: true, // 默认已开启
    }
    
  • style-loader 已经实现热替换接口,一旦样式发生变化,就用新的 <style> 替换掉旧的。

JS 热替换:

  • 默认情况下,一旦更改 JS 代码,就会刷新页面

  • 手动将某个模块添加到热替换

    if (module.hot) {
      // 一旦 sum.js 发生变化,就会将新模块重新执行一次
      // 如果此模块有副作用,就会出问题
      module.hot.accept('./js/sum');
    }
    

# 2.2. oneOf

说明:

  • 打包时每个文件都要把所有 rule.test 过一遍,看是否匹配,匹配则执行相应 loader
  • oneOf 用于,一旦匹配第一个就停止,不再继续匹配下去

配置:

module.rules = [
  // 把所有规则包在一个 rule 里,用 oneOf
  {
    oneOf: [
      { test: /\.css$/, /* ... */  },
      { test: /\.less$/, /* ... */  },
      { test: /\.(png|jpe?g|gif|webp)$/, /* ... */  },
      { test: /\.(ttf|woff2?)$/, /* ... */  },
      // ...
    ]
  }
]

备注:

  • 针对文件特别多的项目,打包速度是有提升的

参考:

# 2.3. include / exclude

npm 安装的第三方库都在 node_modules 里,这些文件通常都是编译好的,不需要我们额外编译。

src 目录下的才是真正需要处理的。

以下两个配置,只能选择一个:

  • include: 包含
  • exclude: 排除

include/exclude 值:

  • string: To match the input must start with the provided string. I. e. an absolute directory path, or absolute path to the file.
  • RegExp: It's tested with the input.
  • function: It's called with the input and must return a truthy value to match.
  • array: At least one of the Conditions must match.
  • object: All properties must match. Each property has a defined behavior.
    • { and: [Condition] }: All Conditions must match.
    • { or: [Condition] }: Any Condition must match.
    • { not: [Condition] }: All Conditions must NOT match.

示例:

module.rules = [
  { include: path.resolve(ROOT_PATH, './src'), },
  { exclude: /node_modules/, },
  { exclude: (...args) => { /* ... */ return true; } },
  {
    include: [
      path.resolve(ROOT_PATH, './src'),
      /node_modules[/\]xyz/,
    ],
  },
  {
    include: {
      and: [/* ... */],
      or: [/* ... */],
      not: [/* ... */],
    }
  }
]

参考:

# 2.4. cache

说明:

  • 缓存 eslint 检查结果、babel 的编译结果,再次打包速度会更快

配置:

module.rules = [
  // ...
  {
    test: /\.js$/,
    loader: 'babel-loader',
    options: {
      /*
        the default cache directory in node_modules/.cache/babel-loader
        Default false
      */
      cacheDirectory: true,
      /*
        When set, each Babel transform output will be compressed with Gzip
        Default true
      */ 
      cacheCompression: false,
    },
  },
]

plugins = [
  new ESLintWebpackPlugin({
    /*
      The cache is enabled by default to decrease execution time.
    */
    cache: true,
    /*
      Specify the path to the cache location. Can be a file or a directory.
      Default: node_modules/.cache/eslint-webpack-plugin/.eslintcache
    */
    cacheLocation: path.resolve(ROOT_PATH, './node_modules/.cache/.eslintcache'),
  }),
]

参考:

# 2.5. thead

说明:

  • 提升打包速度,就是提升处理 JS 的速度
  • JS 的处理,主要涉及的工具 —— eslint、babel、Terser(JS 压缩,已内置,生产默认开启)
  • 可以开启多线程同时使用这些工具处理 JS

注意:

  • 每个进程启动就有大约要 600ms,在特别耗时的操作中使用比较好

  • 可以启动的进程数,就是 CPU 的核数

    const os = require('os');
    
    const threads = os.cpus().length;
    
    console.log('CPU 核数:', threads); // 4
    

安装:

npm i -D thread-loader

## "thread-loader": "^4.0.1",

配置:

const os = require('os');
const TerserPlugin = require('terser-webpack-plugin');

const THREADS = os.cpus().length - 1;

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.js$/,
        use: [
          {
            /*
              Runs the following loaders in a worker pool.
            */
            loader: 'thread-loader',
            options: {
              /*
                the number of spawned workers, 
                defaults to (number of cpus - 1) 
                or fallback to 1 when require('os').cpus() is undefined
              */
              workers: THREADS,
            },
          },
          { loader: 'babel-loader' },
        ],
      },
    ]
  },

  plugins: [
    new ESLintWebpackPlugin({
      /*
      Will run lint tasks across a thread pool. 
      The pool size is automatic unless you specify a number.
        Default: false
      */
      threads: THREADS, // boolean | number
    }),
    
  ],

  optimization: {
    // 压缩相关的放这里
    minimizer: [
      /*
        生产模式下,已开启此插件,额外配置的话就需要重新写
      */
      new TerserPlugin({
        /*
          Use multi-process parallel running to improve the build speed.
            Default: true. 
            Default number of concurrent runs: os.cpus().length - 1
        */
        parallel: THREADS, // boolean | number
      }),
    ],
  },
}

参考:

# 3. 减少代码体积

# 3.1. Tree Shaking

说明:

  • 摇树,移除未使用的 JS 代码
  • 特别是第三方 工具函数库、组件库,我们只使用了极少部分,却引入了全部
  • webpack 默认已开启此功能
  • 针对 ES Module

参考:

# 3.2. Babel

说明:

  • 默认情况下,babel 会给所有编辑的文件添加相同的辅助代码(运行时代码),如 _extend
  • 可以使用 @babel/plugin-transform-runtime 插件,将运行时代码作为一个单独的模块引入,避免为每个编译的文件都引入

安装:

npm i -D @babel/plugin-transform-runtime

## "@babel/plugin-transform-runtime": "^7.21.4",

配置:(babel.config.js)

module.exports = {
  presets: [ '@babel/preset-env' ],
  plugins: [ 
    /*
      A plugin that enables the re-use of Babel's injected helper code to save on codesize.
    */
    '@babel/plugin-transform-runtime' 
  ]
};

参考:

# 3.3. Image Minimizer

说明:

  • 压缩图片

安装:

# image-minimizer-webpack-plugin 使用 imagemin 工具
npm i -D image-minimizer-webpack-plugin imagemin

    ## "image-minimizer-webpack-plugin": "^3.8.2",
    ## "imagemin": "^8.0.1",

# imagemin 使用以下插件处理图片

# gif svg
npm i -D imagemin-gifsicle imagemin-svgo

    ## "imagemin-gifsicle": "^7.0.0",
    ## "imagemin-svgo": "^10.0.1",

# 无损压缩 (jpg png)
npm i -D  imagemin-jpegtran imagemin-optipng 

    ## "imagemin-jpegtran": "^7.0.0",
    ## "imagemin-optipng": "^8.0.0",

# 有损压缩 (jpg png)
npm i -D  imagemin-mozjpeg imagemin-pngquant

配置:

const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

optimization.minimizer = [
  // ...
  new ImageMinimizerPlugin({
    minimizer: {
      implementation: ImageMinimizerPlugin.imageminGenerate,
      options: {
        plugins: [
          ['gifsicle', { interlaced: true }],
          ['jpegtran', { progressive: true }],
          ['optipng', { optimizationLevel: 5 }],
          [
            'svgo',
            {
              plugins: [
                'preset-default',
                'prefixIds',
                {
                  name: 'sortAttrs',
                  params: {
                    xmlnsOrder: 'alphabetical',
                  },
                },
              ],
            },
          ],
        ],
      },
    },
  }),
]

备注:

参考:

# 4. 优化代码运行性能

# 4.1. Code Split

# 4.1.1. 说明

  • 将所有 JS 文件打包到一个文件中,体积太大了,使加载时间变长了
  • code split
    • 分割文件:将打包的文件分成多个
    • 按需加载:需要哪个就加载哪个

# 4.1.2. module、chunk、bundle

  • module: ESM、CommonJS
  • chunk: 1. 入口点; 2. 异步载入(import()); 3. splitChunks.cacheGroups.{cacheGroup}
  • bundle: 最终生成的文件

参考:Webpack 理解 Chunk (opens new window)

# 4.1.3. 动态导入(按需加载)

import('../common/math')
  .then(({ add, default: math }) => {
    console.log(add(1, 2))
  })

# 4.1.4. ESLint 配置(import() 语法

module.exports = {
  // 如果不支持 `import()` 语法,则配置以下插件
  plugins: [ 'import' ]
}

# 4.1.5. 异步 chunk 命名

webpack.config.js:

  • 参考:output/#outputchunkfilename (opens new window)

    module.exports = {
      output: {
        // This option determines the name of each output bundle.
        filename: 'static/js/main.js',
    
        // This option determines the name of non-initial chunk files.
        // 就算设置了 `[name]`,webpack 还是以 `[id]` 命令,这时就需要设置 Magic Comments
        chunkFilename: 'static/js/[name].js'
      } 
    }
    

Magic Comments:

# 4.1.6. splitChunks

默认配置:

module.exports = {
  //...
  optimization: {
    // This configuration object represents the default behavior of the SplitChunksPlugin
    splitChunks: {
      /*
        This indicates which chunks will be selected for optimization
        'initial': 初始 chunk, 入口点 chunk
        'async': 异步 chunk
        'all': 'initial' + 'async'
       */
      chunks: 'async',
      /*
        Minimum size, in bytes, for a chunk to be generated.
       */
      minSize: 20000,
      /*
        拆分后剩余的块的最小体积,开发模式默认为 0,生产模式默认为 minSize
       */
      minRemainingSize: 0,
      /*
        The minimum times must a module be shared among chunks before splitting.
       */
      minChunks: 1,
      /*
        Maximum number of parallel requests when on-demand loading. (按需加载时的最大并行请求数。)
       */
      maxAsyncRequests: 30,
      /*
        Maximum number of parallel requests at an entry point.
       */
      maxInitialRequests: 30,
      /*
        强制拆的阈值,默认大于 50KB 就强行拆
        忽略 minRemainingSize, maxAsyncRequests, maxInitialRequests 
       */
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

配置:

module.exports = {
  // ...
  optimization: {
    // ...
    splitChunks: {
      chunks: 'all',
    }
  },
}

参考:

# 4.1.7. 统一命名

config value desc
output.filename 'static/js/[name].js' entry (non-initial)
output.chunkFilename 'static/js/[name].chunk.js' non-initial chunk files
output.assetModuleFilename 'static/media/[name].[hash:8][ext][query]' image,audio,video files
MiniCssExtractPlugin.filename 'static/css/[name].css' css file
MiniCssExtractPlugin.chunkFilename 'static/css/[name].chunk.css' non-initial chunk css file

备注:

  • Asset Modules: rule.type 的值为 asset, asset/**
  • Asset Modules 可以使用 output.assetModuleFilename 统一配置名称规则
  • 使用 [name] 是避免多入口时名称冲突

参考:

# 4.2. Preload / Prefetch

说明:

  • 在浏览器空闲时,加载后续需要使用的资源
  • 加载资源,缓存,不执行

区别:

- preload prefetch
加载时机 立即加载资源 空闲时加载资源
优先级
资源 只能加载当前页面后续需要使用的资源 可以加载当前页面后续需要使用的资源,也可以加载下一个页面需要使用的资源
使用场景 当前页面优先级高的资源 下一个页面需要使用的资源
兼容性 95.81%, 详情 (opens new window) 78.76%, IE11+, 详情 (opens new window)

安装:

npm i -D @vue/preload-webpack-plugin

## "@vue/preload-webpack-plugin": "^2.0.0",

配置:

const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    // ...
    new PreloadWebpackPlugin({
      /*
        预加载的类型: 'preload' (默认), 'prefetch'
       */
      rel: 'prefetch',
      /*
        设置 as 的值,默认依赖文件的后缀名来设置,也可以显式制定
       */
      // as: 'script',

      /*
        哪些类别的 chunk 需要预加载: asyncChunks (默认值), all, initial
       */
      // include: 'asyncChunks',

      /*
        排除不需要预加载的 chunk,如 sourcemaps ,默认值: [/\.map/]
       */
      // fileBlacklist: [/\.map/]
    }),
  ]
};

参考:

# 4.3. Network Cache

解决的问题:

  1. 当缓存的资源发生变化时,能加载新的资源
  2. 当一个文件发生变化时,不会影响到使用该文件的其他文件

# 4.3.1. hash

说明:

  • 静态资源默认走浏览器强缓存,如果资源的 URL 不变化,不会再去服务器请求该资源
  • 为了使强缓存失效,必须改变 URL,要么改 query string 要么改文件名
  • webpack 给文件名添加 hash 值,一旦文件内容变化 hash 值就改变

hash:

  • fullhash (webpack4 是 hash)
    • 修改任意一个文件,所有文件的 hash 都会改变
  • chunkhash
    • 根据 chunk 生成 hash,chunk 内的 JS、CSS 文件共享这个 hash
  • contenthash
    • 根据文件内容生成 hash,只有文件内容变化了 hash 才会改变
    • 所有文件的 hash 都是独享的

配置:

config value desc
output.filename 'static/js/[name].[contenthash:8].js' entry (non-initial)
output.chunkFilename 'static/js/[name].[contenthash:8].chunk.js' non-initial chunk files
output.assetModuleFilename 'static/media/[name].[hash][ext][query]' image,audio,video files
MiniCssExtractPlugin.filename 'static/css/[name].[contenthash:8].css' css file
MiniCssExtractPlugin.chunkFilename 'static/css/[name].[contenthash:8].chunk.css' non-initial chunk css file

参考:

# 4.3.2. runtimeChunk

问题:

  • 源文件:

    src/
      common.js
      main.js
    
    main.js:
      
      import { add } from './common'
      console.log(add(1, 2))
    
  • 编译后:

    dist/
      common.111.js
      main.222.js
    
    main.222.js:
      
      import { add } from './common.111'
      console.log(add(1, 2))
    
  • common.js 发生变化后,重新编译:

    dist/
      common.333.js
      main.444.js
    
    main.444.js:
      
      import { add } from './common.333'
      console.log(add(1, 2))
    

为了避免修改一个文件,导致其它文件跟着变化,我们需要把 文件和 hash 的对应关系存起来,

这样,一旦文件变化,只会重新请求改变的文件和关系文件。

配置:

module.exports = {
  optimization: {
    /*
      true or 'multiple'
      
        adds an additional chunk containing only the runtime to each entrypoint. 
        This setting is an alias for:
        
          {
            name: (entrypoint) => `runtime~${entrypoint.name}`,
          }
      
      'single'
         
        creates a runtime file to be shared for all generated chunks.
        This setting is an alias for:
       
          {
            name: 'runtime',
          }
     */
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  }
}

参考:

# 4.4. core-js

说明:

  • @babel/preset-env 只能处理 ES6 语法,对 ES6 新增的 API 不做处理
  • core-js 就是专门处理 ES6 API 兼容性的 polyfill (垫片/补丁)

注意:

  • 如果使用 Promise 后,ESLint 报错

    const promise = Promise.resolve();
    promise.then(() => {
      console.log("hello promise");
    });
    
  • 请安装 @babel/eslint-parser

    npm i -D @babel/eslint-parser
    
  • 并配置 ESLint

module.exports = {
  // 支持最新的最终 ECMAScript 标准
  parser: '@babel/eslint-parser', 
};

安装:

npm i core-js

## "core-js": "^3.30.1"

手动全部引入 polyfill:

import 'core-js';

手动按需引入 polyfill:

import 'core-js/es/promise';

自动按需引入,配置 babel.config.js:

module.exports = {
  presets: [
    ['@babel/preset-env', {
      /*
        Adds specific imports for polyfills when they are used in each file. 
       */
      useBuiltIns: 'usage',
      /*
        string or { version: string, proposals: boolean }, defaults to "2.0". 
        The version string can be any supported core-js versions. For example, "3.8" or "2.0".
       */
      corejs: {
        /*
           It is recommended to specify the minor version otherwise "3" will be interpreted as "3.0"
         */
        version: '3.30',
        /*
          This will enable polyfilling of every proposal supported by core-js@3.30
         */
        proposals: true
      }
    }]
  ],
};

备注:

  • babel 的编译,依赖 browserslist 的设置

参考:

# 4.5. PWA

说明:

  • progressive web application, 渐进式网络应用程序
  • 离线后,刷新网页也可以继续访问
  • 静态资源都被缓存了,启动 ServiceWorkers 提供服务

安装:

npm i -D workbox-webpack-plugin

## "workbox-webpack-plugin": "^6.5.4"

配置:

const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new WorkboxPlugin.GenerateSW({
      /*
        这些选项帮助快速启用 ServiceWorkers,不允许遗留任何“旧的” ServiceWorkers
       */
      clientsClaim: true,
      skipWaiting: true,
    }),
  ]
};

使用:(在 main.js 中注册)

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('./service-worker.js')
      .then((registration) => {
        console.log('SW registered: ', registration);
      })
      .catch((registrationError) => {
        console.log('SW registration failed: ', registrationError);
      });
  });
}

备注:

  • 可以在 Cache Storage 中查看缓存的内容,具体位置如下:

    Chrome -> Developer tool -> Application 选项卡
    
    Storage/
      Cache Storage/
        workbox-precache-v2-http://127.0.0.1:8080
    

参考:

# 5. 总结

从四个角度对 webpack 和代码进行了优化:

  1. 提升开发体验

    • 使用 Source Map 让开发或上线时代码报错能有更加准确的错误提示。
  2. 提升 webpack 提升打包构建速度

    • 使用 HotModuleReplacement 让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
    • 使用 OneOf 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
    • 使用 Include/Exclude 排除或只检测某些文件,处理的文件更少,速度更快。
    • 使用 Cache 对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
    • 使用 Thead 多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
  3. 减少代码体积

    • 使用 Tree Shaking 剔除了没有使用的多余代码,让代码体积更小。
    • 使用 @babel/plugin-transform-runtime 插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
    • 使用 Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
  4. 优化代码运行性能

    • 使用 Code Split 对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
    • 使用 Preload / Prefetch 对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
    • 使用 Network Cache 能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
    • 使用 Core-js 对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
    • 使用 PWA 能让代码离线也能访问,从而提升用户体验。
本章目录