1 Babel兼容
1.2 Babel按需加载规则(useBuiltIns)
在Babel>7的条件下,支持通过useBuiltIns参数来实现按需加载必要的垫片。
useBuiltIns=false
不会自动引入垫片。
useBuiltIns=entry
通过@babel/preset-env
插件,按照浏览器环境按需加载需要的模块来替换对core-js
的直接引用。
该方式会以浏览器环境配置为准,全量引入最低浏览器所需要的所有垫片。
例如输入源码为:
import "core-js/stable";
import "regenerator-runtime/runtime";
再Chrome72中,上面代码会被@babel/preset-env
输出为:
import "core-js/modules/es.array.unscopables.flat";
import "core-js/modules/es.array.unscopables.flat-map";
import "core-js/modules/es.object.from-entries";
import "core-js/modules/web.immediate";
useBuiltIns=usage
该方式会根据不支持的浏览器环境且根据扫描到的代码中需要的垫片,自动引入不支持的模块。
例如输入为:
const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);
再IE11中会替换为:
import "core-js/modules/es.array.includes";
import "core-js/modules/es.array.iterator";
import "core-js/modules/es.object.to-string";
import "core-js/modules/es.set";
const set = new Set([1, 2, 3]);
[1, 2, 3].includes(2);
1.2 浏览器环境配置
Babel是根据browserslist来配置浏览器环境的。
browserslist的查询规则如下:
browserslist
key inpackage.json
file in current or parent directories.
We recommend this way..browserslistrc
config file in current or parent directories.browserslist
config file in current or parent directories.BROWSERSLIST
environment variable.- If the above methods did not produce a valid result
Browserslist will use defaults:
> 0.5%, last 2 versions, Firefox ESR, not dead
.
1.3 Next默认配置
看了Next.js(8.1.0)的源码,没有找到浏览器兼容的配置,所以可以认为是默认使用了规则 > 0.5%, last 2 versions, Firefox ESR, not dead
.
而且Babel配置为useBuiltIns: false
1.4 优化
根据Next.js文档对于Babel的描述:
- 可以通过
.babelrc
来配置 - 可以通过配置
next/babel
的参数来进行配置 - Next.js8.1.0对应的依赖为:
"@babel/core": "7.1.2"
,"webpack": "4.29.0"
,"@babel/runtime-corejs2": "7.1.2"
- The modules option on
"preset-env"
should be kept tofalse
otherwise webpack code splitting is disabled.
1.4.1 优化策略v1
通过对比useBuiltIns的选项,可以发现useBuiltIns=usage是最优的方案,它可以真正只加载我们需要的垫片。
.babelrc
:
{
"presets": [
[
"next/babel",
{
"preset-env": {
"useBuiltIns": "usage",
"modules": false,
"debug": true,
},
}
]
]
...
}
.browserslistrc
:
为什么是 > 0.25%?
其他情况按照具体业务来配置,原则当然是版本越高越好。
> 0.25%
last 4 versions
ie 8-11
Android >=4.4
iOS >= 8.4
Firefox > 20
1.4.2 优化策略v2
没有银弹🙁
上面配置完,基本上我们源码里面的垫片都能自动挂上了。但是....,在IE9/Android 4.4/iOS8上还是会报错。
Set/Map,找不到,数组没有includes等。
这是因为:
- 只有Webpack通过Babel的目录才会处理
- 如果使用
useBuiltIns=usage
,Babel 默认会假设处理的文件使用的是ES modules规范,也就是使用(import和export)语法。node_modules里面的第三方库一般都是以CommonJS的规范发布的。
所以因为React位于node_modules中,而且react发布版本已经变成了CommonJS的规范。所以需要真正的适配node_modules里面的所有模块还需要进一步配置。例如下面这个配置,但是实际上,node_modules的发布风格百花齐放,按照下面这个链接配置完之后会有各种问题。
how-do-i-use-babels-usebuiltins-usage-option-on-the-vendors-bundle
对比过网上为了解决第三方库中的垫片问题,基本上有三种方式:
- 最简单的就是使用
useBuiltIns=entry
,根据当前的需要支持的浏览器环境,全局导入所有垫片。 - 为第三方库单独添加全局垫片,一般第三方库会说明需要什么垫片,例如React JavaScript Environment Requirements
- 使用垫片服务,polyfill.io,会自动根据当前浏览器的UA自动添加缺失的功能,缺点就是代码不需要的也会加上。这种方案属于方案1的优化版。
综合下来,目前使用了方案2。
在next.config.js
中的webpack
加入:
config.entry = async () => {
const entries = await originalEntry();
if (entries['main.js']) {
entries['main.js'].unshift('./static/js/polyfills-legacy.js');
}
return entries;
};
static/js/polyfills-legacy.js
:
// React
import 'core-js/es6/map';
import 'core-js/es6/set';
// Next.js
import 'core-js/fn/string/starts-with';
1.5 小结
- 通过配置
.babelrc
文件中的preset-env
给业务代码自动加上垫片 - 通过全局垫片
polyfills-legacy.js
按照实际业务需要补上node_modules
中模块的垫片。
2 Webpack打包优化
2.1 目前打包分析
以insure页面为例,nextjs对应的资源有:
https://res.qixin18.com/v3/h5/_next/static/RRt1cbeJiwK-C3QWNcKTC/pages/product/insure.js
(547.82 KB)https://res.qixin18.com/v3/h5/_next/static/RRt1cbeJiwK-C3QWNcKTC/pages/_app.js
(341.34 KB)https://res.qixin18.com/v3/h5/_next/static/chunks/commons.e06dbd81189492fbc49d.js
(205.13 KB)https://res.qixin18.com/v3/h5/_next/static/runtime/main-17f8ae1732f73a182272.js
(63.6 KB)
从上图可以看出存在大量重复的包引用
2.1.1 main.js
main.js 主要是Next.js核心。 从上图可以看出包含了:
- 垫片(core-js/@babel/runtime-core2)
- Next核心(next-server/next)
- 工具类(url/querystring/unfetch.mjs)
2.1.2 _app.js
app.js 主要是对应业务代码中的app.js文件
从上图可以看出包含了:
- 垫片(core-js/@babel/runtime-core2)
- Redux(
pages/*/store
) - 工具类(immutable.js/moment.js/loadash/axios/antd/exif.js...)
2.1.3 commons.js
commons.js主要是公用的库 从上图可以看出包含了:
- 垫片(core-js/@babel/runtime-core2)
- react
- antd
2.1.4 insure.js
insure.js主要页面的业务代码 从上图可以看出包含了:
- 垫片(core-js/@babel/runtime-core2)
- antd
- 工具类(moment.js/immutable.js/redux)
- 业务代码
2.2 Webpack SplitChunksPlugin
Webpack SplitChunksPlugin默认打包策略
- New chunk can be shared OR modules are from the node_modules folder
- New chunk would be bigger than 30kb (before min+gz)
- Maximum number of parallel requests when loading chunks on demand would be lower or equal to 5
- Maximum number of parallel requests at initial page load would be lower or equal to 3
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
automaticNameMaxLength: 30,
name: true,
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}};
2.3 客户端默认打包策略
if (!isServer) {
const cacheGroups = config.optimization.splitChunks;
console.log(cacheGroups);
}
// 默认值:
splitChunks {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
commons: { name: 'commons', chunks: 'all', minChunks: 78.5 },
react: {
name: 'commons',
chunks: 'all',
test: /[\/]node_modules[\/](react|react-dom)[\/]/
},
styles: { name: 'styles', test: /.+(css)$/, chunks: 'all', enforce: true }
}
}
定义如何根据静态引用
和动态引用
来复用模块
。详情看上面的链接,配置为all
表示使用最优策略来复用模块。
splitChunks.cacheGroups.default: false
把Webpack中的默认splitChunks.cacheGroups.default
禁用。
splitChunks.cacheGroups.vendors: false
把Webpack中的默认splitChunks.cacheGroups.vendors
禁用。
splitChunks.cacheGroups.commons
配置commons
缓存组,包名为commons
,
splitChunks.cacheGroups.commons.minChunks = totalPages > 2 ? totalPages * 0.5 : 2
这里表示当一个模块被一半以上的页面引用的时候,打包到common.js
中,否则待在单独页面的包中。
splitChunks.cacheGroups.react
这里的配置是把所有的React打包在common.js中。
2.4 优化思路
从2.1中可以知道,大部分重复的包在于Moment.js/Core.js/exif.js/immutable.js/redux.js等。
思路一:所有在node_module中的包都打包在common.js中
有点,简单粗暴,但是无法控制真正的按需加载,有可能一个node_modules中的一个小模块只被一个页面引用,实际上也会加载到commons.js中,这样导致很浪费流量。这也是为什么在默认的Next.js中,使用只有一半以上页面引用才会放到commons.js中的原因。
思路二:按照实际情况做配置。
- core.js/redux.js/immutable.js等基础库放到一个组
main.js
。 - 工具类,moment.js/axios.js等工具类放到一个组
vendors.js
。 - 其他的包保留默认策略,只有一半以上的页面引用才打包的
commons.js
。
最终代码如下:
if (!isServer) {
const cacheGroups = config.optimization.splitChunks.cacheGroups;
delete cacheGroups.react;
cacheGroups.default = false;
// 1.基础库和next.js的基础main放到一起:next-server
cacheGroups.main = {
name: 'main',
test: /[\/]node_modules[\/](next-server|next|core-js|regenerator-runtime|@babel)[\/]/,
enforce: true,
chunks: 'all',
priority: 20,
};
// 1.基础库和next.js的基础main放到一起:react
cacheGroups.react = {
name: 'main',
test: /[\/]node_modules[\/](react|react-dom|react-redux|redux|immutable|redux-immutable)[\/]/,
enforce: true,
chunks: 'all',
priority: 20,
};
// 2.工具类
cacheGroups.vendors = {
name: 'vendors',
test: /[\/]node_modules[\/](axios|lodash|moment)[\/]/,
enforce: true,
chunks: 'all',
priority: 20,
};
// 3. 其他的包保留默认策略,只有一半以上的页面引用才打包的commons.js。
// commons: { name: 'commons', chunks: 'all', minChunks: 78.5 },
}
另外一些没用的包已经清理掉:exif-js
2.5 优化结果
大小是Parsed size
文件名称 | 优化前 | 优化后 | 变化 | 优化百分比 |
---|---|---|---|---|
insure.js | 547.82 KB | 364.25 KB | -183.57 KB | |
_app.js | 341.34 KB | 152.6 KB | -188.74 KB | |
commons.js | 205.13 KB | 0 | -205.13 KB | |
main.js | 63.6 KB | 311.43 KB | +247.83 KB | |
vendors.js | 0 | 103.17 KB | +103.17 KB | |
insure页面 | 1,157.89 KB | 931.45 KB | -226.44 KB | -19.55 % |
全部 | 20.61 MB | 12.39 MB | -8.22 MB | -39.88 % |
前后Webpack Bundle Analyzer文件:
优化前:
Webpack-Bundle-Analyzer-client.html
优化后:
Webpack-Bundle-Analyzer-client-optimization
2.6 小结
从Next.js的源码可以看出,Next.js对于拆包这块做得还是很粗糙的。
优化策略:
- 对于基础包,包括
Next.js
、babel
、React.js相关包
放到main.js
中,这一层应该包含几乎所有页面都共用的包
。 - 对于工具包,包括
moment.js
、lodash
、axios
等放到vendor.js
中,这一层应该包含大部分页面都公用的包
。 - 其他情况公用的代码,包括node_modules中的和业务中的,放到
common.js
中,这一层应该包含了一半页面用到的公用包
。
从上面的策略可以看出,各个层次之间其实没有严格的区分,更多是根据项目的实际情况来决定,另外有一些思考:
- 实际上拆包不仅要从单个页面去看,还要从整个项目去看,例如有些小页面不需要依赖太多的外部包,但是很多外部包又打包在
main.js
等基础包中,会影响原来的单个页面的效率。 - 另外不仅要从node_modules去看,还要从业务代码去看,目前业务代码的打包策略是
只有一半以上的页面引用才打包的commons.js
,这只是大概的优化策略,如果想要做到更好,更精细的效果,还需要从实际业务出发去拆包。 - 最后还需要考虑文件大小和包大小之间的平衡。