我理解的前端性能 & 優化

這篇文章會先對筆者理解的 web 性能做一個小小的階段性總結,也就是「初入」的含義,然後會分享一些個人的理解和感想。由於是個人的感受,雖然經過一定的整理,但是還是有很多想到哪說到哪,忘記說以及不如意的地方,精力與實力都有限,錯誤之處還望多多包涵並指出,非常感謝。

首先,筆者認為,開始探索一件事的時候,不說深入,但至少了解其歷史或者發展的進程是非常非常有必要的,我們經常聽到的「前朝的劍不能斬今朝的官」,「特色xxxx」。也具有相似的意味或者說道理。

那我們可以「大膽」的推測(即不經過 wiki 的追溯)一下,就像 template -> ajax -> jquery -> node -> 工程化 -> spa -> ... 一樣。在最開始的 web 開發中,應該是不用也不會考慮到性能的問題,一個服務端「直出」的 html,也許連 script 都沒有,人們能看到新聞與圖片也許就非常開心了,就像當初用「珊瑚蟲」一樣,能進行「你好」,「你是 gg 還是 mm」這樣簡單直白的對話就很滿足了。

我們會發現,很多時候我們繞了一大圈又繞「回來」了,以前 js 和 html 寫在一起,html 與 css 寫在一起,於是出現了「分離」,html 與 css 要分離,js 與 html 也要分離。以前前端很少,甚至沒有前端的時候,「前端」的活基本交由服務端完成,但後面我們又提出了「前後端分離」,提出了 css in js。

https://twitter.com/awbjs/status/963843212262621184

拋開人為和「政治」的影響,發生這種「繞圈」變化的原因,不是大家吃飽了沒事幹,僅僅只是歷史的選擇而已。不是大家探索錯了方向,回到了原地,而是原地已變成了巨人的肩膀。

瞎扯結束:)

本篇主要會分三個部分來講這個問題:

  • 性能是什麼
  • 如何測量性能
  • 性能如何提升

如無特殊說明,以下探索都是以 chrome 為準的。

性能是什麼

在個人看來,這個問題也可以理解為,web 性能表現在哪些方面,或者說存在哪些維度?

對於 web 來講,我們一般會從兩個維度去考慮這個問題,一是頁面層面,二是運行層面。

  1. 頁面層面:頁面的載入到底經歷了哪些過程(又回到了這個萌萌的話題,看到有人說他面試時對面的人滔滔不絕背了 10 多分鐘哈哈),頁面載入之後又有哪些過程,除了這些之外,還有哪些地方是我們需要關注的。
  2. 運行層面:這裡的運行一般指的是動態可觀測到部分,如各種動畫(css 動畫,js 動畫,svg 動畫,canvas 動畫),媒體資源播放等等。可能有人會說,運行一般不是說「流暢」嗎,為什麼也用「快」來衡量呢。因為不流暢其實就是卡頓,都卡頓了,自然也就算不算快了。

頁面層面

對於頁面以及頁面內包含的資源來說,最完整的請求載入過程如下圖所示:

network waterfall

除此之外,我們可能還會關心的是,我的頁面到底是什麼時間被渲染出來的(即常說的首屏渲染),什麼時候才能真正開始響應用戶交互等等。其實我們可以想想,很多指標作為開發人員是完全沒辦法憑自己的能力獲取到的,我們最強大的武器也不過是 javascript,可是我們要怎麼通過 js 去得到這些指標呢,是沒有辦法得到的,即便能夠獲取,不同瀏覽器的實現機制是不一樣的,我們獲取的是否就是我們真的想要的,也很難說。

所以在很早的時候,就成立了 w3c 性能工作小組 ,專門來負責制定相應的指標規範,規範化了各個指標代表的意義,把這些指標的收集工作交給瀏覽器,由瀏覽器來暴露,我們只需要簡單的 get 一下就行了。當然,瀏覽器與 developer 是一個根與葉的關係,葉子也需要主動和瀏覽器交流,葉子不說自己喜歡梁朝偉,大樹還以為你喜歡凡凡呢

如何測量性能

如何測量的問題,上圖已經有一個比較好的概括啦。作為 developer,用 DevTools 來分析,用 WebPerf APIS 來上報和做參考是比較合理的方式。

頁面層面

在這一個維度上,其實已經有很多現有的工具,如 pagespeed,lighthouse,webpagetest 等等,那麼我們為什麼還要自己去分析呢,一方面,他們給出的指標有限,另外 checklist 很多,如果對整個涉及到的內容不太熟悉的話,容易從「找問題」變成「學知識」,然後演變成拿鎚子找釘子的感覺。

另一方面,它們只能在前提條件確定甚至是無法改變的情況下進行測試,如 ios 9, fast 3G 等等,而我們需要的是各個用戶的實際體驗,顯然這些工具是無法滿足的,這也是為什麼它們被推薦用在 CI 上以及某些 excel 表(結尾的更多鏈接中)有一列是人工測量的原因了。

web perf 規範集合中的 Resource Hints 的一部分 Navigation Timing (我們暫時以版本 1 為準)介紹了絕大部分我們關心的與頁面有關的指標:

當然,我們真正需要的指標也許並不是規範中的原始數據,可能需要自己進行一些如 myPageDNSLookUpDuration = performance.timing.domainlookupEnd - performance.timing.domainlookupStart 的換算操作。這裡有幾個小 Tip:

  • loadEventEnd 在 onload 的時候依舊為 0,需要自己稍微處理下。
  • Navigation Timing 中的 navigation-types 在規範中也說明了,我們這個的值呢各個瀏覽器可能不一樣。所以當發現和規範不一致時不要害怕,查查對應瀏覽器的代碼就行了,比如 chrome 的: cs.chromium.org/chromiu
  • 對於 initiatorType,Resource Timing Level 2 規範裡面寫的有 fetch 和 xmlhttprequest 兩種,但是 chrome 暫時只有 xmlhttprequest,所以即便是 fetch,initiatorType 也是 。具體的情況可以參考 cs.chromium.org/chromiu

出於兼容性的考慮,在不支持 navigation-timing 的瀏覽器里,我們可以 fallback 到 chrome.loadtimes(雖然只支持 chrome,也沒有完全覆蓋規範里的所有指標,chrome 64 後此 api 也將被廢棄,但是你想想國內瀏覽器都用到 chrome 64 的內核了,還在干不幹這一行都不一定了!)

除了這些指標之外,我們經常看到的各種優化手段,其中很大一部分都是為了提升「首屏」渲染的速度,所以,首屏到底是什麼時間被渲染的,就成為了一個我們想知道的非常重要的指標。規範當然也考慮到了這一點,w3c/paint-timing 就包含了 FP(First Paint), FCP(Frist ContentfulPaint)。至於 FMP(First MeaningfulPaint) 和 FI(First interactive),目前還沒有在規範之中,並且近期沒有計劃納入規範(以後應該會有)。

值得一提的是,雖然在規範中對這些指標的準確含義以及代表的具體時間點做了闡述,但和大多數規範誕生的過程一樣,討論的過程是極其漫長的(當然也包括提案的動機,背景調研,如何實現,為什麼要搞幾個指標,不像 chrome devtools performance panel 一樣就搞一個 FP 就行了等等)。有興趣的朋友可以閱讀 github.com/w3c/charter- 以及 docs.google.com/documen

那 lighthouse 是怎麼獲取到這些參數的呢,是因為 lighthouse 對頁面進行了錄屏操作,通過分析每一幀的變化,推斷出各個指標的時間點。而作為一種通用的準備用於各個用戶端的方案來講,我們顯然無法做到這一點。

資源層面

通過 resource timing api 就可以獲取每個資源(包括 api 請求)的相關指標啦。但是瀏覽器不支持的話就沒辦法了,不過對於 hero image 而言的話,有這麼幾個方式可以作為備用方案考慮,它們都出自這篇文章:

  • image onload handler
  • mutation observer
  • polling for offsetHeight
  • inline script timer

最後的結論是,只有 image onload handler 的時間是準確的。

上報性能

知道怎麼測量性能之後,我們需要做的是把收集到的原始指標上報到服務端,然後進行統計分析,最後通過可視化的方式呈現出來。無論是通過 grafana 還是自己搭建平台都可以。

由於我們做的具備通用性,所以理想的方式是寫一個 js,需要上報的頁面引入這個 js 即可,需要注意的無非是注意下兼容性的考慮,不要拋出錯就好,還有就是引入的時候放在底部加上 async attribute,盡量避免對現有其它模塊的影響

性能如何提升

在筆者看來,我們最常用的解決「大」問題的方式無非是兩種:分治和緩存。我們也可以用類似的思維逆向去思考性能問題,即分別從局部和整體去思考。

局部來講,頁面由一個一個的資源組合而成,包括我們最初請求的 html。我們可以依次來看下前面列出的時間消耗過程(如果不熟悉它們的含義的話可以查看官方文檔):

  1. Queueing
  2. Stalled
  3. DNS lookup
  4. initial connection
  5. SSL
  6. Request sent
  7. TTFB
  8. Content Download

那麼怎麼樣才算性能「好」呢?個人認為就一個字:「快」。我們希望的結果無非就是一個,希望上面的每一段時間都儘可能的短,儘可能的意思是,一方面受限於物理客觀條件,如網速(雖然當網速達到一定級別後,最大影響因素轉變為了延遲而非網速),另一方面,不同過程之間,資源之間會互相影響。

對於 1:Queueing 和 2:Stalled 的優化,牽扯到整個頁面的調度,我們後面再談。對於 3:DNS lookup,4:initial connection,5:SSL 而言,我們可以思考一下,想讓他儘可能短,甚至「不消耗」時間,有哪些可能的辦法?

試想一下這樣一個場景,過年回家的你看上了隔壁村的張冰冰(static.foo.com),想要她的微信號(ip),於是來到了村頭的「微信查詢中心」(user agent),告訴工作人員(browser),麻煩你幫我查下冰冰的微信號。

好勒,稍等一下,工作人員看了看桌上的「常用漂亮妹紙微信列表」(browser DNS cache/list),沒有找到。問了問旁邊的老五(local computer DNS resolver),我這找不到,你幫這位同志查查。老五表示,俺這也沒找到,只能找負責查詢工作的總部幫忙查了。你有點不耐煩了,一開始就讓總部(root DNS server)幫忙查不行嗎,老五聽到了你的抱怨,說到:小夥子,這也不怪我們呀,讓總部給你查可比我們給你查費時多啦,總部還得先打電話給專門負責查名的部門(.com, .cn, .gov .... DNS server),然後再打給專門查姓的部門(foo.com DNS server),你要是複姓,那就還得打呢(static.foo.com),可麻煩了。唉,主要還是你要找的張冰冰同志呀,我們以前都沒聽過,哪怕是村東頭的二狗來查過一次,我們也就有記錄了嘛,你來查就快多啦。

以上就是常見的遞歸查詢,當然,除此之外還有非遞歸查詢等等。是不是有點像分治加緩存呢?

了解了整個過程後,要優化就很簡單了,一方面,我們知道了從瀏覽器緩存裡面之間讀取是最快的,這一部分瀏覽器已經幫我們做了,你可以通過 chrome://dns 進行查看,而且瀏覽器做的不僅與此,它還會分析一個頁面下面的請求的子資源域名,有什麼用呢,後面我們就知道啦。

另一方面,頁面的不同子資源通常分布在多個 CDN 上,要等到 parse 到對應的標籤再去進行 DNS 查找有點浪費啦,可以在一開始的時候多個 DNS 一起解析嘛。那麼就有一個問題了,瀏覽器怎麼知道各個網站里有哪些 DNS 需要解析呢?

瀏覽器最開始肯定是不知道的,但是載入過一次後,就有記錄了,那萬一後面資源域名變了呢(雖然可能性不大),那就只有猜咯。當然,瀏覽器還是很嚴謹的,不能瞎猜,他的理論依據就是上一段提到的 chrome://dns 里的記錄來滴

除了靠瀏覽器自己去猜測外,更好的方式是 developer 主動告訴瀏覽器本頁面有哪些資源域名,怎麼告訴呢,通過 <link ref="dns-prefetch" href="static.my.com"> 聲明一下就行了,如果你的是 https 的話,你可能還希望不僅僅進行 dns 解析,還希望把 SSL 握手之類的也做了,規範當然也考慮到了,你可以通過 <link ref="preconnect" href="static.my.com"> 來實現:

twitter 也是這樣做滴:

為什麼 dns-prefetch 和 preconnect 都要用呢,那是因為後者的兼容性不是很好

它們都隸屬於前面提到的 web perf 規範集合中的 Resource Hints 的一部分,另外我們可以通過 https://www.chromestatus.com/metrics/feature/popularity 來查看目前它們整體上被使用的情況。

對於 6:Request sent 而言,消耗的時間基本在納秒級別,所以可以忽略,對於 7:TTFB 來說,有兩種思路,一是如果有本地緩存的話,那就用不著發請求啦,TTFB 就直接沒有啦。所以我們需要合理的設置緩存,控制上線頻率。二是如果沒有本地緩存(第一次訪問,緩存過期,緩存被清了等等)的話,個人能想到的除了一個好的 CDN 服務以及分塊編碼,應該沒有太好的辦法?(歡迎補充)

對於 7:Content Download 來說,拋開用戶網速的客觀因素外,我們能做的,就是盡量盡量再盡量壓縮資源的體積。再介紹常用的一些方法之前,最重要的步驟是我們應該通過類似 webpack-bundle-analyzer 這樣的工具分析出需要改進的地方,再進行優化:

  • 再經過了 UglifyJsPlugin 壓縮後,除了常用的 gzip 壓縮外,比較出名的還有 google 最近提出的 brotli ,另外還有幾種(貌似已經涼了?)
  • 對於 React 項目,生產環境通過 babel-plugin-transform-react-remove-prop-types 移除所有的 propTypes
  • 小 tip:UglifyJsPlugin 有個 --screw-ie 參數,設置為 false 的時候能夠減小一點點體積(這個參數從默認值到參數名變動得比較大,有興趣可以去看看)

除了以上最直觀也最傳統的做法外,在最近幾年前端工程化的浪潮下,人們也開始從另外的角度去思考這個問題:

  • js 按需打包(載入),code spliting 等等。webpack 從最初的 require.ensure,到 System.import,再到原生的 dynamic import。當然也有比較成熟的庫如 jamiebuilds/react-loadable ,faceyspacey/react-universal-component 等等
  • 之前有想過 css 是不是也需要,除了路由外,如夜間模式,android/ios 特殊適配,媒體查詢等等都可以進行提取。後面發現,css 的體積和 js 相比還是太小了,儘管理論上這麼做肯定會好一點,但是收益確實不大
  • 在現代網站中,圖片在整個網站的體積中佔據了絕大部分,所以每隔一段時間就能看到什麼什麼公司又研究了一種新的圖片壓縮演算法。咳咳,扯遠了,除了常規壓縮外,webp 和 progressive image(雖然個人不太能接受)也都已經被「廣泛」應用。此外,針對 響應式圖片 的方案也已比較成熟

以上就是局部優化的部分。

而對於整體而言,我們一般通過 network waterfall 以及 performance timeline,來分析我們的頁面整體,「整體」是什麼意思呢?其實隨處都有類似的影子,小到一個插件,庫的設計思路,再大到 webpack 這樣的一體化工具的設計思路,再大點到 react,瀏覽器,操作系統 的調度演算法等等等等。即一種「統籌規劃」的理念。

前面已經提到,只有開發者和瀏覽器之間相互交流,才能達到真正的「和諧」。所以,我們需要先了解下瀏覽器能做或者說已經做了哪些工作,而哪些是需要我們做的。就像辯證的看待「永遠不要覺得你比編譯器更聰明」一樣。

這裡有一篇很好的文章講述了其中一部分,推薦閱讀,下面幾段的內容部分來自這裡。

早些時候,瀏覽器和我們所認為的那樣,請求 html,然後逐行解析,構建 DOM 樹,CSSOM 樹,Render 樹。

後來發現,用戶在地址欄輸入網址的時候,就可以提前做一些工作了,因為這些輸入往往是有規律的,比如我輸入 z 訪問 zhihu 的可能性很大,輸入 gi 訪問 github 的可能性很大(希望沒人問我為什麼不加入書籤。。)。

所以瀏覽器通過 chrome://predictors 記錄了兩部分東西,一是在地址欄每輸入一個字元和用戶最終訪問的地址之間的可能性對應關係,hit 率,miss 率等等,就像記錄緩存命中一樣。二是記錄每個打開過的網站里會請求的資源的可能性對應關係(如果打開了 #speculative-resource-prefetching 的開關的話)。藉助這些信息,在等待 first byte 的時候,瀏覽器也可以預請求某些資源。

同時瀏覽器還發現,必須等到 html 中 parse 到資源對應的那一行才去請求也有點太慢了,先粗略 scan 一遍獲取資源的 type 和 url,早一點請求該多好。於是有了 preloader。

瀏覽器的 preloader 需要注意的地方:

  • 一是如果網站的關鍵圖片是由 js 動態加入的,那麼可能有性能問題,因為瀏覽器會先去 scan img 標籤的圖片並加入隊列,然後才把 js 添加請求的圖片加到隊列裡面,所以 img 的圖片會拖累 js 里添加的關鍵圖片的請求
  • 二是 script 放到 body 底部,但是由於 preloader 的存在,且 script 本身在請求中的優先順序就比較高,所以這些 script 可能會被先建立 tcp 連接甚至請求之類的,這就會拖慢圖片的 render(筆住:貌似 chrome 不存在這樣的問題,首屏的圖片一定會先於 script 加到隊列並請求,除了對 script 用了 preload)

然後瀏覽器就會統籌安排我們網頁中的所有資源,但是網站是千奇百怪的,有的重展示,有的重內容,有的只是一些特效,而有的可能只有簡單的首屏大圖配文字。在不同的網站中,我們對資源的優先順序排序也會發生變化,所以我們需要一種能夠改變瀏覽器默認的優先順序策略的能力。

於是有了 Resource Hints 以及 Preload 規範。提供了兩種能力,一是讓我們提前請求某些自己覺得隨後很可能被用到的資源,二是讓我們能夠把自己認為自己網站較為重要的資源告訴瀏覽器,而不是讓瀏覽器自己根據一套規則去決定。使用方式都很簡單,這裡就不過多介紹了。有幾個點需要說明一下的是:

  • prerender 看起來是個不錯的東西,雖然也只有 chrome 支持過一段時間,但是之前也已經被廢棄了,原因之前查了下,不記得了。。不過替代方案也有,還沒來得及看,爭取之後看了再補充。另外就是 prerender 可能導致頁面埋點被意外觸發,此時需要藉助 Page Visibility API 來處理。
  • 還有兩個小 tip:一是使用的時候可以設置 crossorigin="anonymous" 減小不必要的請求頭大小,當然如果資源是需要許可權的那肯定就不要加了。二是 preload 了一張圖片,這張圖片是用於 console.log 裡面的,會有 warning:you dont use the preloaded resource,簡而言之,preload 對 console.log 是無感的,具體原因見這裡。
  • 我們之前說的 1:Queueing 和 2:Stalled 的優化會牽扯到整個頁面的調度,也是在這裡需要注意的地方。因為瀏覽器對於一個域的同時建立的 tcp 鏈接數是有限制的,要麼通過 iframe 繞過(雖然也有缺點),要麼上 http2。

此外,如果不支持 preload 的話,一般的資源文件好像沒什麼好的方式。不過對於圖片的話,有人提出了幾種備選方案:

  • 在 head 里的 style 設置 html{ background: url(/hero.jpg) no-repeat -9999px -9999px; }。不過這是錯誤的,反而時間是最慢的或者說優先順序是最低的,因為背景圖是在所有的 css(包括 style attribute)被下載解析執行後才會去請求的,因為後面的 css 可能會覆蓋這個 background
  • 在 head 里的 js 裡面通過 new Image().src = //hero.jpg 動態引入。這個也是錯誤的,因為這個 image 沒有被展示,請求的優先順序也是非常低的。
  • 在 body 里的最靠前的位置用一個 img 標籤用去請求 hero image,測試連接: stevesouders.com/tests/,這個是唯一的也是最好的替代方案,當然如果是背景圖的話,可以採取 Prioritize loading of background images 里提到的包裹一個 display:none 的 img 標籤的方案。

用 img 標籤來使圖片的優先順序變高的原理是他們能被瀏覽器的 preloader 識別,而如果用 css 哪怕是 inline style,也必須等到 html parser 去解析才行,而 html parser 還有可能被阻塞。

還有一點需要注意的是,Hero Image Custom Metrics 里提到(筆者還未測試),如果圖片在 html 中最後一個腳本下載完成之前下載完,那麼這個圖片會被 render,如果沒有在最後一個腳本下載完成前下載完,那麼這個圖片的 render 會被推遲(個人認為是因為瀏覽器覺得腳本的執行的優先順序應該更高),即阻塞那些圖片的 render。

以上這些思想其實也潛移默化的具有通用性,如 LazyLoad 一般會有 offset 參數,帶給我們的啟發是 carasoul 也可以類似這樣處理,而不是一下塞進所有圖片。無限列表也不一定非要下拉載入的時候才去載入。

運行層面

對於動畫性能,這裡就不多談了(主要是因為確實不是很了解。。),除了知道一些常見的動畫方案和注意事項外,沒有深入探索過。

最後,升級 node 和 react 這種就不談啦,多多內測下,不要把線上搞掛就行哈哈

寫在最後

由於筆者本身水平很有限(菜),也才幾周前才開始稍微深入的看相關的東西,而且還有一大堆在書籤里沒看,加上又是過年,咳咳,總之錯誤之處還望包涵以及批評指出,新年快樂!

參考/更多鏈接

keycdn.com/blog/resourc

Improving Search Rank by Optimizing Your Time to First Byte

How to Reduce TTFB to Improve WordPress Page Load Times

medium.com/@luisvieira_

responsivedesign.is/art

developers.google.com/w

developers.google.com/w

evemilano.com/wp-conten

垂直同步是幹嘛用的?究竟要不要打開?

docs.google.com/documen

developers.google.com/w

docs.google.com/spreads

html5rocks.com/en/tutor

docs.google.com/spreads

chromium.org/developers

youtube.com/watch?

medium.com/@addyosmani/

docs.google.com/documen

speakerdeck.com/addyosm

User Timing and Custom Metrics

velocity.oreilly.com.cn

w3.org/2012/11/webperf-

developers.google.com/w

assets.en.oreilly.com/1


推薦閱讀:

TAG:前端開發 | 前端工程師 | Web開發 |