白屏不报错

白屏不报错

双十一前一周的例行发布,第二天上午客服转过来几张截图,页面全白。

截图里的手机状态栏一看就有年头。我们工位上清一色 iPhone,自己点进去一切正常。这种情形最容易收到的结论是"用户环境问题"。那天没法这么收尾,首页曝光掉了两个多点。听着不多,换成我们那个量级的日均流量,是个谁都不想认领的数字。

老机型用户很少主动投诉。他们大概要么以为网络不好,要么直接退出去。等我们知道的时候通常已经过了一整晚,这次算快的。

监控是干净的

第一反应查 JS 错误。日志面板干净得让人心里发慌。首屏埋点没了,性能指标没了,错误列表一条都没有。

ErrorBoundary 没接住,window.onerror 没接住。我们平时拿去汇报的那套前端监控,一点动静都没有。

后来才想明白原因。主 bundle 在解析阶段就抛了 SyntaxError,脚本一行都没执行过,而监控 SDK 是打进这个 bundle 里的。烟雾报警器跟灶台接的是同一路电,跳闸的时候一起黑。

扯远说一句。我们花了两年把监控做得很全,全到能算出一个按钮的点击率,但好像从来没验证过它在"页面根本没起来"的时候还在不在。

复现

测试那边借来一台旧安卓备用机。连 adb,开 chrome://inspect,第一行红字是:

Unexpected token '?'

两个问号。空值合并操作符。

顺带一提,那台机器我们连了三次才连上 adb,第三次是因为同事想起来要同时打开开发者模式和 USB 调试。这种细节没人会写进复盘,但它确实吃掉了当天小半个上午。

顺着堆栈翻到 node_modules 里一个不起眼的工具包。它的 package.json 写了 module 字段,直接指向 dist 下一个 .mjs,里面是原封没动的 ES2020 语法。

而我们的构建里 babel-loader 配的是 exclude: /node_modules/。这个配置从脚手架模板继承下来,用了大概三四年,没人动过,也没人觉得它需要被拿出来讨论。

新内核、iOS、Chrome 九十几,都没事。老一点的 WebView 内核不认那个符号,解析器当场罢工,整份脚本作废,页面就白着。

灰度为什么没拦住

两个原因。

灰度第一批放的基本是内部同学加一二线年轻设备,那台安卓测试机的内核版本甚至比线上主流还新。等于拿一把尺子去量一个不在尺子上的东西。

另一个更难受。我们说不出老内核到底占多少。设备分布数据在客户端团队那边,前端能看到的只是埋点上来的一部分,而白屏用户的埋点是空的。我们用来统计设备分布的那个数据集,自己把最该统计的人漏掉了。

后来找客户端同学要过一次,大意是能拿,但得走流程,低端机的上报本来也不全。这事就搁下了。

生态在往边上挪

我们的配置这三年没变过,变的是依赖那边发产物的默认做法。以前主流包会把产物转译到 ES5,这两年不少包只发现代语法,理由也讲得通:转译一遍体积大不少,都 2026 年了谁还跑老内核。这个判断放在整个行业大概是对的,放在我们有存量老机的业务上就不太适用。

同类的事我以前遇过一次。有年后端升 Node 大版本,几个包直接要求 14 以上,我们一个跑在老镜像上的定时任务装依赖装到一半就报错。至少报错明确。这回它连个响都不给,直接白屏。

这里有个容易混的地方。转译管的是语法,API 得靠 polyfill。两个问号是语法,解析器直接拒绝;某个数组上的新方法属于 API,代码跑得起来,跑到那一行才炸。表现不一样,用户看到的都是白屏。

怎么修

眼前那步是把包单独加进转译名单,babel 的 include 或者构建配置里的 transpileDependencies,效果一样。也可以锁回它上一个还发 ES5 产物的版本。我们走的是前者,因为它的版本跟另一条依赖链绑着,往回锁会牵出一串别的毛病。

然后是给 CI 加一道语法门禁,扫产物里有没有超出 target 的语法,扫出来就 fail。命令一行的事。

es-check es2019 './dist/**/*.js'

真正吵过的是要不要把 node_modules 全量转译。试过一次,构建从三分多钟涨到快八分钟,流水线开始排队。团队为这个争论过一轮,我记得最凶的一句是"为了几台老机器拖累所有人"。最后没全量,只对少数几个包开白名单,剩下的交给语法门禁。

看门狗

最后补的那段代码不长。手写 ES5,内联在 head 里,不参与打包,也不给 tree-shaking 留机会。它只做一件事:起个定时器,主 bundle 挂载成功就取消自己;几秒之后还活着,就上报一次白屏。

我们内部叫它看门狗。三十来行,丑得很稳定。

上线第一周就报了两次。一次是这次的语法问题在另一台机型上复发,一次是 CDN 域名抖动、资源没拉下来。原因不一样,但至少我们知道有人在白屏。以前这件事要靠客服截图告诉我们。

还没解的

语法门禁拦得住解析期,拦不住运行期。某个新 API 没补 polyfill,脚本跑得起来,跑到一半照样白。那种情况看门狗会报,可报出来的也只有"有人白屏",不带原因。

那台旧安卓机后来还回去了。我在云真机上挂了个常驻任务,大版本发布前跑几个老内核。跑一次挺慢,占流水线。说实话,最近两次发布我都没跑,只看了 CI 那道门禁。

还有个数我到现在没有:我们到底有多少用户,跑在从来没被我们测过的内核上。每次想到这里,都觉得那次掉的两个多点未必是最坏情况。

另外我一直觉得,支持到什么内核不该由前端在事故复盘里临时决定。它更像一条业务上的判断,低线城市的换机周期、老年用户占比,做理财的恰好这两类人不少。把最低支持版本写进发布前检查,让它变成一次签字,比让它继续靠工程师的偏好要靠谱。阈值定在哪,我也还没定下来。

Total votes: 9

添加新评论