從案例分析如何優化前端效能

發表於2016-08-30

De Voorhoede工作的日子裡,我們一直在追尋為使用者構建高效能的前端解決方案。不過並不是每個客戶會樂於遵循我們的效能指南,以至於我們必須一遍又一遍地跟他們解釋那些保證他們能夠戰勝競爭對手的效能策略的重要性。最近我們也重構了自己的官方主頁,使其能夠擁有更快地響應速度與更好地效能表現。
screenshot-of-site

效能調優始於設計

在前端專案中,我們常常與產品經理以及UI設計討論如何在美感與效能之間達到平衡,我們堅信更快地內容呈現是好的使用者體驗的不可分割的一部分。在我們自己的網站中,我們是以效能優於美感。好的內容、佈局、圖片與互動都是構成你網站吸引力的不可或缺的部分,不過這些複雜的元素的使用往往也意味著頁面載入速度的增加。設計的核心即在於決定我們網站需要呈現哪些內容,往往這裡的內容會指圖片、字型這樣的偏靜態的部分,我們首先也從對於靜態內容的優化開始。

Static Site Generator

為了演示與測試方便,我們基於NodeJS搭建了一個混合使用MarkDown與JSON作為配置的靜態網站生成器,其中一個簡單的部落格型別的網站的配置資訊如下:

而其內容為:

下面,我們就這個靜態網站,進行一些討論。

Image Delivery

圖片是網站的不可或缺的部分,其能夠大大提升網站的表現力與視覺效果,而目前平均大小為2406KB的網頁中就有1535KB是圖片資源,可見圖片佔據了靜態資源多麼大的一個比重,這也是我們需要重點優化的部分。
1585402170-57bfe50058e0b_articlex

WebP

WebP 是面向現代網頁的高壓縮低損失的圖片格式,通常會比JPEG小25%左右。然後WebP目前被很多人忽視,也不常使用。截止到本文撰寫的時候,WebP目前只能夠在Chrome, Opera and Android (大概佔使用者數的 50%)這些瀏覽器中使用,不過我們還是有辦法以JPG/PNG來彌補部分瀏覽器中不支援WebP的缺憾。

picture標籤

使用picture標籤可以方便的對於WebP格式不支援的情況下完成替換:

這裡我們使用了 picturefill by Scott Jehl作為Polyfill庫來保證低版本的瀏覽器中能夠支援picture標籤,並且保證跨瀏覽器的功能一致性。並且我們還使用了img標籤來保證那些不支援picture的瀏覽器能夠正常工作。

圖片多格式生成

現在我們已經可以通過設定不同的圖片尺寸、格式來保證圖片的分發優化,不過我們總不希望每次要用一張圖片的時候就去生成6個不同的尺寸/例項。我們希望有一種抽象的方法可以幫我們自動完成這一步,為我們自動生成不同的格式/尺寸,然後自動插入合適的picture元素,在我們的靜態網站生成器中是這麼做的:

  • 首先是要gulp responsive來生成不同尺寸的圖片,該外掛同樣會輸出WebP格式的圖片
  • 壓縮生成好的圖片
  • 使用者只需要在MarkDown中編寫![Description of the image](image.jpg)即可
  • 我們自定義的MarkDown渲染引擎會在處理過程中自動使用picture元素替換這些img標籤

SVG Animation

我們的網站中也存在著很多的Icon以及動畫性質圖片,這裡我們是選擇SVG作為Icon與Animation的格式,主要考慮有下:

  • SVG是向量表示,往往比點陣圖檔案更小
  • SVG自帶響應式功效,能夠根據容器大小進行自動縮放,因此我們不需要再為了picture元素生成不同尺寸的圖片
  • 最重要的一點是我們可以使用CSS去改變其樣式或者新增動畫效果,關於這一點可以參考CodePen上的這個演示
    從案例分析如何優化前端效能

Custom Web Fonts

我們首先回顧下瀏覽器是如何使用自定義字型的,當瀏覽器識別到使用者在CSS中基於@font-size定義的字型時,會嘗試下載該字型檔案。而在下載的過程中,瀏覽器是不會展示該字型所屬的文字內容,最終導致了所謂的Flash of Invisible Text現象。現在很多的網站都存在這個問題,這也是導致使用者體驗差的一個重要原因,即會影響使用者最主要的內容瀏覽這一操作。而我們的優化點即在於首先將字型設定為預設字型,而後在自定義的Web Font下載完畢之後對標準字型再進行替換操作,並且重新渲染整個文字塊。而如果自定義的字型下載失敗,整個內容還是能保證基本的可讀性,不會對使用者體驗造成毀滅性的打擊。
2928541744-57bfe509d7def_articlex

首先,我們會為需要使用到的Web Fonts建立最小子集,即只將那些需要使用的字型提取出來,而並不需要讓使用者下載整個字型集,這裡推薦使用Font squirrel webfont generator。另外,我們還需要為字型的下載設定監視器,即保證能夠在字型下載完畢之後自動回撥,這裡我們使用的是fontfaceobserver,它會為頁面自動建立一個監視器,在偵測到所有的自定義Web Fonts下載完畢後,會為整個頁面新增預設的類名:

不過現在CSS的font-display屬性也原生提供了我們這種替換功能,更多詳情可見font-display屬性。

JS 與 CSS 的懶載入

總的來說我們希望所有的資源能夠儘可能快地載入完畢,不過往往為了保證首頁載入的速度,我們會考慮將部分非首屏需要的JS/CSS檔案進行延遲載入,或者對於重複的檢視使用瀏覽器本地快取。

Lazy Load JS

目前來說,我們的網站都是偏向於靜態,並不需要太多的JavaScript介入,不過考慮到日後的擴充套件空間,我們還是構建了一套完整的JS的工作流。眾所周知,如果將JS直接放置到head標籤中,其會阻塞整個頁面的渲染。對於該點,最簡單的方式就是將會阻塞渲染的JS指令碼移動到頁面的尾部,在整個首屏渲染完畢之後再進行載入。另一個常用的手段就是依然保持JS檔案位於head標籤中,不過為其新增一個defer的屬性,這保證了瀏覽器只會先將該指令碼下載下來,然後等到整個頁面載入完畢再執行該指令碼。
另一個需要注意的是,因為我們並不使用類似於jQuery這樣的第三方依賴庫,而更多的依賴於瀏覽器原生的特性,因此我們希望在合適的瀏覽器內載入合適版本的JS程式碼,其效果大概如下:

Lazy Load CSS

正如上文所述,我們的網站偏向於靜態展示,因此首屏的最大問題就是CSS檔案的載入問題。瀏覽器會在head標籤中宣告的所有CSS檔案下載完畢之前一直處於阻塞狀態,這種機制很是明智的,不然的話瀏覽器在載入多個CSS檔案的時候會進行重複的佈局與渲染,這更是對於效能的浪費。
為了避免非首屏的CSS檔案阻塞頁面渲染,我們使用loadCSS這個小的工具庫來進行非同步的CSS檔案載入,它會在CSS檔案載入完畢後執行回撥。不過,非同步載入CSS也會帶來一個新的問題,如果我們將所有的CSS全部設定為了非同步載入,那麼使用者會首先看到單純的HTML頁面,這也會給使用者不好的體驗。那麼我們就需要在非同步載入與首屏渲染之間找到一個平衡點,即首先載入那些必要的CSS檔案。
我們一般將首屏渲染中必要的CSS檔案成為Critical CSS,即關鍵的CSS檔案,代指在保證頁面的可讀性的前提下需要載入的最少的CSS檔案數目。Critical CSS的選定會是一個非常耗時的過程,特別是我們網站本身的CSS樣式設定也在不停變更,我們不可能完全依賴於人工去提取出關鍵的CSS檔案,這裡推薦Critical這個輔助工具能夠幫你自動提取壓縮Critical CSS。下圖的一個對比即是僅載入Critical CSS與載入全部CSS的區別:

4293272407-57bfe529b0ae0_articlex

上圖中紅色的線,即是所謂的摺疊分割點。

服務端與快取

高效能的前端離不開服務端的支援,在我們的實踐中也發現不同的服務端配置同樣會影響到前端的效能。目前我們主要使用Apache Web Server作為中介軟體,並且通過HTTPS來安全地傳遞內容。

Configuration

我們首先對於合適的服務端配置做了些調研,這裡推薦是使用H5BP Boilerplate Apache Configuration作為配置模板,它是個不錯的兼顧了效能與安全性的配置建議。同樣地它也提供了面向其他服務端環境的配置。我們對於大部分的HTML、CSS以及JavaScript都開啟了GZip壓縮選項,並且對於大部分的資源都設定了快取策略,詳見下文的File Level Caching章節。

HTTPS

使用HTTPS可以保證站點的安全性,但是也會影響到你網站的效能表現,效能損耗主要發生在建立SSL握手協議的時候,這會導致很多的延遲,不過我們同樣可以通過某些設定來進行優化。

  • 設定HTTP Strict Transport Security請求頭可以讓服務端告訴瀏覽器其只允許通過HTTPS進行互動,這就避免了瀏覽器從HTTP再重定向到HTTPS的時間消耗。
  • 設定TLS false start允許客戶端在第一輪TLS中就能夠立刻傳遞加密資料。握手協議餘下的操作,譬如確認沒有人進行中間人監聽可以同步進行,這一點也能節約部分時間。
  • 設定TLS Session Resumption,當瀏覽器與服務端曾經通過TLS進行過通訊,那麼瀏覽器會自動記錄下Session Identifier,當下次需要重新建立連線的時候,其可以複用該Identifier,從而解決了一輪的時間。

這裡推薦擴充套件閱讀下Mythbusting HTTPS: Squashing security’s urban legends by Emily Stark

Cookies

我們並沒有使用某個服務端框架,而是直接使用了靜態的Apache Web Server,不過Apache Web Server也是能夠讀取Cookie並且進行些簡單的操作。譬如在下面這個例子中我們將CSS快取資訊存放在了Cookie中,然後交付Apache進行判斷是否需要重複載入CSS檔案:

這裡Apache Server中的邏輯控制程式碼就是有點類似於註釋形式的<!-- #,其主要包含以下步驟:

  • $HTTP_COOKIE!=/css-loaded/ 檢測是否有設定過CSS快取相關的Cookie
  • $HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css'檢測快取的CSS版本是否為當前版本
  • If <!-- #if expr="..." --> 值為true 我們便能假設該使用者是第一次訪問該站點
  • 如果使用者是首次瀏覽,我們新增了一個<noscript>標籤,裡面還包含了一個阻塞型的<link rel="stylesheet">標籤。新增該標籤的意義在於我們在下面是使用JavaScript來非同步載入CSS檔案,而在使用者禁止JavaScript的情況下也能保證可以通過該標籤來正常載入CSS檔案。
  • <!-- #else --> 表示式在使用者二次訪問該頁面時,我們可以認為CSS檔案已經被載入過了,因此可以直接從本地快取中載入而不需要重複請求。

上述策略同樣可以應用於Web Fonts的載入,最終的Cookie如下所示:
3888680170-57bfe53b2a5e6_articlex

File Level Caching

在上文可以發現,我們嚴重依賴於瀏覽器快取來處理使用者重複訪問時資源載入的問題,理想情況下我們肯定希望能夠永久地快取CSS、JS、Fonts以及圖片檔案,然後在某個檔案發生變化的時候將快取設定為失效。這裡我們設定了以https://www.voorhoede.nl/assets/css/main.css?v=1.0.4形式,即在請求路徑上加上版本號的方式進行快取。不過這種方式的缺陷在於如果我們更換了資原始檔的存放地址,那麼所有的快取也就自然失效了。這裡我們使用了gulp-rev以及gulp-rev-replace來為檔案新增Hash值,從而保證了僅當檔案內容發生變化的時候檔案請求路徑才會發生改變,即將每個檔案的快取驗證獨立開來。

Result

上面我們介紹了很多的優化手段,這裡我們以實驗的形式來對優化的結果與效果進行分析。我們可以用類似於PageSpeed Insights或者WebPagetest來進行效能測試或者網路分析。我覺得最好的測試你站點渲染效能的方式就是在限流的情況下觀察頁面的呈現效果,Google Chrome內建了限流的功能:
4159997457-57bfe53fd55a2_articlex
這裡我們將我們的網路環境設定為了50KB/S的GPRS網路環境,我們總共花費了2.27秒完成了首屏渲染。上圖中黃線左側的時間即指明瞭從HTML檔案開始下載到下載完成所耗費的時間,該HTML檔案中已經包含了關鍵的CSS程式碼,因此整個頁面已經保證了基本的可用性與可互動型。而剩下的比較大的資源都會進行延時載入,這正是我們想要達到的目標。我們也可以使用PageSpeed來測試下網站的效能,可以看出我們得分很不錯:
pagespeed-insights-voorhoede
而在WebPagetest中,我們看出瞭如下的結果:
426616130-57bfe551ba828_articlex

Roadmap

優化之路漫漫,永無止境,我們在未來也會關注以下幾個方面:

  • HTTP/2:我們目前已經開始嘗試使用HTTP/2,而本篇文章中提到的很多的優化的要點都是面向HTTP/1.1的。簡言之,HTTP/1.1誕生之初還是處於Table佈局與行內樣式流行的時代,它並沒有考慮到現在所面對的2.6MB大小,包含200多個網路請求的頁面。為了彌合這老的協議的缺陷,我們不得不連線JS與CSS檔案、使用行內樣式、對於小圖片使用Data URL等等。這些操作都是為了節約請求次數,而HTTP/2中允許在同一個TCP請求中進行多個併發的請求,這樣就會允許我們不需要再去進行大量的檔案合併操作。
  • Service Workers:這是現代瀏覽器提供的後臺工作執行緒,可以允許我們為網站新增譬如離線支援、推送訊息、後臺同步等等很多複雜的操作。
  • CDN:目前我們是自己維護網站,而在真實的應用場景下可以考慮使用CDN服務來減少服務端與客戶端之間的物理距離,從而減少傳輸時延。

相關文章