兄弟们,今天不聊虚的,就聊聊我这两天踩的一个大坑,以及怎么爬出来的。事情是这样的,我们线上有个老项目,一直跑得好好的,结果最近用户反馈页面偶尔白屏,报错率虽然不高,但架不住基数大,每天都有一堆堆的error日志往我邮箱里灌。一开始以为是后端接口抽风,查了半天没毛病,后来一看前端监控,直接傻眼——全是JS报错,而且都集中在压缩后的那个bundle文件里。
你们也知道,生产环境的JS都是压过的,一行几万字符,报错信息根本没法看。我盯着那个“Unexpected token”看了半天,就感觉它在嘲笑我。后来没办法,我开了sourcemap去定位,发现报错的地方五花八门,但都有一个共同点:全是写在条件判断里的代码,或者用了ES6+的新语法。
这时候我才反应过来,问题可能出在压缩配置上。我们项目用的压缩工具比较老,当时图省事儿,直接用的默认配置,根本没细看。默认配置里有些优化项,比如“drop_debugger”、“compress”里的那些花活儿,对于老代码来说简直就是灾难。
我举个最典型的例子,压缩工具默认会把一些你认为“永远为假”的条件分支直接删掉。比如你在代码里写了 if (typeof window !== 'undefined') 这种判断,目的是为了兼容SSR或者防止在某些特殊环境报错。但在压缩器看来,你这判断就是废话,因为它分析上下文觉得window肯定存在,直接给你把整个if块干掉了。结果呢?在真机上,某些WebView环境里,window的某些属性就是访问不到,代码直接崩。
还有一个更坑的,就是压缩器把一些函数内的变量名改得极其短,比如把 userName 变成 a,把 orderList 变成 b。这在大部分时候没事,但如果你代码里用了 eval 或者 new Function,里面引用了外部变量名,那压缩后变量名对不上,直接报ReferenceError。我们线上就有几个老模块用了这个,平时没事,一压就废。
所以这两天我痛定思痛,专门花了一下午去研究这个压缩配置。改完之后你猜怎么着?线上报错直接降了八成!真不是玄学,就是配置没整对。
具体我改了几个地方,你们可以拿小本本记一下,没准儿哪天也能救命:
第一,把 compress 选项里的 conditionals 和 dead_code 给关掉。就是不让它自作聪明去删代码分支。虽然这样压缩率会稍微降一点,可能多个几KB,但换来的是稳定,值!特别是对于那种历史悠久、写满了各种兼容判断的老项目,这俩选项就是定时炸弹。
第二,把 mangle 里的 eval 参数设为 true。这个意思是,如果代码里检测到了 eval 或者 new Function,那就别去改写里面的变量名,或者干脆保留这些函数内的变量不缩短。虽然这样压缩效果会打折扣,但总比线上崩了强。
第三,我把 compress 里的 unused 也关了。这个选项会删除那些“定义了但没用到”的参数。但有些时候,我们是故意留一个参数占位的,比如回调函数里,第三个参数才是有用的,前面两个是固定签名。压缩器不知道啊,它一看前两个没用到,直接给你删了,导致回调参数错位,调用时拿到的是undefined,逻辑全乱套。
第四,也是最容易被忽略的,就是 output 的 ascii_only 设置。如果你的代码里有中文或者emoji字符串,建议把这个设置成 false,否则它全会转成 \uXXXX 这种转义序列。虽然功能一样,但字符串长度会爆炸,而且有些老浏览器对超长的字符串解析有问题,容易卡死或者报错。
反正我现在这套配置,就是“少管闲事”模式。压缩的目的本来就是为了减小体积,但如果为了减小体积把功能搞坏了,那省下的那点流量还不够付加班费的。我算了一下,改完配置后,打包出来的文件大小其实只增加了大概 15%,但线上错误率从之前的每天几百条,降到了现在每天个位数,那点体积算个屁啊。
最后提醒一句,改完压缩配置一定要跑一遍完整的回归测试,特别是那些涉及路由懒加载、动态import的地方。别问我为什么知道,说多了都是泪。反正现在线上稳了,我也能睡个好觉了。你们要是也有类似的问题,别怀疑代码逻辑,先回头看看你的压缩配置是不是在帮你“倒忙”。