Webpack优化总结

缩小文件搜索范围

  1. 优化loader配置。

由于Loader对文件的转化操作很耗时,需要让尽可能少的文件被Loader处理。为了尽可能少的让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。

  1. 优化resolve.modules配置

resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是 [‘node_modules’],含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。

当安装的第三方模块都在项目根目录下的 ./node_modules 目录下时,没有必要按照默认方式去一层层的寻找,可以指明存放第三方模块下的绝对路径,以减少寻找。

1
2
3
4
5
6
7
module.exports = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')]
},
};
  1. 优化resove.mainFields配置

resove.mainFields 用于配置第三方模块使用哪个入口文件。安装的第三方模块中会有一个 package.json 文件用于描述这个模块的属性,其中有些字段用于描述入口文件在哪里,resolve.mainFields 用于配置采用哪个字段作为入口文件的描述。可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境中,针对不同的运行环境需要使用不同的代码。

为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main 字段去描述入口文件的位置,直接把mainFields设置为[main]

  1. 优化resolve.alias配置

resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径。

vue-cli3的默认配置已做次优化:

1
2
3
4
5
alias: {
'@': '/Users/biyangjun/gridsum/zeta/metaspace-ui/src',
vue$: 'vue/dist/vue.runtime.esm.js', // 直接将Vue库指向Vue源码中的runtime版本文件,从而跳过耗时的递归解析操作
src: '/Users/biyangjun/gridsum/zeta/metaspace-ui/src'
},

  1. 优化 resolve.extensions 配置

在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在,resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认为[‘.js’,’.json’]。

后缀尝试列表尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。

频率出现最高的文件优先放到最前面,做到尽快的退出寻找过程。

在源码中写入导入语句时,要经可能带上后缀,避免寻找过程。

  1. 优化module.noParse配置

module.noParse 配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库,例如 jQuery 、ChartJS, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。

注意被忽略掉的文件里不应该包含 import 、 require 、 define 等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。

使用可以多进程处理的Plugin

  1. 使用HappyPack

在整个Webpack构建流程中,最耗时的流程可能就是Loader对文件的转换操作,因为需要转换的文件太多,而且这些转换操作只能一个个挨着处理,HappyPack和核心原理就是把这部分任务分解到多个进程去并行处理,从而减少总的构建时间。

核心调度器的逻辑代码在主进程中,也就是运行着 Webpack 的进程中,核心调度器会把一个个任务分配给当前空闲的子进程,子进程处理完毕后把结果发送给核心调度器,它们之间的数据交换是通过进程间通信 API 实现的。核心调度器收到来自子进程处理完毕的结果后会通知 Webpack 该文件处理完毕。

Webpack4官方推出了 thread-loader, 把这个loader放置在其他loader之前,放置在这个loader之后的loader就会在一个单独的worker池中运行。但在worker池中运行的loader是受到限制的,例如:

  • 这些loader不能产生新的文件
  • 这些loader不能使用定制的loader API(也就是通过插件)
  • 这些loader无法获取webpack的选项设置
  1. 使用ParalleUglifyPlugin

在使用 Webpack 构建出用于发布到线上的代码时,都会有压缩代码这一流程。 最常见的 JavaScript 代码压缩工具是 UglifyJS,并且 Webpack 也内置了它。由于压缩JavaScript代码需要先把代码解析成用Objectt抽象表示的AST语法树,再去应用各种规则分析和处理AST,导致这个过程计算量巨大,耗时非常多。

ParalleUglifyPlugin会开启多个子进程,把对多个文件的压缩工作分配给多个子进程完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行,所以ParalleUglifyPlugin能更快的完成对多个文件的压缩工作。

Webpack文件监听的原理

在Webpack中监听一个文件发生变化的原理是定时的去获取这个文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。当发现某个文件发生了变化时,并不会立刻告诉监听者,而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者。

压缩代码

  1. 从UglifyJS入手

浏览器从服务器访问网页时获取的js、css资源都是文本形式的,文件越大网页加载时间越长。

为了提升网页加速速度和减少网络传输流量,可以对这些资源进行压缩,压缩的方法除了可以通过GZIP算法对文件压缩外,还可以对文本本身进行压缩。

目前最成熟的JavaScript代码压缩工具是UglifyJS,它会分析JavaScript代码语法树,理解代码含义,从而能做到诸如去掉无效代码,去掉日志输出代码,缩短变量名等优化。

UglifyJS如何改写配置以达到最优的压缩效果。

  • sourceMap:是否为压缩后的代码生成对应的SourceMap,默认不开启,开启后耗时会大大增加。建议不开启。
  • beautify:是否输出可读性较强的代码,即会保留空格和制表符,默认是。建议关闭。
  • comments:是否保留代码中的注释,默认为保留。可以关闭。
  • compress.warnings: 是否在UglifyJS删除没有用到的代码时输出警告信息,默认开启。可以关闭。
  • drop_console: 是否删除代码中的console语句,默认为不删除。可以开启删除,开启后不仅可以提升代码压缩效果,也可以兼容不支持console语句的IE浏览器。
  • collapse_vars:是否内嵌定义了但是只用到一次的变量,例如把 var x = 5; y = x 转换成 y = 5,默认为不转换。为了达到更好的压缩效果,可以设置为 true。
  • reduce_vars: 是否提取出出现多次但是没有定义成变量去引用的静态值,例如把 x = ‘Hello’; y = ‘Hello’ 转换成 var a = ‘Hello’; x = a; y = b,默认为不转换。为了达到更好的压缩效果,可以设置为 true。
  1. 压缩ES6

因为ES6的代码相比转换后的ES5代码有以下优点

  • 一样的逻辑ES6实现的代码量比ES5少
  • JS引擎对ES6中的语法做了性能优化。

所以在运行环境允许的情况下,要尽可能的使用原生的ES6代码去运行。但是UglifyJS只认识ES5语法的代码,为了压缩ES6代码,需要专门针对ES6代码的UglifyES。

UglifyES 和 UglifyJS 来自同一个项目的不同分支,它们的配置项基本相同,并且在给 Webpack 接入 UglifyES 时,不能使用内置的 UglifyJsPlugin,而是需要单独安装和使用最新版本的 uglifyjs-webpack-plugin。

优化配置和上面的配置一样。

  1. 压缩CSS

目前比较成熟可靠的css压缩工具是cssnano,基于PostCSS,把 cssnano 接入到 Webpack 中也非常简单,因为 css-loader 已经将其内置了,要开启 cssnano 去压缩代码只需要开启 css-loader 的 minimize 选项。

1
use: ['css-loader?minimize']

使用Tree Shaking

Tree Shaking 可以用来剔除JS上用不到的死代码,它依赖静态的ES6模块化语法,例如通过import和export导入导出。

栗子:

1
2
3
4
5
6
7
8
// 一个工具函数文件 util.js
export function a() {}
export function b() {}
export const d = 'd';
// 使用到util.js的main.js
import {a} from './utils.js'
// 经过Tree Shaking处理后的util.js
export function a() {}
我想吃鸡腿!