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

摘要:
前言 如今用戶手機性能,瀏覽器性能,網絡性能,愈來愈好,後端邏輯逐漸向前端轉移,前端渲染變得愈來愈廣泛。前端渲染主要依賴JS去完成核心邏輯,JS正變得愈來愈重要。而JS文件是以源碼的形式傳輸,能夠在Chrome Devtools上輕易地被修改和調試。

前言

如今用戶手機性能,瀏覽器性能,網絡性能,愈來愈好,後端邏輯逐漸向前端轉移,前端渲染變得愈來愈廣泛。前端渲染主要依賴JS去完成核心邏輯,JS正變得愈來愈重要。而JS文件是以源碼的形式傳輸,能夠在Chrome Devtools上輕易地被修改和調試。咱們通常不但願核心業務邏輯輕易的被別人瞭解,每每會經過代碼混淆的方式去進行保護。javascript

那麼,代碼混淆對JS性能是否有影響呢?咱們下面討論一個真實的案例,看看混淆如何讓JS性能變差100倍,並詳細介紹如何去跟進和處理相似問題。html

混淆引入性能問題

一般JS混淆有兩種方式,一種是正則替換,強度比較弱,很容易被破解;另一種是修改抽象語法樹,比較難破解。前端

一些比較重要的JS文件,通常會使用修改抽象語法樹的方式去進行混淆保護。相關的原理請參考知乎上的文章:前端如何給 JavaScript 加密java

通常來講,JS混淆會引入多餘代碼,修改原來的抽象語法樹,可能會引入性能問題,但性能影響通常很是小。git

可是,也有異常的狀況,咱們在一個業務上發現它的isdsp_securitydata_send.js執行很是耗時,居然達到驚人的1.6秒。Trace信息以下,

b9f9c20687e8c3221852f5b3e16b6a0ec5c91779
github

而使用它未混淆的源碼去執行時,發如今15毫秒就執行完了。這是一個很是明顯的混淆引入性能問題的案例。後端

分析性能問題

大部分問題,在找到根本緣由以後,咱們都會以爲很是簡單,也很容易解決。而分析問題緣由的過程和方法則更加劇要,咱們下面分享一些通用的分析問題的方法。瀏覽器

(1)確認性能問題性能優化

通常來講,確認一個JS執行是否存在性能問題,使用Chrome Trace仍是比較方便的。咱們下面先說說怎麼看Trace信息。
bash

上圖中,

v8.run 對應內核的V8ScriptRunner::runCompiledScript, 表明blink端的JS的執行時間,即JS執行的實際耗時。

V8.Execute 表明v8內部的JS執行時間,與v8.run表明的意義同樣,耗時也相近。

顏色與V8.ParseLazy同樣的部分,表明JS編譯耗時,從上圖能夠看到,編譯耗時佔了絕大部分。

注:上圖僅僅爲了展現Trace中V8相關的含義,不是咱們要討論的JS耗時問題。

咱們再來看看存在性能問題的Trace信息,

c58fc75f6053d2274141f158540706bea6a4f680

從上圖能夠看到,v8.run下面幾乎沒有藍色的片斷,即幾乎沒有編譯耗時,基本上都是JS代碼執行的耗時。

這樣咱們能夠判斷,abc.js 執行的耗時達到了驚人的1.6秒,而這個JS的邏輯很是簡單,它頗有多是存在嚴重性能問題的。

注:上圖是abc.js在真實環境執行消耗的時間。

(2)分析問題緣由

在上面咱們已經定位到abc.js的執行耗時存在較大問題,那麼能夠怎麼去定位問題的準確緣由呢?

咱們先將問題簡化,把這個JS抽取出來單獨去執行,好比,使用下面示例代碼:

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

而後抓取該示例代碼的Trace信息,

從上面Trace能夠看到,裏面一些JS函數的執行很是耗時,每一個耗時都有幾百毫秒。

但這個外聯的JS是沒法定位到代碼行的,咱們能夠將外聯JS文件的內容直接拷貝到上述<script>標籤裏面去執行,看看具體的代碼行在哪裏?

從上圖能夠發現,耗時的代碼在2117行,直接點擊能夠定位到具體的代碼行,

4100d1a1c02528d2a717c1d2333690cff736676a

從上圖能夠看到,下面函數執行很是耗時,耗時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= "xxx.com/.../abc.js" ></script>
</body>
</html>

咱們看看改動以後的JS執行的Trace信息,

35469ec1652e67783f36d29daef00f19271ad1ef

從上圖能夠看到,isdsp_securitydata_send.js在幾毫秒就執行完了。

咱們再在真實的業務頁面上驗證優化後的效果,

8c33b60fb17dcc5f4e8559bb57c09845fdff2287

執行耗時直接從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

原文連接

相關文章
相關標籤/搜索