最近工做中一個項目在運行時有一些性能問題,爲此我看了不少與性能優化相關的內容,下面作個簡單的分享。javascript
前端性能優化,這包括 CSS/JS 性能優化、網絡性能優化等等內容,這方面的內容 《高性能網站建設指南》、《高性能網站建設進階指南》、《高性能JavaScript》 等等書都作了不少講解,強烈推薦閱讀。css
下面的內容,上面提到的書中大都包含了,所以能夠考慮轉而去讀這些書,作一個完徹底全的瞭解,對於本文,也就不要再讀下去了。html
若是你堅持看到了這裏,那就來談談我遇到的一些前端性能問題,並聊聊解決方案。前端
優先優化對性能影響大的部分
當應用有了性能問題後,不要一股腦扎到代碼中去,首先要想一想那部分對性能影響最大。優先優化那些對性能影響大的部分,能夠起到立杆見影的效果。
使用 Chrome DevTools ,能夠很快地找到致使性能變差的最主要因素,關於 Chrome DevTools 的使用強烈推薦閱讀 Google Developers 上面的系列教程 - Chrome DevTools。java
另外在對代碼進行優化的時候,也首先要關注那些存在循環或者高頻調用的地方。有的時候咱們可能不知道某個地方是否會高頻執行,好比某些事件的回調。這個時候可使用 console.count
來對執行次數進行統計。當這部分高頻執行的代碼已經足夠優化的時候,就要考慮是否可以減小執行次數。好比一個時間複雜度爲 O(n*n*n)
的算法,再怎麼優化也不如將其變爲 O(n*n)
來的快。node
對高頻觸發的事件進行節流或消抖
對於 Scroll 和 Touchmove 這類事件,永遠不要低估了它們的執行頻率,處理這類事件的時候能夠考慮是否要給它們添加一個節流或者消抖過的回調。節流和消抖,可能其餘人不這麼翻譯,其實也就是 debounce
和 throttle
這兩個函數。webpack
debounce
和 throttle
是兩個類似(但不相同)的用於控制函數在某段事件內的執行頻率的技術。你能夠在 underscore 或者 lodash 中找到這兩個函數。git
使用 debounce
進行消抖
屢次連續的調用,最終實際上只會調用一次。想象本身在電梯裏面,門將要關上,這個時候另一我的來了,取消了關門的操做,過了一下子門又要關上,又來了一我的,再次取消了關門的操做。電梯會一直延遲關門的操做,直到某段時間裏沒人再來。github
因此 debounce
適合用在好比對用戶輸入內容進行校驗的這種場景下,屢次觸發只須要響應最後一次觸發就行了。web
使用 throttle
進行節流
將頻繁調用的函數限定在一個給定的調用頻率內。它保證某個函數頻率再高,也只能在給定的事件內調用一次。好比在滾動的時候要檢查當前滾動的位置,來顯示或隱藏回到頂部按鈕,這個時候可使用 throttle
來將滾動回調函數限定在每 300ms 執行一次。
須要提到的是,這兩個函數經常被誤用,且不少時候當事人並無意識到本身誤用了。我曾經用錯過,也見過別人用錯。這兩個函數都接受一個函數做爲參數,而後返回一個節流/去抖後的函數,下面第二種用法纔是正確的用法:
1 |
// 錯誤的用法,每次事件觸發都獲得一個新的函數 |
JavaScript 很快,DOM 很慢
JavaScript 現在已經很快了,真正慢的是 DOM。所以避免使用一些不易讀但聽說能提升速度的寫法。不久前,
一位朋友對我說使用 ‘+’ 號將字符串轉爲數字比使用 parseInt
快。對此我並無懷疑,由於直覺上 parseInt 進行了函數調用,極可能會慢一些,咱們一塊兒在 node v6.3.0 上進行了一些驗證,結果的確如咱們所預計的那樣,可是差異有多大呢,進行了 5 億次迭代,使用 +
號的方法僅僅快了2秒。雖然快了兩秒,但實際中將字符轉爲數字的操做可能只會進行幾回,所以這樣的作法根本沒有意義,它只會讓代碼變得更難讀。
1 |
plus: 1694.392ms |
真正慢的是 DOM,DOM 對外提供了 API,而 JavaScript 能夠調用這些 API,它們二者就像是使用一座橋樑相連,每次過橋都要被收取大量費用,所以應該儘可能讓減小過橋的次數。
爲何 DOM 很慢
談到這裏須要對瀏覽器利用 HTML/CSS/JavaScript 等資源呈現出精彩的頁面的過程進行簡單說明。瀏覽器在收到 HTML 文檔以後會對文檔進行解析開始構建 DOM (Document Object Model) 樹,進而在文檔中發現樣式表,開始解析 CSS 來構建 CSSOM(CSS Object Model)樹,這二者都構建完成後,開始構建渲染樹。整個過程以下:
在每次修改了 DOM 或者其樣式以後都要進行 DOM樹的構建,CSSOM 的從新計算,進而獲得新的渲染樹。瀏覽器會利用新的渲染樹對頁面進行重排和重繪,以及圖層的合併。一般瀏覽器會批量進行重排和重繪,以提升性能。但當咱們試圖經過 JavaScript 獲取某個節點的尺寸信息的時候,爲了得到當前真實的信息,瀏覽器會馬上進行一次重排。
避免強制性同步佈局
在 JavaScript 中讀取到的佈局信息都是上一幀的信息,若是在 JavaScript 中修改了頁面的佈局,好比給某個元素添加了一個類,而後再讀取佈局信息。這個時候爲了得到真實的佈局信息,瀏覽器須要強制性對頁面進行佈局。所以應該避免這樣作。
批量操做 DOM
在必需要進行頻繁的 DOM 操做時,可使用 fastdom 這樣的工具,它的思路是將對頁面的讀取和改寫放進隊列,在頁面重繪的時候批量執行,先進行讀取後改寫。由於若是將讀取與改寫交織在一塊兒可能引發屢次頁面的重排。而利用 fastdom 就能夠避免這樣的狀況發生。
雖然有了 fastdom 這樣的工具,但有的時候仍是不能從根本上解決問題,好比我最近遇到的一個狀況,與頁面簡單的一次交互(輕輕滾動頁面)就執行了幾千次 DOM 操做,這個時候核心要解決的是減小 DOM 操做的次數。這個時候就要從代碼層面考慮,看看是否有沒必要要的讀取。
另一些關於高效操做 DOM 的方法,能夠參見《高性能 JavaScript》相關章節,也能夠先參考一下個人讀書筆記 《高性能 JavaScript》
優化渲染性能
瀏覽器一般每秒更新頁面 60 次,每一幀的時間就是 16.6ms,爲了能讓瀏覽器保持 60幀 的幀率,爲了讓動畫看起來流暢,須要保證幀率達到 60fps,所以每一幀的邏輯須要在 16.6ms 內完成。
每一幀實際上都包含下列步驟:
所以,一般 JavaScript 的執行時間不能超過 10ms。
- JavaScript:改變元素樣式,添加元素到 DOM 中等等
- Style:元素的類或者style改變了,這個時候須要從新計算元素的樣式
- Layout:須要從新計算元素的具體尺寸
- Paint:將元素的繪製的圖層上
- Composite:合併多個圖層
固然也不是說每一幀都會進行這些操做。當你的 JavaScript 改變了某個 layout 屬性,好比元素的 width
和 height
或者 top
等等,瀏覽器就會從新計算佈局,並對整個頁面進行重排。
若是修改了 background
、color
這樣的僅僅會讓頁面重繪的屬性,這不會影響頁面的佈局,瀏覽器會跳過計算佈局(layout)的過程,只進行重繪(paint)。
若是修改了一個不須要計算佈局也不須要重繪的屬性,那就只會進行圖層的合併,這是代價最小的修改。從 https://csstriggers.com/ 上你能夠知道修改那些樣式屬性會觸發(Layout,Paint,Composite)中的那些操做。
將漸變或者會動畫元素放到單獨的繪製層中
繪製並不是在一個單獨的畫布上進行的,而是多層。所以將那些會變更的元素提高至單獨的圖層,可讓他的改變影響到的元素更少。
可使用 CSS 中的 will-change: transform;
或者 transform: translateZ(0);
這樣來將元素提高至單獨的圖層中。
在調試的時候你能夠在 Chrome DevTools 的 timeline 面板來觀察繪製圖層。固然也不是說圖層越多越好,由於新增長一個圖層可能會耗費額外的內存。且新增長一個圖層的目的是爲了不某個元素的變更影響其餘元素。
下降繪製複雜度
某些屬性的重繪相對而言更加複雜,好比 filter、box-shadow 等濾鏡或漸變效果。所以不要濫用這類效果。
優化 JavaScript 的執行
下面提到的 JavaScript 優化,並非說如何讓 JavaScript 執行的更快,而是如何讓 JavaScript 更高效地與 DOM 配合。
使用 requestAnimationFrame
來更新頁面
咱們但願在每一幀剛開始的時候對頁面進行更改,目前只有使用 requestAnimationFrame
可以保證這一點。使用 setTimeout
或者 setInterval
來觸發更新頁面的函數,該函數可能在一幀的中間或者結束的時間點上調用,進而致使該幀後面須要進行的事情沒有完成,引起丟幀。
requestAnimationFrame
會將任務安排在頁面重繪以前,這保證動畫能有足夠的時間來執行 JavaScript 。
使用 Web Worker 來處理複雜的計算
JavaScript 是在單線程的,而且可能會一直這樣,所以 JavaScript 在執行復雜計算的時候極可能會阻塞線程,致使頁面假死。但 Web Worker 的出現,以另一種方式給了咱們多線程的能力,能夠將複雜計算放在 worker 中進行,當計算完成後,以 postMessage
的形式將結果傳回來。
對於單個函數,由於 Web Worker 接受一個腳本的 url 做爲參數,使用 URL.createObjectURL
方法,咱們能夠將一個函數的內容轉換爲 url,利用它建立一個 worker。
1 |
var workerContent = ` |
使用 transform 和 opacity 來完成動畫
現在只有對這兩個屬性的修改不須要經歷 layout 和 paint 過程。
優化 CSS
CSS 選擇器在匹配的時候是由右至左進行的,所以最後一個選擇器常被稱爲關鍵選擇器,由於最後一個選擇越特殊,須要進行匹配的次數越少。要千萬避免使用 *
(通用選擇器)做爲關鍵選擇器。由於它能匹配到全部元素,進而倒數第二個選擇器還會和全部元素進行一次匹配。這致使效率很低下。
1 |
/* 不要這樣作 */ |
另外 first-child
這類僞類選擇器也不夠特殊,也要避免將它們做爲關鍵選擇器。關鍵選擇器越特殊,瀏覽器就能用較少的匹配次數找到待匹配元素,選擇器性能也就越好。
還有一個老生常談的注意事項,不要使用太多的選擇器。若是還有同窗很悲劇地要兼容低版本 IE,要避免使用 CSS 表達式,它的性能不好,詳細內容可參見我以前記錄的一篇筆記 《高性能網站建設指南》筆記
合理處理腳本和樣式表
現在有了 requirejs,webpack 等工具,可能不多會在頁面中加載不少 JavaScript/CSS 代碼了。儘管如此,仍是有必要談談如何合理處理腳本和樣式表。
大多數人已經知道一般要把 JavaScript 放在文檔底部,把 CSS 放在文檔頂部。爲何呢?由於 JavaScript 會阻塞頁面的解析,而外部樣式表會阻塞頁面的呈現和 JavaScript 的執行。
CSS阻塞渲染
一般狀況下 CSS 被認爲是阻塞渲染的資源,在CSSOM 構建完成以前,頁面不會被渲染,放在頂部讓樣式表可以儘早開始加載。但若是把引入樣式表的 link 放在文檔底部,頁面雖然能馬上呈現出來,可是頁面加載出來的時候會是沒有樣式的,是混亂的。當後來樣式表加載進來後,頁面會當即進行重繪,這也就是一般所說的閃爍了。
JavaScript 阻塞文檔解析
當在 HTML 文檔中遇到 script 標籤後控制權將交給 JavaScript,在 JavaScript 下載並執行完成以前,都不會解析 HTML。所以若是將 JavaScript 放在文檔頂部,剛好這個時候 JavaScript 腳本加載的特別慢,用戶將會等待很長一段時間,這段個時候 HTML 文檔尚未解析到 body 部分,頁面會是空白的。
另外經常被忽略的事實是:在瀏覽器沒有下載並解析完成使用 link 引入的 CSS 文件以前,JavaScript 是不會執行的,由於 JavaScript 中可能須要讀取樣式,而此時樣式表尚未加載回來,所以瀏覽器不會執行 JavaScript。能夠給 JavaScript 加上 async 標記,表示 JavaScript 的執行不會讀取 DOM ,JavaScript 能夠不被 CSS 阻塞,能夠在空閒時間馬上執行。
綜上所述,你更要保證 CSS 文件加載的足夠快。
關於這部份內容, 《高性能網站建設指南》 上有很精彩的講解,牆裂推薦。《高性能網站建設指南》我在讀的時候記錄了筆記,能夠在這裏看到。
最後強烈推薦閱讀 Google Developers 中關於性能優化的系列文章。
參考資料
- 《高性能網站建設指南》
- 《高性能網站建設進階指南》
- 《高性能JavaScript》
- Google Developers
- Efficient JavaScript
- Best Practices for Speeding Up Your Web Site
本文章轉自:http://ymfe.tech/blog/2016-09-24-fe-performance-optimization/
分享技術,分享快樂!