曾經看過一篇文章,有一句話這樣說:css
只有在大學的圖書館裏,你才能真正賺回你交的學費。html
臨近畢業,還想再去圖書館多轉轉。偶然在架子上發現了這本書,一看做者是寫大名鼎鼎的紅寶書的人,就很感興趣。再者,最近用 JavaScript 刷 LeetCode 發現,提交顯示 JavaScript 要比 Go 語言或 Python 有更大的時間和內存消耗,也使我把了解 JavaScript 內存機制和性能優化提上了日程。前端
本書雖然有部分章節涉及到的問題有必定年代感,好比最後一章[工具],因爲前端技術的快速迭代和瀏覽器的不斷支持下,已經不適用了。可是這本書的前面章節詳細地從js運行、訪問、代碼結構優化、異步編程等多方面講解了 JavaScript 優化的策略,解答了我刷題時的疑惑,也讓我認識到以前秋招面試的時候遇到的一些坑,仍是有許多須要引發重視的知識體系須要不斷擴容。es6
總之,這本書不只是對前端性能優化基礎知識的一種補充掌握,還有不少底層原理的實現值得學習,推薦有項目經驗並但願提高Web應用性能的前端開發人員閱讀。web
下面是我認知的前端性能優化的策略,本書主要着手 JavaScript 優化展開闡述。面試
JavaScript優化正則表達式
非核心代碼異步加載算法
瀏覽器緩存express
使用CDN編程
DNS預解析
優化資源
清理沒必要要的依賴
早期,IE瀏覽器的JS引擎基於「靜態垃圾回收機制(Static Garbage Collection)」,該引擎監視內存中固定數量的對象來肯定什麼時候進行垃圾回收。隨着Web應用的日益發展,JS引擎吃不消了。
雖然其餘瀏覽器有着更加完善的GC和更好的性能,但大多數都是使用JS解釋器來執行。
這也正解釋了開篇刷 LeetCode 題時的困惑,解釋型代碼爲何沒有編譯型代碼快?
由於,解釋型代碼必須經歷把代碼轉換成計算機指令的過程。不管解釋器多麼智能,都會帶來一些性能的消耗。 而編譯器已經有了各類各樣的優化,能夠基於詞法分析去判斷代碼想實現什麼,產生完成任務的運行最快的機器碼。解釋器不多有這樣的優化,每每代碼怎麼寫就怎麼被執行。
2008年,JS引擎收穫最大的一次性能升級,該引擎的研發代號爲V8。V8是一款爲 JavaScript 打造的實時(JIT)編譯引擎,它把 JavaScript 代碼轉化爲機器碼來執行。緊接着其餘瀏覽器也優化了JS引擎,這些只是編譯器層面的優化,代碼的性能依然須要開發人員關注。
瀏覽器中js代碼的執行可能會阻塞瀏覽器的其餘進程,下邊列出了幾點棘手的問題以及優化方式。
腳本阻塞:將<script>標籤放在頁面底部,</body>閉合標籤以前。
延遲時間:
內嵌<script>不緊跟<link>標籤
運用打包工具,合併js文件
無阻塞加載js:關鍵是在window對象的load事件觸發後再下載腳本
使用<script>標籤的defer屬性 注意:defer屬性僅當src屬性聲明時才生效
動態腳本加載:使用動態建立的<script>元素來下載並執行代碼 注意:須要經過偵聽事件,跟蹤並確保腳本下載完成並準備就緒 優點是跨瀏覽器兼容性和易用,也是最通用的無阻塞加載的策略。
使用 XHR 對象下載 JavaScript 代碼並注入頁面中
侷限性:JavaScript 文件必須和所請求的頁面同域,不適用大型Web項目。
無阻礙腳本加載工具:YUI三、LazyLoad、LABjs
經過以上策略,能夠極大地提升JavaScript的Web應用的性能。 此外,還有一些策略例如:減小js文件的大小、限制HTTP請求數。這兩點策略,隨着Web應用的日益複雜,可行性也隨之下降,也不是作的越極致效果越好,須要實際狀況具體分析。
數據存儲的位置關係到數據的檢索速度,直接影響代碼執行的效率。JavaScript 有如下四種基本的數據存儲位置:
字面量:值的記法,包括:字符串、數字、布爾值、對象、數組、函數、正則表達式,還有特殊的 null 和 undefined 值
本地變量:使用 var/let/const 關鍵字定義的數據存儲單元
數組元素:以數字爲索引,存儲在 JavaScript 數組對象內部
對象成員:以字符串做爲索引,存儲在 JavaScript 對象內部
在函數的執行過程當中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪裏獲取或存儲數據。該過程的搜索執行環境是做用域鏈,這個搜索過程會影響性能。
注意:總的趨勢是,標識符所在位置越深,它的讀寫速度越慢。若採用優化過的 JavaScript 引擎的瀏覽器性能損失會大大減小。
原型鏈和嵌套成員也聽從此關係。
能夠在執行時改變做用域鏈,影響性能的語句:
with 語句:會致使一個新的變量對象被置於做用域鏈的首位,形成訪問特定對象的屬性很是快,而訪問局部變量則變慢。 建議:棄用
try-catch語句中的 catch 子句:會把異常對象推入一個變量對象並置於做用域的首位。 建議:將錯誤委託給一個函數處理
閉包的[[scope]]
屬性包含了與執行環境做用域鏈相同的對象的引用,同時會影響內存開銷和執行速度,應當心使用閉包。
能夠經過把經常使用的數組元素、跨域變量保存在局部變量中來改善性能。
這種策略不推薦用於對象的成員方法,會改變this的值。
瀏覽器中一般會把 DOM 和 JavaScript 獨立實現,因此訪問DOM元素消耗很大。
策略:減小訪問DOM的次數,把運算留給ECMAScript一端。
舊版瀏覽器中,使用innerHTML會更快一些。在基於 WebKit 內核的新版瀏覽器中,用DOM略勝一籌。
策略:根據可讀性、穩定性、團隊習慣、代碼風格來綜合決定。
節點克隆element.cloneNode()
比建立新元素document.createElement
更有效率,但不明顯。
返回值是集合的方法:
document.getElementByName()
document.getElementByClassName()
Document.getElementByTagName()
返回值是集合的屬性:
document.images
document.links
document.forms
document.forms[0].elements
HTML集合是包含DOM節點引用的類數組對象。和數組的區別是沒有push和slice方法,有length屬性和數字索引的方式訪問元素。
HTML集合低效之源:假定實時態 assumed to be live
策略:
把集合的長度緩存到一個局部變量中,在循環條件的退出語句中使用該變量。
使用數組拷貝。
function toArr() { for (var i = 0, arr = [], len = coll.length; i < len; i++) { arr[i] = coll[i]; } return arr;}複製代碼
屬性名 | 被替代的屬性 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
queryAelectorAll()
和firstElementChild()
方法使用CSS選擇器做爲參數並返回一個NodeList,不會返回HTML集合。適合處理大量組合查詢。
在瀏覽器的渲染過程當中,瀏覽器會在下載完頁面全部組件以後,解析並生成兩個數據結構:
DOM Tree(DOM樹)
Render Tree(渲染樹)
一旦上述兩種結構構建完成,瀏覽器就開始繪製(paint)頁面元素。
注:對重排和重繪的理解是很是必要的
定義:當DOM結構的變化影響了元素的幾何屬性,瀏覽器須要根據樣式來從新計算元素出現的位置。瀏覽器會使渲染樹中受到影響的部分失效,並從新構造渲染樹。
觸發Reflow的條件:
添加或刪除可見的DOM元素
元素位置改變:如,添加動畫效果
元素尺寸改變:如,改變邊框寬高、內外邊距等
內容改變:如,改變段落文字行數、圖片替換等
瀏覽器Resize窗口(移動端不會出現)
修改默認字體
頁面渲染器初始化
特別的:當滾動條出現時,會觸發整個頁面的重排
定義:完成重排後,瀏覽器會根據渲染樹從新繪製受影響的部分到屏幕中。
不是全部的DOM變化都會影響幾何屬性,例如改變一個元素的背景色只會發生一次重繪。
特別的,要注意分析改變所影響的階段是重排仍是重繪。
綜上,重排和重繪都是昂貴的操做,會致使Web應用反應遲鈍。因此,應該儘量減小這類過程的發生。
瀏覽器會經過隊列化批量執行來優化重排過程。
如下獲取佈局的操做會致使隊列刷新:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
修改樣式時,應避免以上屬性。
策略:不要在佈局信息改變時操做它。
策略:
合併屢次對DOM和樣式的修改,而後一次處理掉。(n -> 1)
如:cssText屬性,className屬性等。
儘可能減小offsets等佈局信息的獲取次數,方法是獲取一次起始位置的值,在動畫循環中,直接使用變量。
讓元素脫離動畫流:拖放代理
使用絕對定位頁面上的動畫元素,將其脫離文檔流。
讓元素動起來,這時會臨時覆蓋部分頁面,只會發生小規模重繪。
當動畫結束時恢復定位,從而只會下移一次文檔的其餘元素。
在元素不少時,避免使用:hover
關鍵:「離線」操做DOM樹,使用緩存,減小訪問佈局信息的次數。 策略:
使元素脫離文檔流
隱藏元素(display:none),應用修改,從新顯示。
使用文檔片斷在當前DOM以外構建一個子樹(document.createDocumentFragment()),再把它拷貝迴文檔。(推薦)
將原始元素拷貝到一個脫離文檔流的節點中,修改副本,完成後再替換原始元素。
對其應用多重改變
把元素帶回文檔中
以前寫過一篇理解DOM事件處理程序和事件委託的文章,涉及事件模式的基本概念、事件流、事件委託的實現等的闡述,若是你們對以上概念有所遺忘,歡迎點擊連接查看原文。
每綁定一個事件處理器都會加劇頁面負擔、延長執行時間、消耗更多的內存(由於瀏覽器會跟蹤每一個事件處理器)。
一個優雅的策略就是利用事件委託。
能夠將冗長的瀏覽器兼容性代碼移入可重用的類庫:
訪問事件對象,判斷事件源
取消文檔樹中的冒泡
阻止默認動做
大部分性能問題的來源是低效的算法或工具編寫出的糟糕代碼。
代碼執行的大部分消耗在循環。
JS循環的類型:
for循環
注:for循環初始化中var語句會建立一個函數級的變量,應儘量使用ES6中的let語句定義循環級變量。
while 循環:和for相似,是最簡單的前測循環
do-while 循環:惟一的後測循環,循環體至少運行一次。
for-in 循環:枚舉任何對象的屬性名key
for-of 循環:ES6新特性,枚舉任何對象的值value
拓展知識:for-in 和 for-of 區別
所返回的屬性:
對象的實例屬性
從原型鏈中繼承的屬性
因爲每次操做會同時搜索實例和原型屬性,查詢散列鍵,會產生更多開銷。因此,除了明確須要迭代一個屬性數量未知的對象,其餘狀況應避免使用for-in。
若其餘循環的性能都差很少,其實只有兩個因素能夠提高總體性能:
減小每次迭代的工做量:限制循環中的耗時操做總數
最小化屬性查找 關鍵:減小對象成員及數組項的查找次數 策略:只查找一次屬性,並把值存到一個局部變量中。例如:var len = items.length;
倒序循環 一般,數組項的順序與所要執行的任務無關。倒序循環是編程語言中一種通用的性能優化方式。
當循環複雜度爲O(n)時,減小每次迭代的工做量是最有效的。當複雜度大於O(n),建議着重減小迭代次數。
減小迭代的次數 達夫設備(Duff's Device):循環體展開技術,一次迭代中實際執行了屢次迭代的操做。
迭代數超過1000,使用 Duff's Device 的執行效率將明顯提高。
緣由:對每一個數組項調用外部方法所帶來的額外開銷。
if-else 對比 switch 基於測試條件的數量選擇:條件數量越大,越傾向於使用switch,易讀性強且速度快。
大多數語言對 switch 語句的實現都採用了 branch table(分支表)索引進行優化。
優化 if-else
最小化到達正確分支前所需條件判斷的次數 策略:條件語句按照從大機率到小几率的順序排列
把 if-else 組織成一系列嵌套的if-else 語句 策略:二分法把值域分紅一系列區間,逐步縮小範圍。 適用範圍:有多個值域須要測試。 查找表 當條件語句數量很大或有大量散離值須要測試時,使用數組和普通對象構建查找表訪問數據比較快。
優勢:當單個鍵和單個值之間存在邏輯映射時,隨着候選值增長,幾乎不產生額外開銷。
傳統算法的遞歸實現:階層函數 潛在問題;
假死 策略:爲了安全在瀏覽器工做,能夠迭代和Memoization結合使用。
瀏覽器調用棧大小限制 Call stack size limites 當超過最大調用棧容量時,瀏覽器會報錯,能夠用try-catch定位。 策略:ES6中使用尾遞歸就不會發生棧溢出,相對節省性能。
方法 | 示例 |
---|---|
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"); |
轉義字符"" | 在每一行的最後,都加上轉義斜線 \ |
使用es6模版字符串 | 使用鍵盤1左邊的字符 ` 拼接 |
str += 'zhu' + 'yue'; //2個以上的字符串拼接,會在內存中產生臨時字符串str = str + 'zhu' + 'yue'; //推薦,直接附加內容給str,提速10%~40% 複製代碼
瀏覽器合併字符串時分配的方法:除IE外,爲表達式左側的字符串分配更多的內存,而後簡單地將第二個字符串拷貝至它的末尾。
使用正則表達式和倒序循環能夠簡單實現trim方法,去首尾空白。
優化正則表達式的策略:
具體化分隔符之間的字符串匹配模式
使用預查和反向引用的模擬原子組
避免嵌套量詞與回溯失控
關注如何讓匹配更快失敗
以簡單必需的字元開始
使用量詞模式,使它們後面的字元互斥
較少分支數量,縮小分支範圍
把正則表達式賦值給變量並重用
化繁爲簡
在特定位置上提取並檢查字符串的值:slice、substr、substring
查找特定字符串位置,或者判斷它們是否存在:indexOf、lastIndexOf
Web Workers 引入了一個接口,能使代碼運行且不佔用瀏覽器線程的時間。
Worker的運行環境:
一個 navigator 對象,只包括四個屬性:appName、appVersion、user Agent 和 platform
一個 location 對象(與window.location 相同,不過全部屬性都是隻讀的)
一個 importScripts() 方法,用來加載 Worker 所用到的外部 JavaScript 文件
全部的 ECMAScript 對象
XMLHTTPRequest 構造器
setTimeout() 和 setInterval() 方法
一個 close() 方法,能夠當即中止 Worker 運行。
Web Workers 適用於:
處理純數據
與瀏覽器無關的長時間運行腳本
編碼/解碼大字符串
複雜數學運算,如:圖像和視頻
大數組排序
例子:解析一個很大的JSON字符串
var worker = new Worker("jsonParser.js");//數據就位時,調用事件處理器worker.onmessage = function (event) { //JSON結構被回傳回來 var jsonData = event.data; //使用JSON結構 evaluateData(jsonData);};//傳入要解析的大段JSON字符串worker.postMessage(jsonText);複製代碼
jsonParser.js文件中 Worker 中負責解析JSON的代碼:
//當JSON數據存在時,該事件處理器會被調用self.onmessage = function (event) { //JSON字符串由event.data傳入 var jsonText = event.data; //解析 var jsonData = JSON.parse(jsonText); //回傳結果 self.postMessage(jsonData);}複製代碼
超過100毫秒的處理過程,應該考慮 Worker 方案。
經常使用XMLHttpRequest(XHR)、Dynamic script tag insertion、multipart XHR技術向服務器請求數據。
XMLHttpRequest:能夠參考以前寫過的文章 用原生JS封裝AJAX Dynamic script tag insertion:能夠跨域請求數據 multipart XHR:將服務端資源打包成約定好的字符串分割的長字符串,併發送到客戶端。
數據格式:JSON
此章節優化主要是有效的利用瀏覽器緩存,還有本章沒有說起的如今逐漸開始流行的 fetch API也值得討論。
避免雙重求值,即在JavaScript代碼中執行另外一段JavaScript代碼,是JavaScript運行期性能優化的關鍵。
使用 Object/Array 直接量
經過延遲加載和條件預加載,避免重複工做
使用語言中速度快的部分,如:位操做(& | ^ ~
)、原生方法
構建和部署的過程對基於js的web應用的性能有着巨大影響。這個過程當中最重要的步驟有:
使用Gzip合併、壓縮js文件,可以減小約70%的體積。
經過正確設置HTTP響應頭來緩存js文件,經過向文件名增長時間戳來避免緩存問題。
使用CDN提供js文件;CDN不只能夠提高性能,也幫助管理文件的壓縮與緩存。
使用Webpack構建。
拓展:前端構建工具的發展
主要分析方面:
性能分析
網絡分析
JavaScript 在不斷髮展和擴充它的邊界,咱們也要不斷學習大量的優化技術和方法。當把這些策略應用在項目中時,將會看到性能的明顯提高,這也就是細節決定成敗。
最後,培養和保持良好的開發習慣,對於我的發展和團隊合做都是頗有必要的,推薦閱讀《高性能JavaScript》這本小薄書。🤗