creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.63k stars 211 forks source link

webpack 核心概念和优化指南 #60

Open creeperyang opened 1 year ago

creeperyang commented 1 year ago

从功能核心来说,webpack 是JS应用的打包工具(static module bundler)。webpack 会从入口(entry point)开始处理你的应用,构建依赖图,把多个模块(module)合并到一个或多个包(bundle)。

webpack 本身只能处理 JS/JSON 文件,它依赖各种 loader、plugin 来共同完成对复杂应用的支持。

(一)核心概念

loader vs plugin

Loaders are transformations that are applied to the source code of a module. Plugins are the backbone of webpack. Webpack itself is built on the same plugin system that you use in your webpack configuration!

loader 就是转换模块代码的工具函数,允许你预处理你要加载的文件。

plugin 是 webpack 的基石,而 webpack 也是基于同样的插件系统打造。plugin 本质是注册webpack的生命周期事件来做到 loader 做不到的事。

loader 顺序

module.exports = {
  module: {
    rules: [
      {
        use: ['a-loader', 'b-loader', 'c-loader'],
      },
    ],
  },
};

loader 是洋葱型顺序,pitch 从左到右,loader 本身从右到左执行。

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

module vs chunk

Every file used in your project is a Module

模块化编程中,完整的程序可以被分成完成特定功能的模块。在 webpack 里,一个文件就是一个模块。

编译过程中,modules are combined into chunks, Chunks combine into chunk groups。

chunk 有两种形式:

(二)优化构建效率

总的来说,想要提高打包/构建效率,要么是减少打包工作量,要么是提高打包速度。相关措施可以总结为:

  1. 并行打包:多线程/多进程打包。
  2. 更高性能的打包工具:利用esbuild、swc。
  3. 怎么减少工作量?缓存,以及缩减查找步骤、范围。

持久化缓存,提高二次构建性能

如果不想变更一个模块导致打包过程(可能几百个模块)重复一遍,那么我们需要利用缓存持久化来避免不必要的工作。

Cache the generated webpack modules and chunks to improve build speed.

配置缓存很简单,配置 cache 即可:

{
    cache: {
        type: 'filesystem', // 缓存到 memory 或 filesystem
        // 额外的依赖文件,当这些文件内容变化时,缓存会完全失效而执行完整的编译构建,通常可设置为项目配置文件
        buildDependencies: {
            config: [path.join(__dirname, 'webpack.dll_config.js')],
        },
        // 缓存文件存放的路径,默认为 node_modules/.cache/webpack
        cacheDirectory: 'node_modules/.cache/webpack',
        maxAge: 5184000000,
    },
}

配置完缓存后,测试的两次编译时间为 2047ms417 ms,效果显著。

关于 dll

dll 动态链接库本质也是缓存,即不经常改变的代码抽取成一个共享的库,然后直接使用。

通常:

  1. 打包dll库:通过单独的 webpack 配置(DllPlugin),打包得到 [name].dll.js 和 [name].manifest.json
  2. 引用dll库:另外的 webpack 配置(DllReferencePlugin、AddAssetHtmlWebpackPlugin)引入上面的文件。

由于从 webpack@4 开始,webpack 打包性能已经足够好,dll 模式被弃用

减少编译查找路径、编译范围(减少查找时间,减少需要编译的文件)

除了缓存,还可以缩减编译查找步骤、范围来减小工作量。

1. Rule 的 exclude/include/issuer 等多种方式减少查找范围

{
    module: {
        rules: [
            {
                test: /\.jsx?/,
                exclude: [/node_modules/],
            },
            {
                test: /\.css$/,
                include: [
                    path.resolve(__dirname, 'app/styles'),
                    path.join(__dirname, 'vendor/styles/'),
                ],
            },
        ],
    },
}

2. noParse 跳过编译

使用 noParse 让 webpack 不要去解析特地文件,对忽略一些大型类库,可以节省很多时间。

module.exports = {
  module: {
    noParse: /jquery|lodash/,
  },
};

3. 配置 resolve 减少查找范围

尽量减少webpack的查找范围。

module.exports = {
  resolve: {
    // 视情况可减少
    importsFields: ['browser', 'module', 'main'],
    // 视情况可减少
    extensions: ['.js', '.json', '.wasm'],
    modules: [path.resolve(__dirname, 'src'), 'node_modules'],
  },
};

提升编译性能(通过跳过不必要的编译步骤等)

1. 开发阶段禁止产物优化

除了压缩强烈建议开发阶段关闭,其它几项看个人需要。

2. 合适的 sourcemap 配置

{
    devtool: __DEV__ ? 'eval' : 'source-map'
}

3. 减少 watch 文件范围

module.exports = {
  watchOptions: {
    aggregateTimeout: 600,
    ignored: '**/node_modules',
  },
};

4. experiments.lazyCompilation 需要时再编译

{
// define a custom backend
backend?: ((
  compiler: Compiler,
  callback: (err?: Error, api?: BackendApi) => void
  ) => void)
  | ((compiler: Compiler) => Promise<BackendApi>)
  | {
    /**
     * A custom client.
    */
    client?: string;

    /**
     * Specify where to listen to from the server.
     */
    listen?: number | ListenOptions | ((server: typeof Server) => void);

    /**
     * Specify the protocol the client should use to connect to the server.
     */
    protocol?: "http" | "https";

    /**
     * Specify how to create the server handling the EventSource requests.
     */
    server?: ServerOptionsImport | ServerOptionsHttps | (() => typeof Server);

},
entries?: boolean,
imports?: boolean,
test?: string | RegExp | ((module: Module) => boolean)
}

多线程/多进程处理

// TerserWebpackPlugin 的多进程模式
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};

利用更高性能的 swc/esbuild 来压缩

两者通过 Rust/Go 提高了性能。

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        minify: TerserPlugin.swcMinify,
        // `terserOptions` options will be passed to `swc` (`@swc/core`)
        // Link to options - https://swc.rs/docs/config-js-minify
        terserOptions: {},

        minify: TerserPlugin.esbuildMinify,
        // `terserOptions` options will be passed to `esbuild`
        // Link to options - https://esbuild.github.io/api/#minify
        // Note: the `minify` options is true by default (and override other `minify*` options), so if you want to disable the `minifyIdentifiers` option (or other `minify*` options) please use:
        // terserOptions: {
        //   minify: false,
        //   minifyWhitespace: true,
        //   minifyIdentifiers: false,
        //   minifySyntax: true,
        // },
      }),
    ],
  },
};

(三)优化

webpack caching (长期缓存)

理解 hash/chunkhash/contenthash

首先了解 webpack 中 hash 相关概念: https://webpack.js.org/configuration/output/#template-strings

  1. hash/fullhash,Compilation-level,本次编译(compilation)的 hash。可以理解为项目级别的,任意改动基本都会导致 hash 变更。

  2. chunkhash,Chunk-level,chunk 的 hash。不同 chunk 之间互不影响。

  3. contenthash,Module-level,模块相关内容的 hash。

cache 第一步:Output FileNames

推荐使用 contenthash 来防止有改动但缓存未失效的问题(用户页面没访问最新内容)。

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "[name].[contenthash].js",
    path: path.resolve(__dirname, "dist"),
    clean: true
  }
};

cache 第二步:拆出模板代码(Extracting Boilerplate)

  1. runtime 代码拆到单独文件
module.exports = {
  optimization: {
    runtimeChunk: "single" // 等价于
    /**
     * runtimeChunk: {
     *     name: 'runtime',
     * },
     */
  }
};

optimization.runtimeChunk设置为"single" 会为所有生成的 chunk 创建一个共用的 runtime。如果设置为false则会在每个 chunk 里面嵌入 runtime。

  1. 公共库单独拆出
module.exports = {
    entry: './src/index.js',
    output: {
      filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  optimization: {
    runtimeChunk: "single",
    splitChunks: {
        // 所有 node_modules 内的公共包单独打包为 "vendors.contenthash.js"
       cacheGroups: {
         vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'vendors',
           chunks: 'all',
         },
       },
    },
  }
};

以上两步让公共包和runtime可以不因为业务代码变更而缓存失效。

cache 第三步:Module Identifiers

假设这样一个情况:新增一个文件'./src/print.js' 并被 './src/index.js' (main)作为依赖引入使用。重新编译:

  1. The main bundle changed because of its new content.
  2. The vendor bundle changed because its module.id was changed.
  3. And, the runtime bundle changed because it now contains a reference to a new module.

我们发现 main/vendors/runtime 文件名(hash)都变了。理论上 vendors 应该不变。

引入 optimization.moduleIds="deterministic" 可以解决这类问题:

另外 optimization.chunkIds="deterministic" 也是 production 模式默认的。

Concatenate Module(Scope Hoisting)

optimization.concatenateModules允许webpack去查找可以安全串联的模块来串联/合并到一个模块。

webpack之前会把每个module都放入单独的wrapper function,但这会拖慢执行速度。concatenateModules 可以像 RollupJS 之类尽可能把module安全合并,合并到同一个闭包下面。

image

image

对 Concatenate Module 而言:

creeperyang commented 1 year ago

一些细节问题

1. output.library 及相关的 commonjs | commonjs2 等差别

一个问题,commonjs2commonjs 有什么差别?

一句话解释

基于webpack来给出更多信息

有如下 webpack 配置来生成 umd 格式的代码:

module.exports = {
  //...
  output: {
    library: {
      name: 'MyLibrary',
      type: 'umd',
    },
  },
};

编译产出:

(function webpackUniversalModuleDefinition(root, factory) {
  if (typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if (typeof define === 'function' && define.amd) define([], factory);
  else if (typeof exports === 'object') exports['MyLibrary'] = factory();
  else root['MyLibrary'] = factory();
})(global, function () {
  return _entry_return_;
});
18boys commented 1 year ago

已收到,稍候会处理best wish~