前言
现在用户手机性能, 浏览器性能, 网络性能, 越来越好, 后端逻辑逐渐向前端转移, 前端渲染变得越来越普遍前端渲染主要依赖 JS 去完成核心逻辑, JS 正变得越来越重要而 JS 文件是以源码的形式传输, 可以在 Chrome Devtools 上轻易地被修改和调试我们一般不希望核心业务逻辑轻易的被别人了解, 往往会通过代码混淆的方式去进行保护
那么, 代码混淆对 JS 性能是否有影响呢? 我们下面讨论一个真实的案例, 看看混淆如何让 JS 性能变差 100 倍, 并详细介绍如何去跟进和处理类似问题
混淆引入性能问题
通常 JS 混淆有两种方式, 一种是正则替换, 强度比较弱, 很容易被破解; 另外一种是修改抽象语法树, 比较难破解
一些比较重要的 JS 文件, 一般会使用修改抽象语法树的方式去进行混淆保护相关的原理请参考知乎上的文章: 前端如何给 JavaScript 加密
一般来说, JS 混淆会引入多余代码, 修改原来的抽象语法树, 可能会引入性能问题, 但性能影响一般非常小
但是, 也有异常的情况, 我们在一个业务上发现它的 isdsp_securitydata_send.js 执行非常耗时, 竟然达到惊人的 1.6 秒 Trace 信息如下,
而使用它未混淆的源码去执行时, 发现在 15 毫秒就执行完了这是一个非常明显的混淆引入性能问题的案例
分析性能问题
大部分问题, 在找到根本原因之后, 我们都会觉得非常简单, 也很容易解决而分析问题原因的过程和方法则更加重要, 我们下面分享一些通用的分析问题的方法
(1) 确认性能问题
一般来说, 确认一个 JS 执行是否存在性能问题, 使用 Chrome Trace 还是比较方便的我们下面先说说怎么看 Trace 信息
上图中,
v8.run 对应内核的 V8ScriptRunner::runCompiledScript, 代表 blink 端的 JS 的执行时间, 即 JS 执行的实际耗时
V8.Execute 代表 v8 内部的 JS 执行时间, 与 v8.run 代表的意义一样, 耗时也相近
颜色与 V8.ParseLazy 一样的部分, 代表 JS 编译耗时, 从上图可以看到, 编译耗时占了绝大部分
注: 上图仅仅为了展示 Trace 中 V8 相关的含义, 不是我们要讨论的 JS 耗时问题
我们再来看看存在性能问题的 Trace 信息,
从上图可以看到, v8.run 下面几乎没有蓝色的片段, 即几乎没有编译耗时, 基本上都是 JS 代码执行的耗时
这样我们可以判断, abc.js 执行的耗时达到了惊人的 1.6 秒, 而这个 JS 的逻辑非常简单, 它很有可能是存在严重性能问题的
注: 上图是 abc.js 在真实环境执行消耗的时间
(2) 分析问题原因
在上面我们已经定位到 abc.js 的执行耗时存在较大问题, 那么可以怎么去定位问题的准确原因呢?
我们先将问题简化, 把这个 JS 抽取出来单独去执行, 比如, 使用下面示例代码:
然后抓取该示例代码的 Trace 信息,
从上面 Trace 可以看到, 里面一些 JS 函数的执行非常耗时, 每个耗时都有几百毫秒
但这个外联的 JS 是无法定位到代码行的, 我们可以将外联 JS 文件的内容直接拷贝到上述 < script > 标签里面去执行, 看看具体的代码行在哪里?
从上图可以发现, 耗时的代码在 2117 行, 直接点击可以定位到具体的代码行,
从上图可以看到, 下面函数执行非常耗时, 耗时 800 多毫秒
function a(r) { var n = Mo; var a = sn; for (var
var
a = t } return n } |
上述函数为什么会非常耗时呢? 这里就是 JS 引擎专家发挥的地方了! 通过我们技术专家分析 JS 引擎的执行, 发现 String[Oa[Wo + D[Lo](U)](U) + ad + fr[Qo + Z[Lo](U)](U) + kt](t) 这一句代码, 其实是 s += String.fromCharCode(p) 混淆之后的结果
这种混淆会带来什么问题呢? V8 和 JSC 引擎的字符串拼接查找性能都非常弱, 比如, String["toS" + "tring"](),number to string, 都是 V8 和 JSC 引擎的超级弱点
JS 字符串拼接的性能为什么会很差呢?
在 JavaScript 中, 字符串是不可变的 (immutable), 只能被另外一个字符串替换
- var combined = "";
- for (var i = 0; i < 1000000; i++) {
- combined = combined + "hello";
- }
上述示例代码中, combined + "hello" 不会直接修改 combined 变量, 而会新建一个临时对象存储计算结果, 然后再使用该临时对象替换 combined 变量所以上述 for 循环中会产生海量的临时变量, JS 引擎 GC 需要大量工作来清理这些临时变量, 从而会影响性能
注: 上述解析来自 Why is + so bad for concatenation?
我们再进一步去验证去掉字符串混淆的代码效果,
<html> <body>
></script> </body> </html> |
我们看看改动之后的 JS 执行的 Trace 信息,
从上图可以看到, isdsp_securitydata_send.js 在几毫秒就执行完了
我们再在真实的业务页面上验证优化后的效果,
执行耗时直接从 1.6 秒, 优化为 15 毫秒, 优化幅度大于 100 倍!
解决性能问题
从上面的分析可以看到, JS 混淆引入了大量的字符串拼接, 从而导致性能大幅下降
那么, 解决问题的方案也就很显然了, 那就是去掉这些字符串拼接, 即降低混淆的强度, 把字符串混淆部分去掉
去掉字符串混淆部分之后, isdsp_securitydata_send.js 的执行耗时变为 15 毫秒, 完美的实现了优化
结束语
现在前端渲染非常流行, 页面大部分逻辑由 JS 控制从我们长期进行页面性能优化的经验来看, 页面性能优化的 20-40% 与浏览器内核相关, 而 60-80% 与前端 JS 相关, 即前端 JS 是性能优化的重中之重
那么, 前端 JS 优化有那些比较好的实践呢? 内核直接参与分析前端 JS, 成本非常大, 并非长久之计, 内核更应该做的是赋能前端
在赋能前端方面, 内核可以做那些事情呢?
(1) 将一些通用的前端分析方法整理成文档, 供前端参考
(2) 将一些人工分析总结的经验, 固化到自动化的工具, 比如, WDPS Lighthouse
(3) 提供一些更有效的分析工具比如, 在 Trace 中更清晰的展现 JS 引擎的运行逻辑
(4) 与前端更多交流合作, 建立互信, 深入合作研究疑难问题和普遍问题
参考文档
前端如何给 JavaScript 加密
- Why is + so bad for concatenation?
- Optimization killers
作者: 小扎 zack
来源: https://juejin.im/post/5aaf1d4c6fb9a028bc2d89d6