前言 現在使用者手機效能,瀏覽器效能,網路效能,越來越好,後端邏輯逐漸向前端轉移,前端渲染變得越來越普遍。前端渲染主要依賴JS去完成核心邏輯,JS正變得越來越重要。而JS檔案是以原始碼的形式傳輸,可以在Chrome Devtools上輕易地被修改和除錯。
前言
現在使用者手機效能,瀏覽器效能,網路效能,越來越好,後端邏輯逐漸向前端轉移,前端渲染變得越來越普遍。前端渲染主要依賴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 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?
我們再進一步去驗證去掉字串混淆的程式碼效果,
我們看看改動之後的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)與前端更多交流合作,建立互信,深入合作研究疑難問題和普遍問題。
參考文件
Why is + so bad for concatenation?
作者:小扎zack