精讀《2017前端性能優化備忘錄》
本期精讀的文章是:Front End Performance Checklist 2017
現在隨著 web 應用的複雜性日益增加,其性能優化就會顯得尤為必要,同時會給性能指標分析帶來新的挑戰,因為性能指標之間的差異性非常大,這取決於使用的設備、瀏覽器、協議、網路類型以及其它能夠對性能產生影響的潛在因素(如:CDN、ISP、cache、proxy、firewall、load balancer、server等)。
1 引言
本文提供了解決如何讓網站響應更加迅速、訪問更加流暢等前端性能優化問題的方法,讀者們可以提供一些在實際場景中的性能優化問題以及解決方案,可泛談優化策略,亦可針對性深入討論某個優化方法。
2 內容概要
文中列舉了很多不同的性能優化策略、模型或方法,如下:
制定目標
網站速度快於他人 20%
根據 psychological research 指出,網站最少在速度上比別人快 20%,才能讓用戶感覺到比別人的更快。這個速度說的並不是整個頁面的載入時間,而是啟動渲染時間,首次有效渲染時間,交互時間。
控制響應時間在100ms,控制幀速在60幀/秒
RAIL performance model 提出的性能優化指標:務必在用戶初始操作後的 100ms 內提供反饋。考慮到存在響應時間不足 100ms 的情況,頁面最遲要在 50ms 的時候,把控制權交給主線程。
針對動畫,其每一幀都需要在 16ms 內完成,這樣才能保證每秒 60幀(一秒/60=16.6ms),如果可以的話最好能在 10ms 內完成。
控制首次有效渲染時間在 1.25s,控制 SpeedIndex 在 1000
控制啟動渲染時間在 1s 以內,且速度指數在 1000 以內,對於首次有效渲染時間,最好可以優化到 1.25s 以內。
環境搭建
做好構建工具的選型
不要過度使用那些酷炫的技術棧,堅持選擇適合開發環境的工具,如Grunt、Gulp、Webpack、PostCSS,或者組合起來的工具。只要這個工具運行的速度夠快,而且沒有給項目維護帶來太大問題,就夠了。
漸進增強
在構建前端結構的時,應始終將漸進增強作為指導原則。首先設計並且構建核心體驗,再完善為高性能瀏覽器設計的高級特性的相關體驗。
前端框架
最好使用那些支持伺服器端渲染的框架,如Angular,React,Ember 等。所選的框架要保證是被廣泛使用並且經過考驗的。不同框架對性能有著不同程度的影響,同時對應著不同的優化策略,所以要清楚的了解所選擇框架的每個方面。
AMP 或 Instant Articles
- Google 的 AMP 技術會提供一套可靠的性能優化框架(基於免費的CDN網路)
- Facebook 的 Instant Articles 技術可以在 Facebook 上提升網站的性能。
合理利用 CDN
根據網站的動態數據量,可以將部分內容給靜態網站生成工具生成一個靜態版本,將其置於 CDN 上,從而避免資料庫的請求,亦可選擇基於 CDN 的靜態主機平台,通過交互組件豐富頁面。
優化構建
確定優先順序
將網站的所有文件(js,圖片,字體,第三方 script 文件,多媒體內容等)進行分門別類。根據優先順序區分基礎核心內容,高性能瀏覽器設計的升級體驗,附加內容等。具體細節可參考 Improving Smashing Magazine』s Performance。
使用 cutting-the-mustard 技術
使用 cutting-the-mustard 技術能夠實現不同類型的瀏覽器載入不同類型的資源(傳統瀏覽器載入核心型資源,現代瀏覽器載入增強型資源)。在載入資源時要嚴格遵守相應的規則:頁面載入時應首先載入 Core 資源,然後在 DomContentLoaded 事件觸發時載入 Enhancement 資源,最後在 Load 事件觸發時載入 Extras 資源。
micro-optimization 和 progressive booting
- 使用 skeleton screens 代替loading indicator 展示
- 使用能夠加速 App 初始化渲染的技術,如 tree-shaking、code-splitting
- 針對服務端渲染增加預編譯環節
- 使用 Optimize.js 來加快初始載入速度,其原理是包裝優先順序高的調用函數
- 漸進啟動,先通過使用伺服器端渲染快速完成首次有效渲染,瀏覽器再通過少量的 JS 代碼就可以讓交互時間接近於首次有效渲染時間。
正確設置 HTTP cache header
需要正確設置 expires、cache-control、max-age以及其它 HTTP 緩存響應頭。請使用 Cache-control: immutable,可以參考 Heroku』s primer on HTTP caching headers、HTTP caching primer以及緩存之最佳實踐。
減少使用第三方庫,非同步載入 JS
想要在不等 js 執行完就開始渲染頁面,可以通過在 HTML 的 script 標籤上添加 defer 以及 async 屬性來實現。減少第三方庫和腳本的使用,尤其是社交網站的分享按鍵和 iframe 嵌入等。
合理優化圖片
- 要實現圖片的響應式,應儘可能地使用帶有 srcset、sizes 屬性的 HTML 標籤,如 <picture>
- 使用 WebP 格式的圖片
圖片優化進階
- 可以使用漸進式 JPEG 圖片
- 可以使用壓縮工具對不同格式的圖片進行壓縮,如 JPEG 圖片用 mozJPEG 壓縮、PNG 圖片用 Pingo 壓縮、GIF 圖片用 Lossy GIF 壓縮、SVG 圖片用 SVGOMG 壓縮
- 可以通過過濾掉不必要的圖片細節(通過給圖片添加高斯模糊濾鏡實現)來減小文件的大小
- 可以使用 PhotoShop 導出(質量在 0-10%)的圖片用於做背景圖
- 可以使用多張背景圖的技巧來提高對圖片性能感知的能力
優化 web 字體
- 如果使用開源字體,可以使用字體庫中的子集或自己歸類的子集來壓縮文件大小
- 瀏覽器對 WOFF2 的支持度較高,當瀏覽器不支持 WOFF2 時,可以將 WOFF、OTF 作為備用
- 可以從 Comprehensive Guide to Font-Loading Strategies 中選擇一些針對字體優化的策略
- 可以使用 service worker 來達到字體緩存持久化
- 關於如何快速入門字體優化的教程
快速推送 critical CSS文件
為了保證能夠讓瀏覽器快速渲染,會將所有用於首屏渲染的 CSS 文件整合成一個文件(即 critical CSS),以 <style> 的行內形式內嵌到 <head>,這樣可以減少 critical 渲染路徑。由於 HTTP 數據包大小的限制,因此 critical CSS 文件大小不能超過14KB。
HTTP/2 協議可以讓 critical CSS 用單個 CSS 文件存儲,通過伺服器推送 CSS 文件的傳輸方式來減少HTML 文件數據量,由於存在高速緩存問題,因此需要建立帶有緩存的 HTTP/2 伺服器傳輸機制。
tree-shaking 和 code-splitting 機制減輕負載
- Tree-shaking 機制能夠幫助清理生產環境中的冗餘代碼。可以通過 Webpack2 Tree-Shaking 機制來清理冗餘的 exports 代碼或者使用 UnCSS、Helium 工具來清理冗餘的CSS代碼
- code splitting 機制是Webpack 的另一個特性,它能夠將構建的代碼分成多個 chunk,並且對 chunk 按需載入。只要在代碼中定義了分離點(split point),Webpack 便會處理好相關的輸出文件,不僅能夠較少文件數據量,而且還能對代碼做到按需載入。
- 用 Rollup 來 export 代碼也能夠取得不錯的效果
提升渲染性能
可以通過使用 css containment 屬性的方式來達到隔離性能開銷大的組件,限制瀏覽器樣式的範圍,限制作用在 canvas 以外的布局和繪製工作中,限制用在第三方工具上,以確保頁面滾動和出現動畫效果時沒有延遲。推薦使用 CSS 屬性 will-change,該屬性能夠在元素的屬性改變之前通知瀏覽器。
需要衡量瀏覽器在處於運行時渲染模式下的性能,可以參考瀏覽器渲染優化、如何正確的使用 GPU。
優化網路環境,加快網路傳輸
- 使用 skeleton screen 或者使用懶載入的方式載入字體或者開銷大的組件,如視頻、iframe、圖片等
- dns-prefetch,能夠讓瀏覽器在後台進程執行一次 DNS 查詢
- preconnect,能夠讓瀏覽器在後台進程發起一次握手(DNS,TCP,TLS)
- prefetch,能夠讓瀏覽器發起對資源的請求
- prerender,能夠讓瀏覽器在後台進程渲染出特定的頁面
- preload,在不執行資源的前提下,預先拿到該資源
HTTP/2
為 HTTP/2 環境的搭建做好準備
從目前來看,瀏覽器對 HTTP/2 支持度還不錯,使用 HTTP/2 後,就可以利用 service worker 以及 HTTP/2 的伺服器推送功能來獲取更顯著的性能提升。
在項目進行 HTTPS 改造時,需要評估 HTTP/1.1 項目的用戶基數,需要針對這類用戶構建並發送符合HTTP2規範的報頭。
正確部署 HTTP/2
需要在載入大模塊以及並行載入小模塊之間找到一個平衡點。
- 將所有視圖都分散到小模塊中,然後在項目構建的過程中完成對小模塊的壓縮,最後通過 scount approach 以及非同步的方式來分別實現對模塊的引用及載入,對一個文件將不再需要重新下載整個樣式清單或 js 文件
- HTTP/2 環境下打包 js 文件時存在問題,由於向瀏覽器發送很多 js 小文件的過程中會存在很多問題。 首先,文件壓縮的優勢被破壞。在壓縮大文件的過程中,藉助 dictionary reuse 可以達到優化性能的目的,然而單個小文件就不能。其次,瀏覽器不能針對一些工作流進行優化
確保伺服器的安全性
需要檢查是否正確設置 HTTP 請求頭部,如 strict-transport-security,使用 Snyk 工具排除已知的漏洞以及使用 SSL Server Test 網站來檢查證書是否失效。 盡量保證從外部引入的插件以及 js 腳本的載入是通過 HTTPS 協議的,發起 HTTP 請求同時設置 strict-transport-security 以及 content-security-policy HTTP請求頭。
伺服器和 CDN 是否支持 HTTP/2
通過 Is TLS Fast Yet 來查看不同伺服器和 CDN 對 HTTP/2 的兼容情況。
Brotli 或 Zopfli 壓縮演算法
- Brotli,是 Google 開源的無損數據格式,其壓縮效率要遠高於 Gzip 和 Deflate
- Zopfli壓縮演算法,能夠將數據編碼成 Deflate、Gzip、Zlib 數據格式。用 Zopfli 演算法壓縮過後的文件能夠比同樣用 Zlib 演算法壓縮的文件小 3%-8%
激活 OCSP stapling
激活伺服器的 OCSP stapling,可以減少 TLS 握手所需的時間,加速 TLS 握手過程。
使用 IPv6
因為 IPv6 自帶 NDP 以及路由優化,能夠讓網站的載入速度提升 10%-15%。
HPACK 壓縮演算法
如果網站使用了 HTTP/2,需要檢查伺服器有沒有執行 HPACK 對 HTTP 的響應頭進行壓縮,來減少不必要的消耗。
使用 service worker
如果網站切換到 HTTPS,可以使用 pragmatist-service-worker 通過 service worker cache 來緩存靜態資源、離線頁面等,也可以從緩存中拿數據。參考當前瀏覽器對 service worker 的支持程度。
測試與監控
監控警告
- 通過 Report-URI.io 工具監控混合內容中出現的警告
- 通過 Mixed Content Scan 工具掃描支持 HTTPS 的網站是否存在混合內容
使用 Devtools
在 DevTool 中選一個調試工具來對每一個功能進行檢查,確保知道如何分析渲染性能和控制台輸出、明白如何調試 JS 以及編輯 CSS 樣式。參考開發者工具的調試技巧。
使用代理瀏覽器或過時瀏覽器測試
完成 Chrome 和 Firefox 的測試是不夠的,還需要關注部分區域佔比較高的瀏覽器,如 UC 瀏覽器、Opera Min 等, 也需要了解一下受關注國家的平均網速。
持續監控
在進行快速、無限制的測試時,最好使用一個個人的WebPageTest實例。建立一個能自動預警的性能預算監聽。建立自己的用戶時間標記從而測量並監測具體商用的數據。使用SpeedCurve對性能的變化進行監控,同時利用New Relic獲取WebPageTest沒法提供的數據。SpeedTracker,Lighthouse和Calibre都是不錯的選擇。
部署私密的 WebPageTest 測試環境,有助於快速構建測試用例。針對性能開銷大的環節建立自動報警機制,可以使用 SpeedCurve 對性能的變化進行監控,利用 New Relic 獲取 WebPageTest 無法提供的數據。
3 精讀
這一部分會介紹一些上述沒有提到的方法,主要是利用 Devtools 工具對性能優化策略或方法進行深入的解讀和分析。
通過 Devtools 排查渲染性能問題
頁面代碼被轉換成屏幕上顯示的像素,這個轉換過程可以簡單歸納為以下流程,包含五個關鍵步驟:
- Javascript
- Style
- Layout
- Paint
- Composite
Timeline
通過 Chrome Timeline 對頁面進行 Record,其中綠色波浪線就是頁面的幀率。波浪線越高表示幀率越高,反之亦然,幀率區域上邊標紅一行區域,表示有問題的幀,凡是標紅的幀都是存在問題的,排查問題時,需要著重關注幀率低和標紅的區域。
需要逐一排查帶紅色角標的幀,即是有問題的幀:
點擊選中該幀,可以看到詳細的耗時和簡單的問題描述:
Javascript Profiler
如果發現運行時間很長的 JavaScript 代碼,則可以開啟 DevTools 中 JavaScript profiler 選項,可以看到頁面中的函數調用鏈路,就能分析出 JavaScript 代碼對於頁面渲染性能的影響,從而發現並修復 JavaScript 代碼中性能低下的部分。那麼如何修復 JavaScript 代碼中性能問題呢?
使用 requestAnimationFrame
假設頁面上有一個動畫效果,想在動畫剛剛發生的那一刻運行一段 JavaScript 代碼。那麼唯一能保證這個運行時機的,就是 requestAnimationFrame。而大部分代碼都是用 setTimeout 或 setInterval 來實現頁面中的動畫效果。這種實現方式的問題是,setTimeout 或 setInterval 中指定的回調函數的執行時機是無法保證的,如果是在幀結束的時候被執行,就意味著可能失去這一幀的信息,也就是發生 jank。
降低代碼複雜度或者使用 Web Workers
JavaScript 代碼是運行在瀏覽器的主線程上的。與此同時,瀏覽器的主線程還負責樣式計算、布局,甚至繪製等的工作。可以想像,如果 JavaScript 代碼運行時間過長,就會阻塞主線程上其他的渲染工作,很可能就會導致幀丟失。
因此,需要規劃 JavaScript 代碼的運行時機和運行耗時,或在瀏覽器空閑的時候來來運行更多的 JavaScript 代碼。
也可以把純計算工作放到 Web Workers 中做,前提是這些計算工作不會涉及 DOM 元素的存取。一般來說,JavaScript 中的數據處理工作,如排序或搜索比較適合這種處理方式。
如果 JavaScript 代碼需要存取 DOM 元素,即必須在主線程上運行,那麼可以考慮批處理的方式,把任務細分為若干個小任務,每個小任務耗時很少,各自放在一個 requestAnimationFrame 中回調運行。
Render(Style & Layout)
render 部分包括 Recalculate Style 和 Layout,如果發現 render 部分耗時較長,需要分別從這兩部分進行分析。如果這一幀,觸發了強制 layout,Timeline 會用紅色角標標出,這是需要進行優化的地方。
如果需要具體分析 Recalculate Style,可以選中 Recalculate Style 部分,查看受影響的元素個數、觸發 Recalculate Style 函數以及警告提示。
如果需要分析 Layout,可以選中 Layout 部分,同 Recalculate Style 一樣。
那麼如何提升 Render 部分的性能問題呢?
降低樣式計算和複雜度
添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操作,都會引起DOM結構的改變,從而導致瀏覽器需要重新計算每個元素的樣式、對頁面或其一部分重新布局(多數情況下),這就是所謂的樣式計算。
因此需要減少執行樣式計算的元素的個數,降低樣式選擇器的複雜度,使用基於 class 的方式,如以BEM (Block, Element, Modifier)的方式編寫 CSS 代碼,能達到最好的樣式計算的性能,因為這種方式建議對每個DOM元素都只使用一個樣式class。
避免大規模、複雜的布局
布局,就是瀏覽器計算 DOM 元素的幾何信息的過程:元素大小和在頁面中的位置。
- 儘可能避免觸發布局,當修改了元素的樣式屬性之後,瀏覽器會將會檢查為了使這個修改生效是否需要重新計算布局以及更新渲染樹。對於 DOM 元素的幾何屬性的修改,比如 width/height/left/top 等,都需要重新計算布局。通過 DevTools Timeline 可以查看頁面性能的分解圖,從而判斷布局過程是否是頁面性能的瓶頸,參考能觸發布局、繪製或渲染層合併的 CSS 屬性清單
- 使用 flexbox 替代老的布局模型,在相同數量的元素下 Flexbox 布局,不僅達到了同樣的顯示效果,而且時間消耗也大大降低,因此需要在對頁面布局模型的性能分析的基礎之上,來選擇一種性能最優的布局方式,而且應該努力避免同時觸發所有布局
- 避免強制同步布局事件的發生,將一幀畫面渲染到屏幕上的處理順序是執行 JavaScript 腳本、樣式計算、布局。但還可以強制瀏覽器在執行 JavaScript 腳本之前先執行布局過程,這就是所謂的強制同步布局。為了避免觸發不必要的布局過程,應該首先批量讀取元素樣式屬性,然後再對樣式屬性進行寫操作,過早地同步執行樣式計算和布局是潛在的頁面性能的瓶頸之一
- 避免快速連續的布局,如果想確保編寫的讀寫操作是安全的,你可以使用 FastDOM,它能幫你自動完成讀寫操作的批處理,還能避免意外地觸發強制同步布局或快速連續的布局
Paint
Paint(繪製)其實是生成元素呈現的像素的過程。在頁面的整個被解析、執行、渲染的過程中,Paint 通常來說是代價最高的一步,因此盡量減少 Paint 時間,甚至避免 Paint 的發生,對頁面性能的提升有著很重要的作用。
如何觸發 Paint
- 觸發了 Layout,那麼一定會觸發 Paint
- 改變元素的一些非幾何屬性,如背景、顏色、陰影等,不會觸發 Layout,但是依然會觸發 Paint
如何定位 Paint
Timeline 中綠色部分就是 Paint 部分,Summary 會展示繪製的總體情況,包括繪製的元素、元素本身繪製耗時、元素子元素繪製耗時。如果發現繪製的區域超過了本來期望的區域,那麼就是需要優化的。更加詳細的信息,可以切換至 Paint Profiler,包括了每個具體 Paint 的調用和 Paint 區域截圖。當頁面發生 Paint 時,如果發現不期望的區域進行了 Paint,那麼這裡就是可以優化的。
如何優化 Paint
- 提升元素渲染層為合成層,頁面的繪製並非是在單層畫面里完成的,瀏覽器的渲染原理,是瀏覽器將 DOM tree 映射成 GraphicsLayer tree,中間是經過了 RenderObject、RenderLayer 的一系列映射。元素所在的層提升為合成層後可以減少 Repaint
- 使用 transform 或 opacity 實現動畫,對於獨立的合成層應用 transform 和 opacity 是不會觸發 Repaint的,因此盡量對 transform 或 opactiy 應用動畫來實現效果
- 減少繪製區域,對於不需要重新繪製的區域應盡量避免繪製,已減少繪製區域,比如一個 fix 在頁面頂部的固定不變的導航 header,在頁面底部某個區域 Repaint 時,整個屏幕包括 fix 的 header 也會被重繪,而對於固定不變的區域,期望其並不會被重繪,因此可以通過之前的方法,將其提升為獨立的合成層
- 降低繪製複雜度,對於無法避免的 Paint,需要儘可能的減少 Paint 的消耗,有些效果的 Paint 代價十分昂貴,比如繪製一個陰影可能就比繪製一個邊框更加耗時,因此開發過程中,需要研究能夠實現相同的效果,同時卻能達到更小的 Paint 消耗的方法
Composite
渲染層合併,對頁面中 DOM 元素的繪製是在多個層上進行的。在每個層上完成繪製過程之後,瀏覽器會將所有層按照合理的順序合併成一個圖層,然後顯示在屏幕上。
提升為合成層簡單說來有以下優點:
- 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理更快
- 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層
- 對於 transform 和 opacity 效果,不會觸發 layout 和 paint
- 對於諸如 fixed 的合成層,移動時不會觸發 repaint
提升動畫效果的元素
合成層的好處是不會影響到其他元素的繪製,因此,為了減少動畫元素對其他元素的影響,從而減少 paint,可以把動畫效果中的元素提升為合成層。提升合成層的最好方式是使用 CSS 的 will-change 屬性。
合理管理合成層
創建一個新的合成層並不是無消耗的,它得消耗額外的內存和管理資源。實際上,在內存資源有限的設備上,合成層帶來的性能改善,可能遠遠趕不上過多合成層開銷給頁面性能帶來的負面影響。同時,由於每個渲染層的紋理都需要上傳到 GPU 處理,因此還需要考慮 CPU 和 GPU 之間的帶寬問題、以及有多大內存供 GPU 處理這些紋理的問題。
防止層爆炸
同合成層重疊也會使元素提升為合成層,雖然有瀏覽器的層壓縮機制,但是也有很多無法進行壓縮的情況。因此顯式聲明的合成層,還可能由於重疊原因不經意間產生一些不在預期的合成層,極端一點可能會產生大量的額外合成層,出現層爆炸的現象。
3 總結
現在隨著 web 應用的複雜性日益增加,其性能優化的重要性越來越突出,且性能優化的方法、技巧、工具也越來越豐富和複雜,本文所展示的內容僅僅只是管中窺豹,希望讀者們可以在此討論一些在實際場景中的性能優化問題以及解決方案。
討論地址是:精讀《2017前端性能優化備忘錄》 · Issue #39 · dt-fe/weekly
如果你想參與討論,請點擊這裡,每周都有新的主題,每周五發布。
推薦閱讀:
※如何不擇手段提升scroll事件的性能
※「每日一題」你是如何做性能優化的?
※Angular AOT優化構建嘗試
※PWA 漸進式實踐 (1) - Lighthouse in Action
TAG:前端性能优化 |