前端优化系列 - JS混淆引入性能天坑

简介:

前言

现在用户手机性能,浏览器性能,网络性能,越来越好,后端逻辑逐渐向前端转移,前端渲染变得越来越普遍。前端渲染主要依赖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抽取出来单独去执行,比如,使用下面示例代码:

<html>
<body>
<script type= "text/javascript"  src= "https://xxx.com/.../abc.js" ></script>
</body>
</html>

然后抓取该示例代码的Trace信息,

从上面Trace可以看到,里面一些JS函数的执行非常耗时,每个耗时都有几百毫秒。

但这个外联的JS是无法定位到代码行的,我们可以将外联JS文件的内容直接拷贝到上述<script>标签里面去执行,看看具体的代码行在哪里?

从上图可以发现,耗时的代码在2117行,直接点击可以定位到具体的代码行,


从上图可以看到,下面函数执行非常耗时,耗时800多毫秒。

function  a(r) {
   var  n = Mo;
   var  a = sn;
   for  ( var  o = S; o < r[L[No + J[Lo](U)](U) + P[Qo + Z[Lo](U)](U)]; o++) {
     var  t = ((r[yr[No + J[Lo](U)](U) + mv + xv](o) - _) * cr + X - a) % V + _;
     n += String[Oa[Wo + D[Lo](U)](U) + ad + fr[Qo + Z[Lo](U)](U) + kt](t);
     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 type= "text/javascript"  src= "https://xxx.com/.../abc.js" ></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

目录
相关文章
|
4月前
|
监控 负载均衡 JavaScript
有哪些有效的方法可以优化Node.js应用的性能?
有哪些有效的方法可以优化Node.js应用的性能?
283 69
|
10月前
|
算法 JavaScript 前端开发
垃圾回收机制对 JavaScript 性能的影响有哪些?
【10月更文挑战第29天】垃圾回收机制对JavaScript性能有着重要的影响。开发者需要了解不同垃圾回收算法的特点和性能开销,通过合理的代码优化和内存管理策略,来降低垃圾回收对性能的负面影响,提高JavaScript程序的整体性能。
|
7月前
|
前端开发 JavaScript Java
JavaScript闭包深入剖析:性能剖析与优化技巧
JavaScript 闭包是强大而灵活的特性,广泛应用于数据封装、函数柯里化和事件处理等场景。闭包通过保存外部作用域的变量,实现了私有变量和方法的创建,提升了代码的安全性和可维护性。然而,闭包也可能带来性能问题,如内存泄漏和执行效率下降。为优化闭包性能,建议采取以下策略:及时解除对不再使用的闭包变量的引用,减少闭包的创建次数,使用 WeakMap 管理弱引用,以及优化闭包结构以减少作用域链查找的开销。在实际开发中,无论是 Web 前端还是 Node.js 后端,这些优化措施都能显著提升程序的性能和稳定性。
193 70
|
6月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
6月前
|
JavaScript 前端开发 Java
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
Array.find() 是 JavaScript 数组方法中一个非常实用和强大的工具。它不仅提供了简洁的查找操作,还具有性能上的独特优势:返回的引用能够直接影响原数组的数据内容,使得数据更新更加高效。通过各种场景的展示,我们可以看到 Array.find() 在更新、条件查找和嵌套结构查找等场景中的广泛应用。 在实际开发中,掌握 Array.find() 的特性和使用技巧,可以让代码更加简洁高效,特别是在需要直接修改原数据内容的情形。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
6月前
|
前端开发 JavaScript 大数据
关于JavaScript性能问题的误解
JavaScript 是单线程语言,代码逐行执行,遇到大数据量计算可能影响性能。前端同事担心遍历大量数据会导致性能问题,但实际上,即使遍历1000、10000条数据,耗时也较少。测试代码执行时间有三种方法:Date.now、console.time 和 performance.now,其中 performance.now 精度最高。开发中不必过度担忧遍历带来的性能损耗,保持代码清晰更重要。
|
10月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
369 77
|
10月前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
488 62
|
10月前
|
JavaScript 前端开发 数据处理
模板字符串和普通字符串在浏览器和 Node.js 中的性能表现是否一致?
综上所述,模板字符串和普通字符串在浏览器和 Node.js 中的性能表现既有相似之处,也有不同之处。在实际应用中,需要根据具体的场景和性能需求来选择使用哪种字符串处理方式,以达到最佳的性能和开发效率。
226 63