前端性能優(yōu)化
下面是我認知的前端性能優(yōu)化的策略,本書主要著手 JavaScript 優(yōu)化展開闡述屡久。
- JavaScript優(yōu)化
- 非核心代碼異步加載
- 瀏覽器緩存
- 使用CDN
- DNS預解析
- 優(yōu)化資源
- 清理不必要的依賴
本文目錄
高性能JavaScript
早期,IE瀏覽器的JS引擎基于“靜態(tài)垃圾回收機制(Static Garbage Collection)”,該引擎監(jiān)視內(nèi)存中固定數(shù)量的對象來確定何時進行垃圾回收。隨著Web應用的日益發(fā)展奕纫,JS引擎吃不消了。
雖然其他瀏覽器有著更加完善的GC和更好的性能烫沙,但大多數(shù)都是使用JS解釋器來執(zhí)行匹层。
這也正解釋了開篇刷 LeetCode 題時的困惑,解釋型代碼為什么沒有編譯型代碼快锌蓄?
因為升筏,解釋型代碼必須經(jīng)歷把代碼轉(zhuǎn)換成計算機指令的過程。無論解釋器多么智能瘸爽,都會帶來一些性能的消耗您访。
而編譯器已經(jīng)有了各種各樣的優(yōu)化,可以基于詞法分析去判斷代碼想實現(xiàn)什么剪决,產(chǎn)生完成任務的運行最快的機器碼灵汪。解釋器很少有這樣的優(yōu)化,往往代碼怎么寫就怎么被執(zhí)行柑潦。
2008年享言,JS引擎收獲最大的一次性能升級,該引擎的研發(fā)代號為V8渗鬼。V8是一款為 JavaScript 打造的實時(JIT)編譯引擎览露,它把 JavaScript 代碼轉(zhuǎn)化為機器碼來執(zhí)行。緊接著其他瀏覽器也優(yōu)化了JS引擎譬胎,這些只是編譯器層面的優(yōu)化差牛,代碼的性能依然需要開發(fā)人員關注命锄。
一、瀏覽器中的 JavaScript
瀏覽器中js代碼的執(zhí)行可能會阻塞瀏覽器的其他進程偏化,下邊列出了幾點棘手的問題以及優(yōu)化方式脐恩。
腳本阻塞:將<script>標簽放在頁面底部,</body>閉合標簽之前侦讨。
-
延遲時間:
- 內(nèi)嵌<script>不緊跟<link>標簽
- 運用打包工具被盈,合并js文件
-
無阻塞加載js:關鍵是在window對象的load事件觸發(fā)后再下載腳本
使用<script>標簽的defer屬性
注意:defer屬性僅當src屬性聲明時才生效動態(tài)腳本加載:使用動態(tài)創(chuàng)建的<script>元素來下載并執(zhí)行代碼
注意:需要通過偵聽事件,跟蹤并確保腳本下載完成并準備就緒
優(yōu)勢是跨瀏覽器兼容性和易用搭伤,也是最通用的無阻塞加載的策略。-
使用 XHR 對象下載 JavaScript 代碼并注入頁面中
局限性:JavaScript 文件必須和所請求的頁面同域袜瞬,不適用大型Web項目怜俐。
無阻礙腳本加載工具:YUI3、LazyLoad邓尤、LABjs
通過以上策略拍鲤,可以極大地提高JavaScript的Web應用的性能。
此外汞扎,還有一些策略例如:減少js文件的大小季稳、限制HTTP請求數(shù)。這兩點策略澈魄,隨著Web應用的日益復雜景鼠,可行性也隨之降低,也不是做的越極致效果越好痹扇,需要實際情況具體分析铛漓。
二、數(shù)據(jù)存儲的位置
數(shù)據(jù)存儲的位置關系到數(shù)據(jù)的檢索速度鲫构,直接影響代碼執(zhí)行的效率浓恶。JavaScript 有以下四種基本的數(shù)據(jù)存儲位置:
- 字面量:值的記法,包括:字符串结笨、數(shù)字包晰、布爾值、對象炕吸、數(shù)組伐憾、函數(shù)、正則表達式算途,還有特殊的 null 和 undefined 值
- 本地變量:使用 var/let/const 關鍵字定義的數(shù)據(jù)存儲單元
- 數(shù)組元素:以數(shù)字為索引塞耕,存儲在 JavaScript 數(shù)組對象內(nèi)部
- 對象成員:以字符串作為索引,存儲在 JavaScript 對象內(nèi)部
標識符解析的性能
在函數(shù)的執(zhí)行過程中嘴瓤,每遇到一個變量扫外,都會經(jīng)歷一次標識符解析過程以決定從哪里獲取或存儲數(shù)據(jù)莉钙。該過程的搜索執(zhí)行環(huán)境是作用域鏈,這個搜索過程會影響性能筛谚。
注意:總的趨勢是磁玉,標識符所在位置越深,它的讀寫速度越慢驾讲。若采用優(yōu)化過的 JavaScript 引擎的瀏覽器性能損失會大大減少蚊伞。
原型鏈和嵌套成員也遵從此關系。
注意作用域鏈的改變
可以在執(zhí)行時改變作用域鏈吮铭,影響性能的語句:
with 語句:會導致一個新的變量對象被置于作用域鏈的首位时迫,造成訪問特定對象的屬性非常快谓晌,而訪問局部變量則變慢掠拳。
建議:棄用try-catch語句中的 catch 子句:會把異常對象推入一個變量對象并置于作用域的首位。
建議:將錯誤委托給一個函數(shù)處理
閉包
閉包的[[scope]]
屬性包含了與執(zhí)行環(huán)境作用域鏈相同的對象的引用纸肉,同時會影響內(nèi)存開銷和執(zhí)行速度溺欧,應小心使用閉包。
策略
可以通過把常用的數(shù)組元素柏肪、跨域變量保存在局部變量中來改善性能姐刁。
這種策略不推薦用于對象的成員方法,會改變this的值烦味。
三聂使、DOM 編程
瀏覽器中通常會把 DOM 和 JavaScript 獨立實現(xiàn),所以訪問DOM元素消耗很大谬俄。
策略:減少訪問DOM的次數(shù)岩遗,把運算留給ECMAScript一端。
innerHTML 對比 DOM 方法
舊版瀏覽器中凤瘦,使用innerHTML會更快一些宿礁。在基于 WebKit 內(nèi)核的新版瀏覽器中,用DOM略勝一籌蔬芥。
策略:根據(jù)可讀性梆靖、穩(wěn)定性、團隊習慣笔诵、代碼風格來綜合決定返吻。
節(jié)點克隆
節(jié)點克隆element.cloneNode()
比創(chuàng)建新元素document.createElement
更有效率,但不明顯乎婿。
HTML集合
返回值是集合的方法:
- document.getElementByName()
- document.getElementByClassName()
- Document.getElementByTagName()
返回值是集合的屬性:
- document.images
- document.links
- document.forms
- document.forms[0].elements
HTML集合是包含DOM節(jié)點引用的類數(shù)組對象测僵。和數(shù)組的區(qū)別是沒有push和slice方法,有l(wèi)ength屬性和數(shù)字索引的方式訪問元素。
HTML集合低效之源:假定實時態(tài) assumed to be live
策略:
把集合的長度緩存到一個局部變量中捍靠,在循環(huán)條件的退出語句中使用該變量沐旨。
-
使用數(shù)組拷貝。
function toArr() { for (var i = 0, arr = [], len = coll.length; i < len; i++) { arr[i] = coll[i]; } return arr; }
遍歷DOM
屬性名 | 被替代的屬性 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
選擇器API
queryAelectorAll()
和firstElementChild()
方法使用CSS選擇器作為參數(shù)并返回一個NodeList榨婆,不會返回HTML集合磁携。適合處理大量組合查詢。
重排和重繪
在瀏覽器的渲染過程中良风,瀏覽器會在下載完頁面所有組件之后谊迄,解析并生成兩個數(shù)據(jù)結(jié)構:
- DOM Tree(DOM樹)
- Render Tree(渲染樹)
一旦上述兩種結(jié)構構建完成,瀏覽器就開始繪制(paint)頁面元素烟央。
注:對重排和重繪的理解是非常必要的
重排 Reflow
定義:當DOM結(jié)構的變化影響了元素的幾何屬性统诺,瀏覽器需要根據(jù)樣式來重新計算元素出現(xiàn)的位置。瀏覽器會使渲染樹中受到影響的部分失效疑俭,并重新構造渲染樹篙议。
觸發(fā)Reflow的條件:
- 添加或刪除可見的DOM元素
- 元素位置改變:如,添加動畫效果
- 元素尺寸改變:如怠硼,改變邊框?qū)捀摺?nèi)外邊距等
- 內(nèi)容改變:如移怯,改變段落文字行數(shù)香璃、圖片替換等
- 瀏覽器Resize窗口(移動端不會出現(xiàn))
- 修改默認字體
- 頁面渲染器初始化
特別的:當滾動條出現(xiàn)時,會觸發(fā)整個頁面的重排
重繪 Repaint
定義:完成重排后舟误,瀏覽器會根據(jù)渲染樹重新繪制受影響的部分到屏幕中葡秒。
不是所有的DOM變化都會影響幾何屬性,例如改變一個元素的背景色只會發(fā)生一次重繪嵌溢。
特別的眯牧,要注意分析改變所影響的階段是重排還是重繪。
綜上赖草,重排和重繪都是昂貴的操作学少,會導致Web應用反應遲鈍。所以秧骑,應該盡可能減少這類過程的發(fā)生版确。
渲染樹的變化的排隊和刷新
瀏覽器會通過隊列化批量執(zhí)行來優(yōu)化重排過程。
以下獲取布局的操作會導致隊列刷新:
- offsetTop乎折、offsetLeft绒疗、offsetWidth、offsetHeight
- scrollTop骂澄、scrollLeft吓蘑、scrollWidth、scrollHeight
- clientTop坟冲、clientLeft磨镶、clientWidth溃蔫、clientHeight
- getComputedStyle()
修改樣式時,應避免以上屬性棋嘲。
策略:不要在布局信息改變時操作它酒唉。
最小化重排和重繪
策略:
-
合并多次對DOM和樣式的修改,然后一次處理掉沸移。(n -> 1)
如:cssText屬性痪伦,className屬性等。
盡量減少offsets等布局信息的獲取次數(shù)雹锣,方法是獲取一次起始位置的值网沾,在動畫循環(huán)中,直接使用變量蕊爵。
-
讓元素脫離動畫流:拖放代理
- 使用絕對定位頁面上的動畫元素辉哥,將其脫離文檔流。
- 讓元素動起來攒射,這時會臨時覆蓋部分頁面醋旦,只會發(fā)生小規(guī)模重繪。
- 當動畫結(jié)束時恢復定位会放,從而只會下移一次文檔的其他元素饲齐。
在元素很多時,避免使用:hover
批量修改DOM
關鍵:“離線”操作DOM樹咧最,使用緩存捂人,減少訪問布局信息的次數(shù)。
策略:
- 使元素脫離文檔流
- 隱藏元素(display:none)矢沿,應用修改滥搭,重新顯示。
- 使用文檔片段在當前DOM之外構建一個子樹(document.createDocumentFragment())捣鲸,再把它拷貝回文檔瑟匆。(推薦)
- 將原始元素拷貝到一個脫離文檔流的節(jié)點中,修改副本栽惶,完成后再替換原始元素脓诡。
- 對其應用多重改變
- 把元素帶回文檔中
事件委托
之前寫過一篇<理解DOM事件處理程序和事件委托>的文章,涉及事件模式的基本概念媒役、事件流祝谚、事件委托的實現(xiàn)等的闡述,如果大家對以上概念有所遺忘酣衷,歡迎點擊鏈接查看原文交惯。
每綁定一個事件處理器都會加重頁面負擔、延長執(zhí)行時間、消耗更多的內(nèi)存(因為瀏覽器會跟蹤每個事件處理器)席爽。
一個優(yōu)雅的策略就是利用事件委托意荤。
可以將冗長的瀏覽器兼容性代碼移入可重用的類庫:
- 訪問事件對象,判斷事件源
- 取消文檔樹中的冒泡
- 阻止默認動作
四只锻、算法和流程控制
大部分性能問題的來源是低效的算法或工具編寫出的糟糕代碼玖像。
循環(huán)
代碼執(zhí)行的大部分消耗在循環(huán)。
JS循環(huán)的類型:
-
for循環(huán)
注:for循環(huán)初始化中var語句會創(chuàng)建一個函數(shù)級的變量齐饮,應盡可能使用ES6中的let語句定義循環(huán)級變量捐寥。
- while 循環(huán):和for類似,是最簡單的前測循環(huán)
- do-while 循環(huán):唯一的后測循環(huán)祖驱,循環(huán)體至少運行一次握恳。
- for-in 循環(huán):枚舉任何對象的屬性名key
- for-of 循環(huán):ES6新特性,枚舉任何對象的值value
所返回的屬性:
- 對象的實例屬性
- 從原型鏈中繼承的屬性
循環(huán)性能:for-in 明顯慢
由于每次操作會同時搜索實例和原型屬性捺僻,查詢散列鍵乡洼,會產(chǎn)生更多開銷。所以匕坯,除了明確需要迭代一個屬性數(shù)量未知的對象束昵,其他情況應避免使用for-in。
若其他循環(huán)的性能都差不多葛峻,其實只有兩個因素可以提升整體性能:
- 減少每次迭代的工作量:限制循環(huán)中的耗時操作總數(shù)
- 最小化屬性查找
關鍵:減少對象成員及數(shù)組項的查找次數(shù)
策略:只查找一次屬性锹雏,并把值存到一個局部變量中。例如:var len = items.length;
- 倒序循環(huán)
通常泞歉,數(shù)組項的順序與所要執(zhí)行的任務無關。倒序循環(huán)是編程語言中一種通用的性能優(yōu)化方式匿辩。
- 最小化屬性查找
當循環(huán)復雜度為O(n)時腰耙,減少每次迭代的工作量是最有效的。當復雜度大于O(n)铲球,建議著重減少迭代次數(shù)挺庞。
- 減少迭代的次數(shù)
達夫設備(Duff's Device):循環(huán)體展開技術,一次迭代中實際執(zhí)行了多次迭代的操作稼病。
迭代數(shù)超過1000选侨,使用 Duff's Device 的執(zhí)行效率將明顯提升。
基于函數(shù)的迭代 forEach() 明顯慢
原因:對每個數(shù)組項調(diào)用外部方法所帶來的額外開銷然走。
條件語句
if-else 對比 switch
基于測試條件的數(shù)量選擇:條件數(shù)量越大援制,越傾向于使用switch,易讀性強且速度快芍瑞。
大多數(shù)語言對 switch 語句的實現(xiàn)都采用了 branch table(分支表)索引進行優(yōu)化晨仑。
優(yōu)化 if-else
- 最小化到達正確分支前所需條件判斷的次數(shù)
策略:條件語句按照從大概率到小概率的順序排列 - 把 if-else 組織成一系列嵌套的if-else 語句
策略:二分法把值域分成一系列區(qū)間,逐步縮小范圍。
適用范圍:有多個值域需要測試洪己。
查找表
當條件語句數(shù)量很大或有大量散離值需要測試時妥凳,使用數(shù)組和普通對象構建查找表訪問數(shù)據(jù)比較快。
優(yōu)點:當單個鍵和單個值之間存在邏輯映射時答捕,隨著候選值增加逝钥,幾乎不產(chǎn)生額外開銷。
遞歸
傳統(tǒng)算法的遞歸實現(xiàn):階層函數(shù)
潛在問題;
- 假死
策略:為了安全在瀏覽器工作拱镐,可以迭代和Memoization結(jié)合使用艘款。 - 瀏覽器調(diào)用棧大小限制 Call stack size limites
當超過最大調(diào)用棧容量時,瀏覽器會報錯痢站,可以用try-catch定位磷箕。
策略:ES6中使用尾遞歸就不會發(fā)生棧溢出,相對節(jié)省性能阵难。
五岳枷、字符串和正則表達式
字符串連接
方法 | 示例 |
---|---|
The + operator | str = "a" + "b" + "c"; |
The += operator | str = "a"; str += "b"; str += "c"; |
array.join() | str = ["a", "b", "c"].join(""); |
string.concat() | str = "a"; str = str.concat("b","c"); |
轉(zhuǎn)義字符"" | 在每一行的最后,都加上轉(zhuǎn)義斜線 \ |
使用es6模版字符串 | 使用鍵盤1左邊的字符 ` 拼接 |
字符串連接優(yōu)化
str += 'zhu' + 'yue'; //2個以上的字符串拼接呜叫,會在內(nèi)存中產(chǎn)生臨時字符串
str = str + 'zhu' + 'yue'; //推薦空繁,直接附加內(nèi)容給str装哆,提速10%~40%
瀏覽器合并字符串時分配的方法:除IE外乒省,為表達式左側(cè)的字符串分配更多的內(nèi)存,然后簡單地將第二個字符串拷貝至它的末尾夷蚊。
正則表達式優(yōu)化
使用正則表達式和倒序循環(huán)可以簡單實現(xiàn)trim方法娱颊,去首尾空白傲诵。
優(yōu)化正則表達式的策略:
- 具體化分隔符之間的字符串匹配模式
- 使用預查和反向引用的模擬原子組
- 避免嵌套量詞與回溯失控
- 關注如何讓匹配更快失敗
- 以簡單必需的字元開始
- 使用量詞模式,使它們后面的字元互斥
- 較少分支數(shù)量箱硕,縮小分支范圍
- 把正則表達式賦值給變量并重用
- 化繁為簡
何時不使用正則表達式
- 在特定位置上提取并檢查字符串的值:slice拴竹、substr、substring
- 查找特定字符串位置剧罩,或者判斷它們是否存在:indexOf栓拜、lastIndexOf
六、快速響應的用戶界面
Web Workers 引入了一個接口惠昔,能使代碼運行且不占用瀏覽器線程的時間幕与。
Worker的運行環(huán)境:
- 一個 navigator 對象,只包括四個屬性:appName镇防、appVersion啦鸣、user Agent 和 platform
- 一個 location 對象(與window.location 相同,不過所有屬性都是只讀的)
- 一個 importScripts() 方法来氧,用來加載 Worker 所用到的外部 JavaScript 文件
- 所有的 ECMAScript 對象
- XMLHTTPRequest 構造器
- setTimeout() 和 setInterval() 方法
- 一個 close() 方法赏陵,可以立即停止 Worker 運行饼齿。
Web Workers 實際應用
Web Workers 適用于:
- 處理純數(shù)據(jù)
- 與瀏覽器無關的長時間運行腳本
- 編碼/解碼大字符串
- 復雜數(shù)學運算,如:圖像和視頻
- 大數(shù)組排序
例子:解析一個很大的JSON字符串
var worker = new Worker("jsonParser.js");
//數(shù)據(jù)就位時蝙搔,調(diào)用事件處理器
worker.onmessage = function (event) {
//JSON結(jié)構被回傳回來
var jsonData = event.data;
//使用JSON結(jié)構
evaluateData(jsonData);
};
//傳入要解析的大段JSON字符串
worker.postMessage(jsonText);
jsonParser.js文件中 Worker 中負責解析JSON的代碼:
//當JSON數(shù)據(jù)存在時缕溉,該事件處理器會被調(diào)用
self.onmessage = function (event) {
//JSON字符串由event.data傳入
var jsonText = event.data;
//解析
var jsonData = JSON.parse(jsonText);
//回傳結(jié)果
self.postMessage(jsonData);
}
超過100毫秒的處理過程,應該考慮 Worker 方案吃型。
七证鸥、AJAX
常常使用XMLHttpRequest(XHR)、Dynamic script tag insertion勤晚、multipart XHR技術向服務器請求數(shù)據(jù)枉层。
XMLHttpRequest:可以參考之前寫過的文章 用原生JS封裝AJAX
Dynamic script tag insertion:可以跨域請求數(shù)據(jù)
multipart XHR:將服務端資源打包成約定好的字符串分割的長字符串,并發(fā)送到客戶端赐写。
數(shù)據(jù)格式:JSON
此章節(jié)優(yōu)化主要是有效的利用瀏覽器緩存鸟蜡,還有本章沒有提及的現(xiàn)在逐漸開始流行的 fetch API也值得討論。
八挺邀、編程實踐
- 避免雙重求值揉忘,即在JavaScript代碼中執(zhí)行另一段JavaScript代碼,是JavaScript運行期性能優(yōu)化的關鍵端铛。
- 使用 Object/Array 直接量
- 通過延遲加載和條件預加載泣矛,避免重復工作
- 使用語言中速度快的部分,如:位操作(
& | ^ ~
)禾蚕、原生方法
九您朽、構建并部署高性能JavaScript應用
構建和部署的過程對基于js的web應用的性能有著巨大影響。這個過程中最重要的步驟有:
- 使用Gzip合并换淆、壓縮js文件哗总,能夠減少約70%的體積。
- 通過正確設置HTTP響應頭來緩存js文件倍试,通過向文件名增加時間戳來避免緩存問題讯屈。
- 使用CDN提供js文件;CDN不僅可以提升性能易猫,也幫助管理文件的壓縮與緩存耻煤。
- 使用Webpack構建具壮。