如今用戶手機性能,瀏覽器性能,網絡性能,愈來愈好,後端邏輯逐漸向前端轉移,前端渲染變得愈來愈廣泛。前端渲染主要依賴JS去完成核心邏輯,JS正變得愈來愈重要。而JS文件是以源碼的形式傳輸,能夠在Chrome Devtools上輕易地被修改和調試。咱們通常不但願核心業務邏輯輕易的被別人瞭解,每每會經過代碼混淆的方式去進行保護。javascript
那麼,代碼混淆對JS性能是否有影響呢?咱們下面討論一個真實的案例,看看混淆如何讓JS性能變差100倍,並詳細介紹如何去跟進和處理相似問題。html
一般JS混淆有兩種方式,一種是正則替換,強度比較弱,很容易被破解;另一種是修改抽象語法樹,比較難破解。前端
一些比較重要的JS文件,通常會使用修改抽象語法樹的方式去進行混淆保護。相關的原理請參考知乎上的文章:前端如何給 JavaScript 加密java
通常來講,JS混淆會引入多餘代碼,修改原來的抽象語法樹,可能會引入性能問題,但性能影響通常很是小。git
可是,也有異常的狀況,咱們在一個業務上發現它的isdsp_securitydata_send.js執行很是耗時,居然達到驚人的1.6秒。Trace信息以下,
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信息,
從上圖能夠看到,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