### 4. 懒加载3类常见问题
在实践中实现模块懒加载的常见问题主要有:
1. 遗留直接导入导致目标模块没有被拆分#
第一类问题的表现是:修改为动态导入import()后不生效,打包构建的产物中,目标模块没有被拆分为独立文件。
这个问题常见的原因是目标模块有多处直接导入(import * from 'module'),只改了部分目标模块的直接导入,仍然遗留了部分。
解决方案可以考虑:直接在目标模块中加debugger断点,在开发环境运行后,根据调用栈,排查哪些组件有导入目标模块。
例如,下图中,直接在content.tsx中写上debugger,在项目运行时,用Devtool的源代码标签页(Source)排查调用栈(Call Stack),一层层检查哪些文件调用了这个目标模块。
确认有没有遗漏的直接导入后,有的话,全部改成动态导入。

2. 被其他代码拆分规则覆盖导致懒加载不生效#
第二类问题的表现也是动态导入import()不生效,目标模块没有被拆分为独立文件。
但是原因却不同,一般是有其他代码拆分规则,例如Webpack配置的缓存组splitChunk.cacheGroup命中了动态导入模块,导致目标模块不能被拆分为独立区块(chunk),也就不能产生独立的产物文件。
可行的解决方案是:检查cacheGroups配置,确认是否有缓存组的匹配规则(test属性)匹配到了目标模块。
如果有,将其chunks区块类型属性从'all'改为'initial',表示这个cacheGroups只包含initial类型的模块,即直接导入的模块。动态导入的模块,将会被排除出cacheGroups,从而将目标模块拆分成独立的产物文件。
3. 预期之外的非目标模块被懒加载处理#
第三类问题的表现是预期之外的非优化目标模块却被打包进了拆分出的独立文件中。
可能的原因是多个模块之间导入、导出关系复杂,例如NPM包内部的依赖关系,非目标模块可能是目标模块依赖的依赖,从而被拆分到了独立产物文件。
通常来说,这种情况是符合预期的,因为非目标模块也算是目标模块依赖的一部分,被拆分出来不会对优化效果产生负面影响。
如果不希望非目标模块被拆分打包,可以考虑为这个模块,在其预期归属模块中,额外增加一处直接导入,从而避免被拆分到独立产物文件中。
我们之前改造的markdown-to-jsx模块没有被拆分,就是遇到了上述第2类问题:缓存组cacheGroup相关问题,在项目的Webpack配置中有一个vendor缓存组,匹配所有来自node_modules的模块(test: /[\/]node_modules[\/]/,),并且其区块类型属性chunks,也是设置的'all',包含了我们想要改成异步加载、动态导入的markdown-to-jsx模块。
把区块类型属性chunks改为'initial',即可将markdown-to-jsx模块从vendor缓存组中释放出来,最终产生一个独立的产物文件。
// production.config.js
splitChunks: {
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
// chunks: 'all', // 指定vendors区块包含同步、异步加载的2类模块。
chunks: 'initial', // 指定vendors区块只包含同步加载的模块。
},
},
},5. 修复问题后再次验证#
再次运行npm run build-with-bundle-analyzer,就能看到可视化图中,路由组件、NPM包markdown-to-jsx模块,所有的懒加载目标模块都被正确拆分成了独立产物文件:
markdown-to-jsx模块:mardown-to-jsx.${hash}.js@/pages/login模块:login.${hash}.js@/pages/home模块:home.${hash}.js- …

3. 验证,量化与评估#
1. 验证#
基于上文的改动,我们可以在本地开发环境,除了通过webpack-bundle-analyzer-plugin可视化图表,
验证懒加载拆分了哪些模块、多大体积,初步确认懒加载的优化效果。
除了验证优化效果,建议也在开发环境大量调试前端项目业务逻辑,充分测试懒加载目标模块的运行情况,其功能逻辑是否正常。
特别是断网等极端情况下,懒加载模块无法加载时的状况,也要确保项目稳定性。
如果因为懒加载模块的文件加载失败,应该考虑通过try {} catch () {}或React的<ErrorBoundary>组件进行保护处理,避免页面报错、崩溃等问题。
2. 量化#
1. 加载JS体积指标#
懒加载优化对加载JS总体积会有直接的影响,我们可以基于第6节中介绍的《加载资源总体积指标》,增加过滤条件,统计出前端项目加载JS总体积的量化指标。
请看示例图:

▶点击展开:加载JS脚本总体积图表的 Grafana 配置JSON
{
"datasource": {
"uid": "grafanacloud-prom",
"type": "prometheus"
},
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"barAlignment": 0,
"lineWidth": 1,
"fillOpacity": 0,
"gradientMode": "none",
"spanNulls": false,
"insertNulls": false,
"showPoints": "auto",
"pointSize": 5,
"stacking": {
"mode": "none",
"group": "A"
},
"axisPlacement": "auto",
"axisLabel": "",
"axisColorMode": "text",
"axisBorderShow": false,
"scaleDistribution": {
"type": "linear"
},
"axisCenteredZero": false,
"hideFrom": {
"tooltip": false,
"viz": false,
"legend": false
},
"thresholdsStyle": {
"mode": "off"
}
},
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "decbytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 1
},
"id": 25,
"options": {
"tooltip": {
"mode": "multi",
"sort": "none"
},
"legend": {
"showLegend": true,
"displayMode": "table",
"placement": "right",
"calcs": [
"lastNotNull"
]
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "sum(ResourceSize{type="Script"})",
"fullMetaSearch": false,
"hide": true,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "avg(ResourceSize{type="Script"})",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "B",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "min(ResourceSize{type="Script"})",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "C",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "max(ResourceSize{type="Script"})",
"fullMetaSearch": false,
"hide": false,
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "D",
"useBackend": false
}
],
"title": "Script 加载资源总体积 ResourceSize",
"type": "timeseries"
}2. FCP和LCP#
懒加载优化后,通常可以显著减少页面加载的JS体积,相应地也会使页面初始化时的性能开销有所降低,所以观察FCP和LCP指标的变化,也可以量化懒加载的优化效果。
3. 评估#
优化完成后,我们就可以在本地环境,基于加载JS体积指标评估优化效果,并在上线生产环境后进一步确认效果是否符合预期。
以我们的前端优化示例项目 fe-optimization-demo 为例,懒加载的优化效果如下:
| 对比项 (单位:KB) | 优化前(无懒加载) | 优化后(懒加载) | 差异 |
|---|---|---|---|
首页(/home)加载JS体积 | 98.2![]() | 85![]() | -13.2 KB (-13% ) |
文章页(/article)加载JS体积 | 98.2 | 98.1![]() | 0(0%) |
文章页(/signin)加载JS体积 | 98.2 | 82.8![]() | -13.2 KB (-13% ) |
对于FCP和LCP指标的优化效果,很大程度上取决于前端项目的业务逻辑,进而决定懒加载具体能节省多少JS、CSS加载体积。
如果节省的体积较多,FCP和LCP2项指标应该也有10%以上的变化。
如果节省的体积较少,也没关系,只要这2项指标没有变差,我们仍然可以基于JS体积指标的变化评估得出优化正向收益。
小结#
在这一节中,我们首先学习了模块懒加载的核心原理:
- 代码模块化
- 动态导入语法:
import()
进而,配合示例项目,进行了一次懒加载优化,介绍了按路由、按体积分析懒加载目标的思路。
讲解了懒加载实施过程中常见的3类问题:
- 多处直接导入,为全部改成动态导入。
- 被Webpack配置的缓存组
splitChunk.cacheGroup等其他代码拆分规则命中了拆分目标模块 - 多个模块之间导入、导出关系复杂,非目标模块可能是目标模块依赖的依赖。
最后,讲解了验证懒加载功能、量化优化效果的各种手段。




