前端性能优化-webpack篇

Author Avatar
我爱吃包子 9月 22, 2019

前端性能优化-webpack篇

本文代码已提交至 github,链接: https://github.com/wangcheng007/webpack4TSReact16tTest

webpack 是一个 js 代码模块化的打包工具,藉由它强大的扩展能力,随着社区的发展,逐渐成为一个功能完善的构建工具。在使用 webpack 的时候,如果你不注意使用它的方式方法很有可能会发生意料之外的问题,比如性能问题。我们都知道 webpack 可以帮助我们打包和压缩代码,所以我们优化的方向也是这两个。

  • 1、减少 webpack 的构建时间。

  • 2、优化 webpack 的打包体积。

本文所有的优化方法都是基于 webpack 4 。

1 优化 webpack 构建速度

webpack 在启动后,会从 entries 入口出发,然后寻找并解析所有依赖。所以我们的优化方向可以有两个,减少寻找文件所用的时间、优化解析文件所用的时间。

1.1 webpack 文件查找策略

我们需要先了解下 webpack 的文件查找策略。我们知道项目中可以使用绝对路径、相对路径和模块路径来进行文件/模块的引入。我们先了解下对三种不同的引入 webpack 是怎么查找文件的。

绝对路径
1
import Test from '/Users/lanjing/Documents/code/temp/test.js';

由于我们获取到了文件的绝对路径,所以不需要再查找。

相对路径
1
import Test from './test.js';

webpack 会以该路径为上下文,以产生模块的绝对路径。

模块路径
1
import React from 'react';

要了解 webpack 模块路径的文件查找方式,我们要先了解下 module.paths 这个值,在任意文件夹新建 js 文件,内容是 console.log(module.paths) 。然后执行这个,其结果可能如下。

module.paths

了解了 module.paths 这个值,下面我们就来说下 webpack 针对模块路径的查找方式。

  • 1、设置 module.paths 的值,也就是模块搜索路径。

  • 2、从 module.paths 的数组中取出第一个目录作为查找基准。

  • 3、直接从该目录中查找该文件,如果存在则结束查找,不存在则进行下一条查找。
  • 4、尝试添加.js、.json、.node 后缀后查找,如果存在文件,则结束查找。如果不存在,则进行下一条。
  • 5、尝试将 require 的参数作为一个包来进行查找,读取目录下的 package.json 文件,取得 main 参数指定的文件。
  • 6、尝试查找该文件,如果存在,则结束查找。如果不存在,则进行第 4 条查找。
  • 7、如果继续失败,则取出 module.paths 数组中的下一个目录作为基准查找,循环第 2 至 6 个步骤。
  • 8、如果继续失败,循环第 2 至 7 个步骤,直到 module path 中的最后一个值。
  • 9、如果仍然失败,则抛出异常。
1.2 webpack.resolve 配置

webpack.resolve 选项能设置模块如何被解析。所以我们第一步要配置好 webpack.resolve 这个配置项。知道了上面模块的查找策略,那么有没有办法绕过一层层目录直接去目标文件夹寻找我们用到的模块文件呢?

resolve.modules

resovle.modules 设置模块的查找路径,避免一层层查找文件,直达目标文件夹。我的配置:

1
2
3
4
5
6
resolve: {
modules: [
path.resolve(__dirname, 'node_modules'),
'node_modules'
]
}

设置了 resolve.modules 绝对路径会直接去指定的目录查找文件,不再一层一层递归去查找。

resolve.extensions

上面解决了递归查找目标文件的问题,那接下来我们需要解决的是文件名后缀的问题。可以通过 resolve.extensions 配置来解决。我的配置:

1
2
3
resolve: {
extensions: ['.jsx', '.js', '.tsx', '.ts', '.json']
}

webpakc 会根据我们配置的文件后缀去进行匹配,匹配到文件最退出,匹配不到换下一个后缀名去匹配。尽量将频率高的写在前面、引入非模块文件时尽量写文件后缀

1.3 不要让 loader 做太多事情

最常见的优化方式是,用 inclue 或者 exclude 来帮我们避免不必要的编译。使用 cache-loader 来缓存我们的编译结果。我的配置:

1
2
3
4
5
6
7
8
9
10
11
12
rules: [{
test: /\.js$/,
include: [
/node_modules\/resolve-pathname/,
/node_modules\/value-equal/,
/node_modules\/type-detect/
],
use: [
'cache-loader',
'babel-loader',
]
}]
1.4 externals 配置

防止将某些 import 的包打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖。我的配置:

1
2
3
4
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
}

2 优化 webpack 打包体积

2.1 区分环境

webpack 4 区分环境可以直接在 webpack 中配置 mode 字段来区分,不需要再使用 DefinePlugin 来注入环境变量(但是如果要处理页数逻辑,还是需要手动注入的)。develop 环境注重代码的构建速度和开发体验,production 提供代码优化(比如压缩等)。比如我在代码中就通过环境来加载不同的 react 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function copyStaticSource2Assets() {
const env = process.env.NODE_ENV;
const DEVSTATICSOURCE = [
'/node_modules/react/umd/react.development.js',
'/node_modules/react-dom/umd/react-dom.development.js'
];
const PRODSTATICSOURCE = [
'/node_modules/react/umd/react.production.min.js',
'/node_modules/react-dom/umd/react-dom.production.min.js'
];
const STATICSOURCE = env === 'production' ? PRODSTATICSOURCE : DEVSTATICSOURCE;

STATICSOURCE.forEach((v) => {
const filePath = path.join(__dirname, '../../', v);
const targetFilePath = path.join(config.outputPath, v);
if (fs.existsSync(targetFilePath)) {
return;
}
fs.copySync(filePath, targetFilePath);
});
}

判断环境加载 react 不同的版本,在开发环境中加载 umd 版本,在生产环境中加载 压缩后的 min 版本。

2.2 压缩代码

压缩代码的方式方法有很多,在这里我写出我用的几个插件。css 优化 mini-css-extract-plugin 和 optimize-css-assets-webpack-plugin。js 优化 terser-webpack-plugin。

mini-css-extract-plugin 和 optimize-css-assets-webpack-plugin

这两个插件一个是用来分离 css 的,一个是用来压缩 css 的。其具体用法可以看 链接1 链接2,就不详细说用法了。

terser-webpack-plugin

terser-webpack-plugin 是 webpack 4 内置的压缩 js 的插件,支持 es6 的压缩。具体用法可以看链接

2.3 提取公共代码和分割代码

这块我按照我的理解,将代码分割成了以下几个部分。

  • pages => 页面独立的 js 和 css
  • commons => 自定义的公共方法
  • components => 自定义的公共组件
  • venders => 安装的 npm 包压缩后的内容

其配置可以在最后的链接查看。

附录

附上打包后的文件目录。

打包后的文件

总结

本文算是一篇笔记式的文章,主要是说一些常用的 webpack 配置和插件,这些配置/插件多少对性能有些帮助,能够帮你更好更舒服的完成开发以及使用体验。本文的配置我已上传至我的 github,可以在这里查看。