Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack入门必备(二):优化配置 #6

Open
psycholali opened this issue Apr 15, 2021 · 1 comment
Open

webpack入门必备(二):优化配置 #6

psycholali opened this issue Apr 15, 2021 · 1 comment

Comments

@psycholali
Copy link
Owner

之前写的《webpack入门必备(一):基础配置》主要介绍了webpack基础解析所需的loader/plugin。而随着日常webpack的使用,我们会更多关注如何构建更快、构建产物更小、构建产物符合规范...希望这篇文章可以让你找到答案。

一、webpack4的构建优化

1. 加快构建速度

1.1 优化配置

这里介绍的主要的几种优化配置如下所示:

  1. 缩小构建范围
  • exclude、include范围
  • noParse
  • IgnorePlugin
  1. 多进程
  • thread-loader/happypack
  1. 缓存
  • cache-loader/cacheDirectory,把loader的处理结果缓存到本地
  • Dll缓存,把一些不常变更的模块构建产物缓存在本地

如果你有没用过的配置可以接着看下面的具体使用方法,如果你已经很熟悉了则可以跳过此节~

1. exclude、include范围

配置来确保转译尽可能少的文件(exclude 的优先级高于 include)

const rootDir = process.cwd();

{
        test: /\.(j|t)sx?$/,
        include: [path.resolve(rootDir, 'src')],
        exclude: [
          /(.|_)min\.js$/
        ],
}

PS. 相比exclude可以多用include

2. noParse

如果一些库不依赖其它库的库,不需要解析他们,可以引入来加快编译速度。

noParse: /node_modules\/(moment|chart\.js)/

3. IgnorePlugin

忽略第三方包指定目录。 (他是webpack 内置的插件)

例如: moment (2.24.0版本) 会将所有本地化内容和核心功能一起打包,我们就可以使用 IgnorePlugin 在打包时忽略本地化内容(语言包),见下图。

plugins: [
  // 表示忽略moment下的locale文件夹内容
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]

image

4.1 thread-loader

把 thread-loader 放置在其它 loader 之前,那么它之后的 loader 就会在一个单独的 worker 池中运行。

// 项目中babel-loader一般耗时比较长,所以可以配置thread-loader 
rules: [ 
    { 
        test: /\.jsx?$/, 
        use: ['thread-loader', 'cache-loader', 'babel-loader'] 
    } 
] 

4.2 happypack

运行在Node.js上的webpack是单线程,将文件解析的任务拆分由多个子进程并发进行,然后子进程处理完任务后再将结果发送给主进程,提升项目构件速度。
(但是因为进程的分配和管理也需要时间,所以使用后不一定快,需要项目接入实验一下)

const Happypack = require("happypack");
module.exports = {
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: "Happypack/loader?id=js",
        include: [path.resolve(__dirname, "src")],
      },
      {
        test: /\.css$/,
        use: "Happypack/loader?id=css",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
        use: "Happypack/loader?id=file",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "public"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
    ],
  },
  plugins: [
    new Happypack({
      id: "js", //和rule中的id=js对应
      //将之前 rule 中的 loader 在此配置
      use: ["babel-loader"], //必须是数组
    }),
    new Happypack({
      id: "css", //和rule中的id=css对应
      use: ["style-loader", "css-loader", "postcss-loader"],
    }),
    new Happypack({
      id: "file", //和rule中的id=file对应
      use: [
        {
          loader: "url-loader",
          options: {
            limit: 10240, //10K
          },
        },
      ],
    }),
  ],
};

5. cache-loader/cacheDirectory

在性能开销较大的loader处使用,将构建结果缓存中磁盘中。
(默认存在node_modueles/.cache/cache-loader目录下。 )

cacheDirectory例子:

rules: [
      {
            test: /\.(j|t)sx?$/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                },
              }
       }
 ]

cache-loader例子:

rules: [
    {
        test: /\.(css)$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'cache-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' }
        ]
      }
]

6. Dll缓存(动态链接库)

将复用性较高的第三方模块打包到DLL中,再次构建时直接复用,这样只需重新打包业务代码。
(注意是DLL缓存是大大缩短了首次构建时间,像之前的cache-loader优化都是缩短rebuild时间

使用相关插件:

  • DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
  • DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。

具体步骤:
(1) 新增一个webpack配置去编译DLL文件([name].dll.js[name].manifest.json

// 新增一个webpack-dll.config.js配置文件

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const distPath = path.resolve(__dirname, 'dll');
module.exports = {
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom'],
    // 把项目需要所有的 polyfill 放到一个单独的动态链接库
    polyfill: [
      'core-js/fn/object/assign',
      'core-js/fn/object/entries',
      ...
    ],
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称(react 和 polyfill)
    filename: '[name].dll.js',
    path: distPath,
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值(_dll_react)
      name: '_dll_[name]',
      context: process.cwd(),
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dll', '[name].manifest.json'),
    }),
  ],
};
// package.json里新增dll的构建命令
"scripts": {
    "dll": "webpack --config webpack-dll.config.js",
}

(2) dev构建时,告诉 Webpack 使用了哪些动态链接库

// webpack.config.js文件

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

plugins: [
    // 使用的动态链接库(react和polyfill的)
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'react.manifest.json'),
    }),
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'polyfill.manifest.json'),
    }),
    ...
]

(3) html template里引入文件

因为我这里只是本地构建加速,所以就以dev的方式引入

<script src="./dll/polyfill.dll.js?_dev"></script>
<script src="./dll/react.dll.js?_dev"></script>

到这DLL就配好了。有些人可能比较好奇react.dll.jsreact.manifast.js到底是什么文件,做了什么事?你看看他两个文件就知道啦~

  • react.dll.js其实主要就是所引用模块的代码集合
  • react.manifast.js则写明包含哪些模块、模块路径
// react.dll.js文件部分内容如下所示。
var _dll_react = (function(modules) {
  // ... 此处省略 webpackBootstrap 函数代码
}([
  function(module, exports, __webpack_require__) {
    // 模块 ID 为 0 的模块对应的代码
  },
  function(module, exports, __webpack_require__) {
    // 模块 ID 为 1 的模块对应的代码
  },
  // ... 此处省略剩下的模块对应的代码 
]));


// react.manifast.js文件部分内容如下所示。
{
  // 描述该动态链接库文件暴露在全局的变量名称
  "name": "_dll_react",
  "content": {
    "./node_modules/process/browser.js": {
      "id": 0,
      "meta": {}
    },
    // ... 此处省略部分模块
    "./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
      "id": 42,
      "meta": {}
    },
     ...
}

1.2 检测工具

常用工具:speed-measure-webpack-plugin
使用方法:用其来包裹 Webpack 的配置

image

2. 构建产物方面

2.1 减小构建产物大小、提高复用率

这里介绍的主要的几种优化配置如下所示:

  • optimization.splitChunks分包
  • babel配置@babel/plugin-transform-runtime
  • tree-shaking

具体使用:

1. optimization.splitChunks分包

将业务代码和第三方依赖库进行分包,减小index.js的大小;
抽离多页应用的公共模块,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。

 optimization: {
    minimize: false,
    moduleIds: 'named',
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 6,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        polyfill: {
          test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/,
          name: 'polyfill',
          priority: 70,
          minChunks: 1,
          reuseExistingChunk: true
        },
        lib: {
            test: /[\\/]node_modules[\\/]/,
            name: 'lib',
            chunks: 'initial',
            priority: 3,
            minChunks: 1,
         },
       ...
      }
    }
 }

2. babel配置

提取所有页面所需的helper函数到一个包里,避免重复注入

"plugins": [
    "@babel/plugin-transform-runtime"
    ...
]

3. tree-shaking

如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。

(1) 具体配置

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

const config = {
 // 生产模式下tree-shaking才生效
 mode: 'production',
 optimization: {
  // Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
  usedExports: true,
  minimizer: [
   // 删除死代码的压缩器
   new TerserPlugin({...})
  ]
 }
};

(2) 哪类代码会被shake掉?以下有一些事例

// no tree-shaking
import Stuff from './stuff';
doSomething(Stuff);

// tree-shaking
import Stuff from './stuff';
doSomething();

// tree-shaking
import './stuff';
doSomething();

// no tree-shaking
import 'my-lib';
doSomething();

// 全部导入 no tree-shaking
import _ from 'lodash';

// 具名导入 tree-shaking
import { debounce } from 'lodash';

// 直接导入具体的模块  tree-shaking
import debounce from 'lodash/lib/debounce';

(3) 什么叫有副作用的代码?
只要被引入,就会对应用程序产生重要的影响。 (一个很好的例子就是全局样式表,或者设置全局配置的js文件。)

(4) 有副作用的代码我们不希望被shake,我们可以配置如下

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

(5) 注意,babel配置需要配modules: false,忽略import/export代码编译

const config = {
 presets: [
  [
   '@babel/preset-env',
   {
     // commonjs代码不能被tree-shaking
     // 所以babel保留我们现有的 es2015 import/export 语句,不进行编译
    modules: false
   }
  ]
 ]
};

2.2 检测工具

常用工具:webpack-bundle-analyzer
使用方法:用其来包裹 Webpack 的配置

image

3. 产物检查

ES check

生产环境构建时,会检查构建产物里是否存在es6语法。有则抛出错误并提示你去进行babel编译,这样避免了构建产物不合要求的情况。

image
image

具体使用例子:

// package.json 命令里加上es-check检查
"dist:basic": "rimraf public && cross-env NODE_ENV=production webpack --config webpack-dist.config.js && es-check es5 ./public/**/*.js"

二、webpack5的构建优化

1. 速度优化

1.1 编译缓存

编译缓存就是在首次编译后把结果缓存起来,在后续编译时复用缓存,从而达到加速编译的效果。
webpack5默认开启编译缓存,缓存默认是在内存里,你可以自定义。

module.exports = {
	cache: {
        // 将缓存类型设置为文件系统
        type: "filesystem", 
        // 缓存的位置(默认是node_modules/.cache/webpack)
        cacheDirectory: path.resolve(__dirname, '.temp_cache'), 
     
        // 指定构建过程中的代码依赖。webpack将使用这些项目以及所有依赖项的哈希值来使文件系统缓存无效。
        buildDependencies: {
     
            // 当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。 
            config: [__filename], 

            // webpack.config、loader和所有从你的配置中require的模块都会被自动添加。如果有其他的东西被构建依赖,你可以在这里添加它们
      },
      
      // 指定缓存的版本。当需要更新配置缓存时,通过设置此版本使缓存失效。
	  version: '1.0'  
    }
}

一些参数注解

  • cache: true 就是 cache: { type: 'memory' } 的别名
  • type: 'filesystem'|'memory'。
    如果设置'memory'则缓存在内存且不能配置其他信息,设置成'filesystem'就可以配置更多信息。默认开发模式使用的是'memory',生产模式是false。
  • version: 当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。

1.2 长效缓存 Long-term caching

长效缓存指的是能充分利用浏览器缓存,尽量减少由于模块变更导致的构建文件hash值的改变,从而导致文件缓存失效。
(由于moduleId和chunkId确定了,构建的文件的hash值也会确定。)

1.2.1 引子

  1. chunk、module都是什么?
  • module:每一个可被导入导出的源码js、css文件就是一个module。
  • chunk:module经webpack依赖分析、打包生成的单独文件块。如:入口entry里的文件、SplitChunks抽离的公共代码
  • bundle:chunk后面经过编译/压缩打包等处理后就变成了bundle,bundle文件直接被html文件引用。

image

  1. webpack提供了以下3种哈希值,分别是什么意思?有啥优缺点?
  • hash 所有bundle文件都是同一个hash。(【缺点】不修改文件的情况下rebuild后hash会更新)
  • chunkhash 同一个entry/及entry引用的chunk文件都是同一个hash。(【缺点】修改chunk文件内容后,这个hash不变)
  • contenthash 一个文件一个hash,修改哪个文件哪个文件的hash就改变。(【缺点】如果删除一个entry里的chunk,entry和chunk及好多个文件的hash都变了,不利于长效缓存。
    比如只是jsx删除引用的一个css文件 好多bundle文件的hash就都变了。)

1.2.2 webpack4实现长效缓存

之前需要通过如下配置达到长效缓存:

plugins: [
- new webpack.NamedModulesPlugin(),
+ new webpack.HashedModuleIdsPlugin(),

或者配置

optimization.moduleIds = 'hashed’ 
optimization.chunkIds = 'named'

配置说明:

  • 在开发环境下使用 NamedModulesPlugin 来固化 module id,在生产环境下使用 HashedModuleIdsPlugin 来固化 module id(因为构建结果文件会更小)
  • 使用 NamedChunksPlugin 来固化 runtime 内以及在使用动态加载时分离出的 chunk 的 chunk id
    (NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块(异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:import(/ webpackChunkName: “lodash” / ‘lodash’).then() 这样就有 Name 了),external 模块是不会起作用的。)

1.2.3 Webpack5默认启用deterministic实现长效缓存

Webpack5采用新的算法,生产模式下默认启用如下配置不仅实现长效缓存,还减少了文件打包大小:

optimization.chunkIds: "deterministic"
optimization.moduleIds: "deterministic"
mangleExports: “deterministic"

PS.具体采用的算法还需要进一步深入研究~

2. 包构建大小优化

2.1 Node Polyfill脚本被移除

Webpack 4版本附带了大多数Node.js核心模块的polyfill,一旦前端使用了任何核心模块,这些模块就会自动应用,导致polyfill文件很大,但是其实有些polyfill是不必要的。
而现在webpack5将不会自动为Node.js模块添加Polyfills,需要开发者手动添加合适的Polyfills。

升级迁移至webpack5需要注意:

  • 尽可能尝试使用与前端兼容的模块。
  • 可以为 Node.js 核心模块手动添加 polyfill。错误消息将提示实现方法。
  • 包作者:使用 package.json 中的 browser 字段使包与前端兼容。提供浏览器的替代实现 / 依赖。

2.2 tree-shaking

1.嵌套tree-shaking
能够跟踪对export的嵌套属性的访问,分析模块的export和import的依赖关系,去掉未被使用的模块

// inner.js
export const a = 1;
export const b = 2;

// module.js
export * as inner from './inner';
// or import * as inner from './inner'; export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a); // 在此示例中,可以在生产模式下移除导出 b。

2.内部模块tree-shaking(深度作用域分析)
新属性optimization.innerGraph分析模块导出和导入之间的依赖关系,在生产模式下默认启用。

import { something } from './something';
function usingSomething() {
  return something;
}
export function test() {
  return usingSomething();
}
// 在使用 test 导出时才使用 something。

可以分析以下符号:

  • 函数声明
  • 类声明
  • 带有以下内容的 export default 或变量声明:函数表达式,类表达式,序列表达式,/#PURE/ 表达式,局部变量,导入绑定

3.package.json 中的“sideEffects”标志允许将模块手动标记为无副作用,从而在不使用它们时将其移除。
webpack 5 还可以根据对源代码的静态分析,自动将模块标记为无副作用。

更多Webpack5的内容推荐阅读:

@weaponic
Copy link

weaponic commented May 31, 2021

内部模块tree-shaking(深度作用域分析)
新属性optimization.innerGraph分析模块导出和导入之间的依赖关系,在生产模式下默认启用。

看了好多都是直接把官方翻译的那段放上来了,并不是很清楚说的什么意思?有具体的4/5的区别么?比如这段代码在4里面会正常打进去,5会把他摇掉?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants