跳到主要内容

webpack优化(2)

优化 babel-loader

babel-loader始终是项目处理任务最多的 loader,尤其是 React 开发过程中,有大量的 JSX 需要去解析,编译。从babel-loader的配置项入手可以进行一些优化。babel-loader使用的插件集合主要是@babel/preset-env@babel/preset-react

@babel/preset-env

@babel/preset-env是一个负责将 JS 代码编译成兼容性更强的低版本 JS 代码的插件集合,同时也会按需为支持目标浏览器引入polyfill

配置项

配置项类型默认值含义
targetsString\|Array\|Object{}配置浏览器的版本或者 nodejs 的版本
bugfixesBooleanfalse取决于targets设定,根据targets来编译目标浏览器支持的最新版本的语法;babel 8 版本后会自动启用
specBooleanfalse启用更多规范,会导致编译速度减慢
looseBooleanfalse宽松的编译规则,正常情况下,编译会尽可能遵循 ECMAScript 6 的语义,但是宽松模式会不严格,看起来像是手写的代码,见——Babel 6: loose mode
modules"amd"\|"umd"\|"systemjs"\|"commonjs"\|"cjs"\|"auto"\|false"auto"将 ES Module 转换为其他类型模块语法的规则
debugBooleanfalse将 preset 使用的 plugin 和 polyfill 输出到控制台,这个配置不会受到 webpack 的stats的影响
includeArray[]自定义采用的插件名称
excludeArray[]排除使用的插件
useBuiltIns"usage"\|"entry"\|falsefalse定义如何处理 polyfill
corejs23{version: 2\|3,proposals: boolean}2仅在useBuiltIns: usage或者useBuiltIns: entry的时候才生效,定义core-js的版本,version必须是数字 2 或者 3
forceAllTransformsBooleanfalse禁用所有编译
configPathStringNode.js 进程的当前工作目录配置指定了browserslist的目录
ignoreBrowserslistConfigBooleanfalse是否忽略browserslist配置
browserslistEnvObjectundefined配置不同开发环境下的browserslist
shippedProposalsBooleanfalse是否启用浏览器已经支持的提案

配置 modules

modules配置项默认会根据 babel 内部的caller判断浏览器是否支持 ES Modules 的一些特性,例如是否支持静态import或者动态import()等语法;从而选择进一步将 ES Module 转换为 CommonJS 的那种require语法。

由于 webpack 内置的 tree shaking 功能依赖于 ES Modules 的静态语法分析,但是目前modules的支持程度也不是太高,IE 11 是完全不支持的,所以看情况选择是否将modules禁用。

配置 targets

targets这个配置项对应的是browserlist的定义的浏览器范围,具体的参考我的这篇文档 —— browserslist | icodex.

默认情况下babel并不会指定targets也不会像browserslist那样自动使用defaults的配置规则,如果不在@babel/preset-env中配置targets,那么它就会默认把 ES6+ 的 JS 代码全部编译成 ES5 的形式

通过开启@babel/preset-envdebug配置项可以清楚的在控制台看到@babel/preset-env使用的plugin有多少。

image-20200917113713934

在开发环境配置targets始终使用最新的 Chrome 版本,可以减少引入的 plugin 数量,提高编译速度:

{
targets: isDevelopment
? "last 1 chrome version"
: "> 1%, last 2 versions, Firefox ESR, ie >= 11, not dead",
}

image-20200917114914492

targets还支持配置成一个对象,包含以下属性:

  • esmodules:默认是false,指定目标浏览器支持 ES Modules 语法
  • node"current" | true,指定针对当前 node 的版本进行编译
  • safari"tp",指定针对 safari 的技术预览版进行编译
  • browsers:指定一个browserslist规则数组,不过配置项会被直接使用targets覆盖。

配置 bugfixes

babel 7版本中,bugfixes的默认值是false,在babel 8中计划将其默认值改成true

默认情况下,@babel/preset-env或者其他的 Babel plugin 会对 ES 语法特性进行相关分组,例如 ES6 中支持对函数参数设置默认值,以及解构剩余参数等语法,如果开启bugfixes@babel/preset-env会根据targets判定浏览器的兼容范围,选择编译到目标浏览器支持的最接近的最新现代语法,这将导致已编译应用程序的大小显著减小,不仅优化 webpack 的构建速度,而且优化了生成的代码的体积。

配置 polyfill

安装 core-js

core-js#babelpreset-env

polyfill就是为旧浏览器提供新的 ES 语法的代码块,@babel/polyfill已经在 7.4 版本以后被废弃了,因此目前在@babel/preset-env中使用的polyfill方案是结合core-js,所以使用polyfill还需要安装core-js,具体的参考我这篇文档 —— core-js | icodex

yarn add core-js@3

指定 corejs 版本

安装完core-js后还需要在@babel-preset-env中指定其版本,如果开启useBuiltIns,默认为2.0版本,这里肯定要改成3.0+的,因为3.0core-js改进很大。

指定 useBuiltIns

安装完core-js后还必须指定useBuiltIns,如果不指定不会有任何 polyfill 被添加进来。

image-20200917155333438

useBuiltIns主要有两个不同的值:entryusage,其不同点为:

  • entry只针对项目入口文件处全局注入的core-js进行优化转换,但是只针对core-js@3版本
  • usage只会在使用到具体语法的模块顶部为其引入polyfill模块,同时保证最终一个bundle文件下只会引入一次该模块

看起来好像没什么不同,但是实际使用usage的优化更好,下面来看个对比:

core-js的全局注入版本为例,指定corejs: "3.6"useBuiltIns: "entry",配置如下:

module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
{
debug: true, // 开启debug模式
targets: 'ie >= 8',
bugfixes: true,
modules: false,
corejs: {
version: 3,
proposals: true,
},
useBuiltIns: 'entry',
},
],
},
},
},
];
}

当指定useBuiltIns:"entry"时,会把core-js所有的针对目标targets的 polyfill 都打包进来,那简直是噩梦!当我只在入口使用了SetArray.from两个 feature 的时候,打包了 1 分钟 生成了一个150KB的 polyfill chunk

import 'core-js';

console.log(Array.from(new Set([1, 2, 3, 2, 1])));

而当指定useBuiltIns:"usage"时,只会根据代码引入需要的 polyfill,相应的打包时间和 chunk 体积就减小很多了,大概只有20KB

image-20200917163433398

因为直到[email protected]版本,useBuiltIns: usage还不够稳定,有时候一些需要的polyfill 并不会自动添加进来,所以可能旧的项目使用会有一点问题。作为兼容性和按需引入更好的选择,可以使用下文core-js-pure@babel/plugin-transform-runtime结合的方案。

指定 shippedProposals

shippedProposals指定在代码中使用浏览器中已经支持部分ts39提案语法,这些语法如果在目标targets支持,将不会进行编译转换,具体有以下这些:

使用 @babel/plugin-transform-runtime

Babel 会使用一些非常小的辅助性的代码插入到需要编译的源代码中,有时候这些代码是重复的,会增加代码体积。通过@babel/plugin-transform-runtime这个 plugin 可以禁用 Babel 自动对每个文件的 runtime 注入;然后通过安装@babel/runtime将 Babel 的辅助代码作为一个独立的依赖模块来引入,这样就可以避免编译后的代码中重复出现辅助性的代码,减小代码体积。

yarn add @babel/plugin-transform-runtime @babel/runtime -D
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime'],
},
},
],
},
};

@babel/preset-react

@babel/preset-react preset 负责转换 JSX 等语法。

配置项

配置项类型默认值含义
runtime"classic"或"automatic""classic"是否自动导入 JSX 转换后的函数,默认是不会
developmentBooleanfalse是否开启开发环境,针对开发环境会启用辅助开发的插件,例如:@babel/plugin-transform-react-jsx-self@babel/plugin-transform-react-jsx-source
throwIfNamespaceBooleanfalse是否在使用 XML 命名空间的标记名是抛出错误;例如<f:image />形式,虽然 JSX 规范允许这样做,但是默认情况下是被禁止的,因为 React 的 JSX 目前并不支持这种方式
importSourceStringreact设置函数导入来源的名称
pragmaStringReact.createElement替换编译 JSX 表达式时使用的函数
pragmaFragStringReact.Fragment设置 JSX fragments 语法转换后的函数
useBuiltInsBooleanfalse是否使用原生内置的 polyfill,而不是通过其他插件进行 polyfill
useSpreadBooleanfalse当展开props的时候,使用 inline object 形式,而不是使用 Babel 拓展或者Object.assign复制对象

inline object 形式:

{
(firstName = 'john'), (lastName = 'walter');
}

针对不同环境配置@babel/preset-react,以在开发环境使用辅助开发的插件

module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
[
'@babel/preset-react',
{
development: isDevelopment, //开发环境
useBuiltIns: true,
},
],
],
},
},
},
];
}

@babel/plugin-transform-runtime

Babel 会使用一些非常小的辅助性的代码插入到需要编译的源代码中,有时候这些代码是重复的,会增加代码体积。通过@babel/plugin-transform-runtime这个 plugin 可以禁用 Babel 自动对每个文件的 runtime 注入;然后通过安装@babel/runtime将 Babel 的辅助代码作为一个独立的依赖模块来引入,这样就可以避免编译后的代码中重复出现辅助性的代码,减小代码体积。

yarn add @babel/plugin-transform-runtime @babel/runtime -D
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime'],
},
},
],
},
};

@babel/plugin-transform-runtime会进行以下三项任务:

  • 当项目中使用生成器或者异步async函数的时候,自动引入@babel/plugin-transform-runtime
  • 使用core-js polyfill,可以通过corejs配置
  • 默认移除 Babel 的辅助代码,使用@babel/runtime/helpers代替,也就是上文说的减少代码体积的优化,可以通过helpers选择是否开启

配置项

配置项类型默认值含义
corejsfalse
2,3
{version:2或3,proposals:boolean}
false指定core-js库的版本
helpersBooleantrue是否将 Babel 插入的辅助代码切换为模块调用
regeneratorBooleantrue是否将生成器函数转化为生不会污染全局范围的运行时生成器
useESModulesBooleanfalse是否使用辅助代码转换不通过@babel/plugin-transform-modules-commonjs的代码
absoluteRuntimeBooleanStringfalse
versionStringfalse指定@babel/runtime-corejs的版本

polyfill

这个插件的另一个用途就是结合pure版本的core-js-pure库来做 polyfill,简化core-js-pure的导入语法,例如原本需要使用模块导入的语法,会自动进行转换。

// 本来的core-js-pure用法
import from from 'core-js-pure/stable/array/from';
import flat from 'core-js-pure/stable/array/flat';
import Set from 'core-js-pure/stable/set';
import Promise from 'core-js-pure/stable/promise';

from(new Set([1, 2, 3, 2, 1]));
flat([1, [2, 3], [4, [5]]], 2);
Promise.resolve(32).then(x => console.log(x));

// 引入@babel/plugin-transform-runtime的简化写法
Array.from(new Set([1, 2, 3, 2, 1]));
[1, [2, 3], [4, [5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));

使用@babel/plugin-transform-runtime结合core-js-pure需要安装对应的@babel/runtime

corejs对应安装的库
falseyarn add @babel/runtime
2yarn add @babel/runtime-corejs2
3yarn add @babel/runtime-corejs3

默认情况下,@babel/plugin-transform-runtime只会采用稳定的 whatwg 规范内容或者 ES 规范内容,但是可以通过corejs.proposals配置项指定其采用尚未稳定的提案。

Note:@babel/plugin-transform-runtime@babel/preset-env都有corejs配置项,不要搞混了,而且不要同时配置,否则会冲突。

module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: {
version: 3,
proposals: true,
},
},
],
],
},
},
],
},
};