webpack5 innerGraph 算法 bug 导致的页面白屏

目录

image

⚠️ 截止目前 webpack 最新版本 5.75.0 存在的 bug

1. 问题背景

M 项目技术改造把 webpack4 升级到 webpack5, 预发环境验收时发现一个页面白屏了,可以说离上线到背事故单仅差一步 ⚠️

2. 问题排查

经过 SourceMap 反解后, 找到了报错信息 TypeError: (0 , i.ZP) is not a function 对应到的 npm 包代码处。即如下图 reducer 函数就是 i.ZP 代码丑化前的名字

1
2
3
4
5
6
7
8
9
10
11
// index.js

import reducer, { getInitState, DEFAULT_PAGE_NUMBER, getDefaultLoading } from './reducer';

let usePagination = function (request, config) {
// ..

let _a = __read(useReducer(function (state, action) { return reducer(state, action, config); }, getDefaultState(configRef.current, cacheKey)), 2), rootState = _a[0], rootDispatch = _a[1];

// ...
};

2.1. 可能性1: ‘./reducer’ 文件没有 export default reducer?

答案是否定的, 如下代码确实是导出了 reducer 函数

1
2
3
4
5
6
7
8
9
// reducer.js

function reducer(state, action, config) {
var _a;
var cacheKey = action.payload.cacheKey;
return __assign(__assign({}, state), (_a = {}, _a[cacheKey] = baseReducer(state[cacheKey], action, config), _a));
}

export default reducer;

2.2. 可能性2: 出现了循环引用

由于本次代码的 diff 涉及的业务改动到的文件数量与 package.json 升级的 npm 包数量都过多, 通过二分法也会消耗不少时间, 于是先搁置该情况的可能

2.3. 其他可能性

接下来先查看一下打包后的 reducer.js 对应的代码来找到突破口, 其中有用的信息是

  1. e.ZP 即是向导出对象进行赋值
  2. 如果5224 == r.j为 false, 那么 e.ZP 为 null 就会导致本次问题

结论: webpack 篡改了代码, 添加了一个三元表达式导致条件不满足时导出为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
{
29971: function (t, e, r) {

e.ZP =
5224 == r.j
? function (t, e, r) {
var i,
o = e.payload.cacheKey;
return n(n({}, t), (((i = {})[o] = m(t[o], e, r)), i));
}
: null;
}
}

那么关键线索中 webpack 添加的 r.j 究竟代表的是什么 ?

于是在本地删除 TerserPlugin 等代码丑化插件后进行生产打包, 来查看 r.j 相关的更多信息。从打包后的代码看到的有用信息是

  1. r.j 丑化前的名字是 __webpack_require__.j
  2. __webpack_require__.j 旁多了注释 /* runtime-dependent pure expression or super */
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    29971: (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {

    function reducer(state, action, config) {
    var _a;
    var cacheKey = action.payload.cacheKey;
    return __assign(__assign({}, state), (_a = {}, _a[cacheKey] = baseReducer(state[cacheKey], action, config), _a));
    }

    /* harmony default export */ __webpack_exports__["ZP"] = ((/* runtime-dependent pure expression or super */ 5224 == __webpack_require__.j ? (reducer) : null));
    )
    }
    我们先搁置 r.j, 从可以全局搜索到的注释开始排查。发现是 webpack 的 PureExpressionDependency 往业务代码中添加的注释, 而 PureXxx 就容易联想到偶尔会看到的 __PURE__ 相关 API 了

webpack.js.org 上搜索关键词__PURE__, 就定位到了 webpack5 默认开启的一项优化 Inner-module tree-shaking

InnerGraph 算法的核心是更加智能化的配合 Tree Shaking, 它试图分析代码间的引用关系。比如你只有使用到了导出的 test 函数, 那么 usingSomething 函数将要被保留, 即 something 也需要保留下来

1
2
3
4
5
6
7
8
9
import { something } from './something';

function usingSomething() {
return something;
}

export function test() {
return usingSomething();
}

3. 问题解决

设置 optimization.innerGraph 为 false 来验证一下是否因为算法分析出错导致的本次白屏, 发现该页面恢复了正常 ✅

1
2
3
4
5
6
7
8
// webpack.config.js

module.exports = {
//...
optimization: {
+ innerGraph: false,
},
};

其实也是因为官方文档最后的这句警告让我觉得该算法漏洞可能性大 😓

image

4. 深入问题

即然确认了是 webpack5 默认开启的 InnerGraph 算法的 bug, 那么我们可以继续排查尝试修复一下算法的逻辑漏洞

首先排查 InnerGraph 算法添加的三元表达式为什么为 false, 即 r.j 对应的 __webpack_require__.j 是表示什么

1
2
3
4
5
6
7
8
9
10
11
12
13
{
29971: function (t, e, r) {

e.ZP =
5224 == r.j
? function (t, e, r) {
var i,
o = e.payload.cacheKey;
return n(n({}, t), (((i = {})[o] = m(t[o], e, r)), i));
}
: null;
}
}

如下图__webpack_require__.j的赋值可以得出, 比如 M 项目是 10 个页面的多页项目 Multi-Page Application, 打包后至少会产生 10 组 js 集合, 一个集合对应一个页面运行所需要的 js。

runtime~xxx.js 即是某个页面加载的第一个 js, 它会给__webpack_require__.j赋值, 其功能是设置当前运行的 js 集合 id, 类似于作用域的概念
image

现在我们猜测算法的问题可能就是遗漏了依赖的 js 集合, 比如把 5224 == r.j 修改为 [7327, 7069, 3233].includes(r.j), 那么 7327, 7069, 3233 对应的3个页面加载 reducer.js 所属的打包后的 js 时导出值就正常, 其他页面就会为 null

到这里我们大致清楚了问题的来龙去脉, 于是给 webpack 提交一个 webpack/pull/16701 来尽快修复 InnerGraph 算法的问题