今晚,我想來點 Web 前端效能優化大補帖!

莫力全 Kyle Mo
Starbugs Weekly 星巴哥技術專欄
29 min readNov 10, 2020

效能是工程師在維護專案時非常重視的要點,不論是 Web 還是 App,甚至是需要大量運算資源的機器學習,都會想追求極致的效能,用高效率換取高價值。不過首先在文章的最開頭想給讀者灌輸一個 mindset:「就 Web Client Side 而言(其他領域我還不夠了解因此先不討論),並不是所有的應用都需要追求效能,有時候獲取效能的背後也許需要花上昂貴的成本,比較起來是得不償失的,因此在進行效能調校前應該先好好衡量進行優化的成本(時間、困難度都需要考慮),有時候先求有再求好反而是較佳的方案。另外一件事就是效能優化沒有所謂的終點,這是一段追求「快還要更快」的過程。」

Web Client Side Performance Optimization

提到 web 應用的效能優化,很多人第一時間會映入腦海裡的可能會是例如 Load Balancing 或是 Caching 等分流或減少網路請求的方式,然而這些針對 Server Side 的效能優化並不是本篇文章的重點,本篇文章將「概述」各種 Web Client Side 效能優化的技巧,標題即點明大補帖,所以會比較像學生時期段考的重點複習一樣,主要闡述各技巧的原理與解決了什麼問題,不會詳細說明實作方式,希望能夠讓讀者對於前端效能優化有個較寬廣的理解與方向。不過會在每個技巧的段落提供一些參考連結讓有興趣更深入研究的讀者自行閱讀。

內容大綱

為什麼需要在前端做效能優化?

Performance Analyzers:LightHouse, PageSpeed

Core Web Vital

Code Uglify

Image Minimize、了解 jpg、png、 svg 的使用時機

Critical Render Path 關鍵渲染路徑

Code Splitting

Dynamic Import

Lazy Load

Webpack Bundle Analyzer

Virtualized List

Tree Shaking

Preload、Prefetch and others in <link> tag

CDN & Cache

Write Good Code

為什麼需要在前端做效能優化?

關於這個問題我認為最主要有三個原因:

  • SEO
  • 使用者體驗
  • 影響營收的重要指標

據說現在網頁的效能也會影響網站的 SEO Ranking 了,不過「使用者體驗」我想才是這個問題最重要的答案,前端主要就是面向使用者,如何提供一個流暢且迅速的使用者體驗是好的 Web 工程師需要去考量的,而效能則是讓使用者最有感的指標,試想如果網站再精緻,提供的服務再豐富,但如果需要非常長的載入時間,一般的使用者都會受不了而直接跳出頁面吧,而這也帶出第三個要點「影響營收的重要指標」,根據美國雲端計算公司 Akamai 的研究

當網站沒有在三秒內顯示完畢,40% 的消費者會選擇直接跳離網站;在網站速度與營收的關係研究上,數據也表示,只要網站速度每提升 100 毫秒,營收就能增加 1%。

這樣讀者們就了解網站效能的重要性了吧,畢竟統計數據會說話呀😂

Performance Analyzers

在進行效能優化以前,我們得先找出效能瓶頸出自哪裡,這時候我們可以使用一些 performance analyzer 來幫助我們找到問題,其中較有名的是 Lighthouse 與 PageSpeed。

PageSpeed 的話打開瀏覽器 Devtool 就可以看到

Lighthouse 則是我比較推薦的工具,我認為介面與提供的資訊都相對較完整,要使用 Lighthouse 則是需要先安裝 Lighthouse chrome extension

Lighthouse 不僅會給出各項指標的分數,也會統計出渲染頁面時各項工作所花費的時間,甚至會列出建議的修改方式,除了協助我們定義出問題,也協助我們對優化方式有個參考的方向。

此外 Lighthouse 更提供 lighthouse-ci 的整合服務,讓專案 CI 可以與 Lighthouse 結合,每一次的 commit 都可以給出一份報表,當效能出現重大變動時就可以輕易追蹤出是哪一個 commit 造成的問題。

圖片來源:lighthouse-ci github

Core Web Vital

圖片來源

Google 根據長期以來大量的使用者體驗制定出了 Core Web Vital 的指標,Google 更指出若 75% 以上的使用者在網站中的瀏覽體驗都能夠通過以上 3 種指標,就能夠大幅的提升使用者的搜尋體驗,甚至能夠讓原本因等待而離開的使用者減少 24%!

LCP (Largest Contentful Paint ) — 顯示最大內容元素所需時間 (速度)

LCP 是計算網頁可視區 (viewport) 中最大元件的載入時間,也就是頁面的主要內容被使用者看到的時間,是速度的指標。

不過可視區內最大的元素並不是固定不變的

圖片來源

上圖的頁面在載入時一開始可視區的最大元素是左上角的文字,接下來隨著頁面載入變成了標題,最後變成了圖片,因為圖片是可視區最大的元素了,因此 LCP 就會以該圖片所需要載入的時間做計算。

如何優化 LCP

減少伺服器回應時間

  • 針對主機效能優化
  • 使用較近的 CDN
  • Cache
  • 提早載入第三方資源

盡量避免 Blocking Time

  • 降低 JavaScript blocking time
  • 降低 CSS blocking time

加快資源載入的時間

  • 圖片大小優化
  • 預先載入重要資源
  • 將文字檔案進行壓縮
  • 根據使用者的網路狀態提供不同的內容
  • 使用 service worker

避免使用客戶端渲染(CSR)

  • 若必須使用 CSR ,建議優化 JavaScript ,避免渲染時使用太多資源
  • 盡量在伺服器端完成頁面渲染,讓用戶端取得已渲染好的內容

FID — First Input Delay 首次輸入延遲/封鎖時間總計 (互動性)

輸入延遲 (Input Delay) 通常發生於瀏覽器的主執行序過度繁忙,而導致頁面內容無法正確地與使用者進行互動。舉例來說,可能瀏覽器正在載入一支相當肥大的 JavaScript 檔案,導致其他元素不能被載入而延遲可互動的時間。

如何優化 FID

  • 減少 JavaScript 運作的時間
  • 降低網站的 request 數並降低檔案大小
  • 減少主執行序的工作
  • 降低第三方程式碼的影響

CLS — Cumulative Layout Shift 累計版面配置轉移 (穩定性)

有沒有遇過一種情況是當你在網頁中準備點擊一個按鈕或連結的瞬間,突然一個廣告被插入,讓你不小心點開別的網站,恨的牙癢癢的呢?這就是 CLS 這個指標想要避免的使用者體驗,如果不清楚以上情境的可以參考這裏

如何避免 CLS

給予會比較慢載入的元素一個預設的寬度與高度

Code Minimize & Uglify

有時候你會想看看一些網頁的原始碼是怎麼運作的,不過當點選「檢查網頁原始碼」後,顯示出來的 code 有時卻讓你不知道這到底是哪個星球的程式語言,例如檢查臉書原始碼會看到

臉書頁面檢查網頁原始碼

不過其實這些看起來混亂的代碼其實就是我們寫出來的程式,雖然變數名稱跟邏輯似乎都跟我們原本開發時寫的不一樣,但它其實只是經過轉譯罷了。而這麼做主要的原因有兩個:

  • 變數跟 code 寫的越短,或是刪除不必要的空白,可以省掉不少瀏覽器 Parse 的時間,也就是提升前端程式的效能 — Minimize
  • 通常會打亂程式的邏輯,避免自家產品的 code 輕鬆的被別人拿去研究或抄襲 — Uglify

如果要試試看效果,可以參考如 JavaScript MinifierUglify JS 等網頁服務,但通常在開發時我們不會笨拙的手動貼 code 去 Minimize 或 Uglify,而是會利用如 webpack 、gulp 等打包工具替我們做這些事情。

Image Minimize & 理解 jpg、png、 svg 的使用時機

現今的網站免不了會需要載入大量的圖片,圖片也因此成為網站載入資源的很大一部分,換句話說就是對網站效能有著直接的影響。在考慮 Image Lazy Load 等技巧以前,我們可以先將圖片壓縮,透過減少檔案大小來加快載入時間,而壓縮又分為兩種狀況:

  • 有損壓縮:如 JPG,使用只取部分像素資料的方式來壓縮圖片大小,並且壓縮後是不可逆的。
  • 無損壓縮:如 PNG,壓縮後不影響圖片品質。

三種圖片類型各自比較有名的圖片壓縮服務有 tiny-pngsvgomgjpeg-optimizer

而其實不同的圖片類型也有各自適合使用的時機,學會將不同類型圖片應用在適合的地方不僅可以提升使用者體驗或 UI 品質,某些狀況下也可以控制載入資源的大小而提升一點效能。這裡推薦一篇文章,說明得十分完整,除了有介紹各種圖片類型的適當使用時機外,也有介紹如響應式圖片、webp、Image CDN 等其他圖片優化技巧,非常推薦一讀。

Critical Render Path 關鍵渲染路徑

提到網頁前端的效能最佳化,我們得先了解網頁是如何渲染到頁面上的,從收到 HTML、CSS 和 JavaScript,再對程式碼進行必需的處理,到最後轉變為顯示像素的過程中還有許多中間步驟。將效能最佳化其實就是瞭解這些步驟中所有的活動,再經過最佳化,這就是所謂的關鍵渲染路徑 Critical Render Path 。

根據上圖,我們可以大致理解出網頁渲染的流程為:

  • 讀取 HTML 後生成 DOM Tree
  • 讀取 HTML 中的 CSS Link Tag 生成 CSSOM Tree
  • DOM Tree 與 CSSOM Tree 共同生成 Render Tree
  • 根據 Render Tree 生成 Layout
  • 最後 Paint 畫面

當然,現今的 web app 不太可能只靠 HTML 跟 CSS 就完成,還是得靠 JavaScript 來修改網頁的內容、樣式、與使用者互動的行為。JavaScript 可以查詢及修改 DOM 和 CSSOM,在 CSSOM 執行完畢後,JavaScript 才會執行 。這邊給一個小 tip:如果可以的話,CSS file 盡快引入,JS 在 CSS 後引入,因為 JS的執行會導致網頁載入的暫停(不過有例外的非同步功能,很快就會講到了)。

一般我們要載入 JavaScript 檔案,通常會透過 <script> 這個 Tag 來達成,不過它的執行是同步的,也就是會導致網頁載入的暫停,如下圖:

normal script

但其實 script tag 的引入還有 async 跟 defer 這兩種方式:

async 會非同步去請求外部腳本,回應後停止解析執行腳本內容。

defer 也會非同步請求外部腳本,但是等待瀏覽器解析完才執行。

<script async> 用於載入第三方函式庫等不需要動到 DOM 結構的狀況

async script

<script defer > 要整個頁面都下載及分析完成後才會執行,非常類似於把 JS 放在頁尾的情況

因此在適當的時機選用不同的載入方式,是有機會提升網頁的效能的,對於 Critical Render Path 或資源載入方式有興趣的讀者,可以更進一步閱讀 google developer 的文章 MDN script tag 的 document

Code Splitting

Code Splitting 是一個非常重要的觀念,現代網頁程式漸漸走向使用框架以模組化方式來開發,即便會透過如 webpack 等 bundler 來 uglify、minimize、打包程式碼,當專案成長到一定程度時,程式 bundle size 仍然會變得過於肥大,導致 client side 的網頁載入時間變長,嚴重影響使用者體驗。Code Splitting 就是為了要解決單一 JS Bundle 過於肥大的問題,將原本單一的 bundle 切分成數個小 chunk,可以搭配平行載入,或者是有需要時才載入某些特定的 chink,又或是對一些不常變動的 chunk 個別做快取,來達到載入效能的優化。

較常見的 Code Splitting 又分為兩種方式 :

  • 抽離第三方套件
  • 動態載入功能模組 Dynamic Import

抽離第三方套件

抽離第三方套件又可以細分兩種方式:

  • 將所有第三方套件打包為單一檔案
  • 將第三方套件打包為多個檔案

將所有第三方套件打包為單一檔案

這邊主要參考今年 Modern Web 「你的 JS 該減肥了!5個提升網頁載入速度的技巧」這個議程的內容。關於 webpack 的 bundle,可以先做一個最大的拆分:

  • Application Bundle :UI 與商業邏輯,跟我們寫的程式有關,是經常變動的部分。
  • Vendor Bundle :第三⽅套件 / node_modules,不太會變動。

拆分出 Vendor Bundle 是有好處的,主要是因為通常它變動的頻率相對較低,因此比較適合被 cache,而在 Vendor Bundle 被 cache 的狀況下由於減少了 Application Bundle 的⼤⼩,因此加快了再訪者的載入速度。採用這樣的方式的優點為邏輯簡單,缺點為更新任何第三方套件都會使快取失效。

將第三方套件打包為多個檔案

採用這種方式的優點是可以根據套件關聯性打包,減少套件更新時造成的延遲。缺點則是相較前面打包成單一檔案的方式,這種方式需要處理的邏輯複雜許多。

透過 webpack 的 CommonsChunkPlugin 實作類似下圖的 config 可以達成這個效果

參考連結

Webpack Bundle Analyzer

透過 webpack-bundle-analyzer,我們可以透過視覺化分析專案有哪些 bundle chunk,各個 bundle chunk 的組成又為何,再針對可以改進的 bundle 進行優化。

(類似功能的工具還有如 WebpackVisualizerPlugin

動態載入功能模組 Dynamic Import

大多數狀態下我們會在檔案的開頭引入需要用到的模組,這些模組通常在網頁載入時就被引入進來,這種方式被稱為 static import,然而當有以下兩種狀況的需求時,static import 卻不能滿足我們:

  • 模組名稱為動態變數時
  • 需依照特定邏輯或特定時機引入時

這時候可以運用與之相對的技術: Dynamic Import。所謂 Dynamic Import 代表的即是

需要用到某段程式碼時才透過網路載入 JS bundle

要實現 Dynamic Import 需要靠 ESM import 語法:

例如上圖我們在 getComponent 這個函式中 import lodash 這個 package,只有當 getComponent 被呼叫時 lodash 才會被當成另外一個 chunk 載入。

目前瀏覽器的支援度也還算不錯,我們也可以透過 webpack 等打包工具來幫助我們實現 Dynamic Import。

參考連結:

webpack-code-splitting-section

v8.dev — dynamic import

了解了 Dynamic Import 的概念,接下來來談談 Dynamic Import 的使用情境,今天主要會介紹兩種情境:

  • 根據路徑做 Dynamic Import
  • 針對肥大套件做 Dynamic Import

根據路徑做 Dynamic Import

根據 GA 等分析工具長期分析後的數據指出,大部分的使用者只會停留在網站中的幾個熱門頁面,如果採用 Client-Side-Rendering 的方式建置網站的話,在沒有對 bundle 做額外處理的狀況下會在一開始載入 JS bundle 時就載入許多頁面的資源,這樣會導致許多不太會被使用者瀏覽的頁面是很有機會被載入卻又沒被使用的。這時候我們可以選擇針對路徑做 Dynamic Import,當切換到特定路徑時再載入該路徑會用到的資源。因為筆者擅長 React,所以就以 React 中的著名路由套件 react-router 搭配 React.lazy 來舉例:

造訪 / 時,Home component 將會被載入,而造訪 /about 頁面時則是 About Component 會被載入,這也就是基於 Route 的 Code-Splitting。

(當然 Component 的 code splitting 也不一定只能做 route based 的,開發者可以自己視情況對 component 做 Dynamic Import,例如 React.Lazy 就是為此而存在,不過 React.Lazy 目前還無法在 SSR 使用,如果使用 SSR 建構專案的讀者可以參考 Loadable Components

參考連結:React Official Doc

針對肥大套件做 Dynamic Import

除了針對路徑做 code splitting 以外,另一種常見的方式就是針對「肥大卻又不會馬上用到」的模組做 Dynamic Import,這時前面介紹過的 Webpack Bundle Analyzer 就展現價值了,有了視覺化的報表,開發者可以依據圖表判斷是不是有過於肥大的套件適合做 Dynamic Import。

(公司專案中剛好就有遇過把肥大又不一定會馬上被用到的 hls.js 做動態載入,大幅減少了一開始載入的 bundle size)

Lazy Load Image

稍早的 Image Minimize 段落中有提到圖片佔了網站資源相當大的比例,因此如果在網頁載入的瞬間就想把所有圖片都載入下來對效能是一個硬傷,這時候可以採用 lazy load 的方法去載入圖片,一開始只需載入部分的圖片,待現有圖片快要接觸到 viewport 的底部時再去動態載入新的圖片,例如 imgur 這種圖床網站就勢必會做圖片的 lazy load。

imgur image lazy load

要實現 image 的 lazy load 主要有兩種方式:

至於瀏覽器原生支援的部分,未來可能只需要在 img tag 加上 loading=lazy

<img src="image.png" loading="lazy" alt="…" width="200" height="200">

就可以自動幫我們做好 lazy load,不過目前瀏覽器支援度還不太普及,就再拭目以待囉!

另外能夠 lazy load 的不只是圖片而已,例如經過 pagination 的 API data 也很適合做 lazy load 喔!

Virtualized List

長列表(例如大量的文章列表)是網站中蠻常見的一個 feature,然而如果有 1000 篇文章,我們又將這些文章同時渲染的話,就必須生成 1000 個 dom 節點,更不用說文章結構通常是相對複雜的,像這樣同時渲染數量頗大的元素會有幾個明顯的缺點:

  • 載入時白屏時間會比較長
  • 渲染了大量的 dom 節點的狀況下,在滾動事件觸發時會大大增加記憶體的用量
  • 容易失幀,因為渲染很慢,所以無法維持瀏覽器的幀率,頁面會顯得卡頓
  • 最慘的話網頁會失去響應

而且這些問題在 Desktop 瀏覽器就會發生了,換作是手機瀏覽器只會讓問題變得更嚴重,因此這種狀況下我們應該優化長列表,提升使用者體驗。

virtualized list 就是優化長列表的一種技巧,名字聽起來很深奧,不過它的概念其實非常簡單:

用陣列儲存所有列表元素的位置,只渲染可視區 (viewport)內的列表元素,當可視區滾動時,根據滾動的 offset 大小以及所有列表元素的位置,計算在可視區應該渲染哪些元素。

圖片來源

以上圖來說,假設可視區最多只能顯示 6 個 item,那即使我們的列表總共有 1000 個,也只會渲染出現在可視區的 6 個元素,當原本被渲染的 item 移出可視區後,就會被 unmount 掉,避免前面說的同時生成一堆 dom 節點的狀況,也因此有效的解決了上面說的幾個缺點。

如果對如何實作一個 virtualized list 有興趣,可以參考這篇文章

也有許多現成的 virtualized list 套件例如 react-window 或是 vue-virtual-scroll-list。在上一個 Section 提到 Intersection Observer API 的練習作業也有使用 react-window 作為 virtualized list,配合 lazy load 實現無限滾動的文章列表,有興趣的讀者可以參考看看(因為是以前拿來練習的 code 所以寫得不太好,還請見諒)。

Tree Shaking

開發專案免不了會下載第三方套件來節省自己重複造輪子的成本,然而也許某些狀況我們只會使用一個套件模組之中的特定幾個 function,其他的 function 幾乎都不會用到。不過如果我們為了這幾個 function 而要載入整個模組,就似乎有點得不償失了,這時候 Tree shaking 會是解救我們的技巧。

什麼是 Tree Shaking ?

其實這個技巧跟字面上的意思很像,當用力搖一棵樹時可能會把很笨重的果實給搖落,在程式面來說就是把「用不到的程式碼給搖落下來」,上面的例子講到我們可能會為了幾個特定函式而需要載入整個套件,運用 Tree Shaking 之後,可以讓打包工具在打包階段就可以分析哪些 code 或哪些 function 是用不到的,而把它們從最終的 bundle 中剔除,換句話說就是確保最後的 bundle 不會包含無用或多餘的程式碼與資源,減少 bundle size。

如何做到 Tree Shaking ?

要做到 Tree Shaking,首先得透過 ES6 import export 的幫忙

// Import all the array utilities!import arrayUtils from "array-utils";

假設我們要使用 array-utils 中的某幾個函式,應該避免上面的引入方式(把整個 array-utils 引入進來,再去使用特定的 property),而改為下面這種引入方式:

// Import only some of the utilities!import { unique, implode, explode } from "array-utils";

不過此時如果用打包工具例如 webpack 打包的話,還是會將整個 array-utils 加進 bundle 裡,此時還得靠例如 uglifyjs-webpack-plugin 或其他的 plugin 再加上一些額外設定才能把用不到的程式從 bundle 中移除。

(關於 CommonJS 與 ES6 的區別,還有為什麼可以實現 Tree Shaking,請參考這篇文章,關於 webpack 針對 Tree Shaking 的設定可以參考 webpack 官方文件

Preload、Prefetch And Others In Link Tag

這邊要介紹五個特殊的 link 技巧:prefetch、preload、preconnect、dns-preconnect、prerender。讀者可能看起來一頭霧水,不過其實它們都有一個共同的目標,或者說共同的效能優化方式:

不久的將來會用到的資源預先處理,這裡的處理有可能是載入資源,或是建立連線,因此在真的要使用到該資源時可以省去不少時間。

Preload VS Prefetch

preload 與 prefetch 是兩個較常被搞混的技巧,兩者的作用都是在提早取得將來會用到的資源,然而兩者的差別在於:

  • Preload:取得當前頁面的資源(例如字體 font)。
  • Prefetch:告訴瀏覽器「這些資源我待會會用到,先幫我下載吧!」不過與 preload 不同的是 prefetch 抓取的資源不限於當前頁面使用,也就是可以跨越 navigation,例如你很確定使用者會點擊下一頁,就可以使用 prefetch 預先抓取下一頁的資源。

瀏覽器對於資源的載入順序是有規則的,是以檔案類型來決定下載的優先順序,以 chrome 舉例來說(不確定不同瀏覽器是否不同):

High priority : style /font / XHR (sync) 
Medium priority : 位於可視區域的圖片 / Preload without as/ XHR (async)
Low priority : favicon、script async / defer / block、不在可視區域的圖片、媒體檔、SVG 等

preload 與 prefetch 也是以屬性來分辨檔案類型

<link rel="preload" as="font" crossorigin="anonymous" type="font/woff2" href="myfont.woff2">
使用 preload 前後的載入順序變動
chrome 資源載入順序對照表
瀏覽器 devtool 的 network tab 也可以看到各資源的 priority

Preconnect

preconnect 相當於告訴瀏覽器:「這個網頁將會在不久的將來下載某個 domain 的資源,請先幫我建立好連線。」

要理解 preconnect 能夠達成的事,得了解瀏覽器在實際傳輸資源前,實際上經過哪些步驟(以下內容與圖片參考這篇文章):

  • 向 DNS 請求解析網域名
  • TCP Handshake
  • (HTTPS connection) SSL Negotiation
  • 建立連線完成,等待拿到資料的第一個byte

上面的四個步驟中,每一步都會需要一個 RTT (Round Trip Time) 的來回時間。所以在實際傳輸資料之前,已經花了3個 RTT 的時間。如果在網路狀況很差的狀況下,會讓獲取資源的速度大大降低。

利用 preconnect 提早建立好與特定 domain 之間的連線,省去了一次完整的 (DNS Lookup + TCP Handshake + SSL Negotiation) ,共三個 Round Trip Time 的時間。

Preconnect Use Cases :

通常只會對確定短時間內就會用到的 domain 做 preconnect,因為如果 10 秒內沒有使用的話瀏覽器會自動把連線 close 掉

  • CDN:如果網站中有很多資源要從 CDN 拿取,可以 preconnect CDN 的域名,這在不能預先知道有哪些資源要抓取的情況,是蠻適合的 use case。
  • Streaming 串流媒體 (待會看下方 lite-youtube-embed 的例子)

DNS Preconnect

跟 preconnect 類似,差別在於只提示瀏覽器預先處理第一步 DNS lookup 而已。也就是說

dns-preconnect = DNS look up
preconnect = DNS look up + TCP Handshake + SSL Negotiation

至於什麼時機要使用哪個方式,可以參考這篇 stackoverflow 問題

Prerender

prerender 比 prefetch 更進一步。不僅僅會下載對應的資源,還會對資源進行解析。解析過程中,如果需要其他的資源,可能會直接下載這些資源,基本上就是盡可能預先渲染下個頁面,這樣一來當用戶在從當前頁面跳轉到目標頁面時,瀏覽器可以快速的響應。適合用在用戶很高機率會轉到另一個頁面的狀況下使用

不過瀏覽器支援度有點低,筆者目前也沒試過這種方式,就留給有興趣的讀者自行研究了。

Preconnect + Preload Example:lite-youtube-embed

lite-youtube-embed 是一個號稱渲染速度比原生 iframe 快 224 倍的 youtube 影片播放元件,它能達成這樣的效能提升其實做的事並不複雜,主要有兩件事:

  • preload youtube 影片的 thumbnail (預覽圖),讓使用者可以儘早看到預覽圖,提升使用者體驗
  • 當使用者鼠標移到元件範圍時,對 youtube domain 進行 preconnect,當使用者真的點下播放鍵時才真正載入 iframe,不過因為有對 youtube domain 做 preconnect,因此省去 3 個 Round Trip Time 的時間,因此可以更快速開始播放影片。

我有試著實作一個 react component 的版本,有興趣的讀者可以參考看看

CDN & Cache

圖片來源

CDN 的全名為 Content Delivery Network 內容傳遞網路。

要知道距離不僅僅是愛情的毒藥(誤),也是影響 response time 的重大因素。假設你身在台灣,跟一個架設在台灣的 server 取資料,花費的時間只要 500 ms,但如果去跟一個架設在美國的 server 取相同的資料,這時候的 response time 可能就增長為 3000 ms。

CDN 就是透過在各個地理位置建立 edge server 來避免取資源時都要跟距離遙遠的 server 溝通,造成效能的低落。當使用者對被 CDN 加速過的域名發出 request 時,CDN 會自動將 request 導到地理位置離使用者較近或是流量較不吃緊的 edge server,儘管第一次取資源時因為 CDN 還沒有快取的資料,所以仍然需要跟 original server 要資料,不過之後的 request 就可以透過地理位置離使用者較近的 CDN cache 取得,加快 client 端資源載入的速度。除了 cahce 機制以外,CDN 某方面也算是增強了服務的可用性、負載功能、安全性(降低 DDOS 對網站的影響)。

如果要更深入了解 CDN 或者 networking cache,推薦參考這篇鐵人賽的優質文章

而目前最熱門的免費 CDN 服務大概就是 Cloudflare 了, 它主要提供的服務有:

  • CDN
  • Cache
  • Load Balancing
  • 代管 DNS
  • 阻擋惡意流量

有興趣的讀者趕緊去體驗看看吧!

Write Good Code

其實並不是實作文章到目前為止介紹的技巧才能做到效能優化,開發者平常在寫 code 的時候就該多注意自己寫的 code 是否會對效能造成影響,例如:

  • 會不會造成不必要的重新渲染 (Re-render)?
  • 事件監聽器 (Event Listener) 在用不到時是否正確被移除?
  • 撰寫的 Function 應該注意一下是否有時間複雜度更低的解法,或是會不會造成不必要的 memory 浪費?
  • 擅用適合情境的 Design Pattern,除了提高程式碼可讀性與可維護性外,也有優化效能的機會。
  • 如果是 React 開發者(筆者是 React 狂粉),注意是否有些地方可以運用 useMemo、React.memo、useCallback 等優化技巧(當然其他框架應該也有對應的解法)

雖然在文章的開頭有說到有時「先求有再求好」反而是較佳的方案,不過我們卻應該在平時寫 code 的時候就去避免寫出效能不佳與不好維護的程式碼,不然最後要做效能優化的時候,可能還得面對自己留下的技術債,解決難度又提昇了不少。

多參考其他成功的效能優化案例

透過參考其他公司的效能優化成功案例,可以得知如何化理論為實作,還有執行效能優化後具體的數據成效為何,例如載入時間減少多少、bundle size 減少多少、或是效能優化後真實的使用者回饋…等,都可以作為自己專案的參考,以下附上幾篇知名企業的 web 效能優化案例:

Netflix

Tinder

Pinterest

結論

果然想透過一篇文章講完 Web 效能優化這個主題還是稍嫌勉強,雖然看起來提到了很多技術,但仍然有非常多優化技巧是沒有機會提到的,而每個技巧也只能講解大略的概念(如果真要認真研究,任一個主題用一篇文章應該都是講解不完的),不過文章內提供了許多參考連結,讀者可以再根據有興趣的技巧深入研究,希望這篇文章可以讓讀者對於 Web 前端的效能優化有個大概的認識與方向,一起跳入效能優化的坑裡吧!

如果喜歡我的文章,可以幫我拍拍手 👏 或訂閱我的 medium,你的支持是我寫作與追求進步的最大動力 !

也歡迎關注我的 github,互相交流,一起進步!

--

--