所以,前端效能優化到底是在優化什麼?
最近蠻多人問前端效能優化到底要從哪裡下手,網路上一堆文章列了十幾二十條,看得眼花撩亂。老實說,你不用全部都做。今天我們不搞那種清單式的東西,我想直接聊聊幾個... 我自己覺得最有感、CP 值最高的東西。這比較像是我在整理筆記,想到什麼講什麼,可能會有點跳躍,但應該比較接近真實世界會遇到的狀況。
重點一句話
與其追求做完 100 件事,不如先找到那 3 件能讓速度提升 80% 的事。通常都跟「第一次載入時,到底給了使用者多少東西」有關。
最大的魔王:關鍵渲染路徑 (CRP)
好,第一個一定是這個,Critical Rendering Path,關鍵渲染路徑。聽起來很學術,但講白了就是,從使用者在瀏覽器輸入網址按下 Enter,到畫面上真的看到東西(比如說網站的標題或第一張圖),中間這段過程就叫 CRP。這段路越順,使用者感覺就越快。
你可以想像成點餐。你點了一份套餐,廚房(瀏覽器)要先拿到你的菜單(HTML),然後去準備食材(下載 CSS、JS),接著開始料理(解析程式碼、計算排版),最後才把菜端到你面前(畫到螢幕上)。
如果你的菜單寫得亂七八糟,或是一開始就點了十道菜,那廚房肯定會手忙腳亂,出菜自然就慢了。
所以,到底要怎麼做?
- 把最重要的 CSS 挑出來內聯 (inline):使用者第一眼會看到的畫面(above-the-fold),它需要的 CSS 樣式,就那麼一小撮。把這一小撮直接塞進 HTML 的
裡,剩下的用非同步方式載入。這樣瀏覽器不用等全部 CSS 下載完才開始畫,速度感會差很多。 - 讓 JS 不要擋路:大部分的 JavaScript 其實沒那麼緊急。比如分析工具的 code、聊天外掛...這些都不是第一時間要有。在
標籤加上defer或async,它就會乖乖在背景下載,不影響畫面先出來。 - DOM 結構愈單純愈好:沒事不要寫一堆層層疊疊的 。DOM 樹太深,瀏覽器計算排版會很累,就像在一個塞滿雜物的房間裡找東西一樣。
對了,講到這個,不得不提 Google 的 Core Web Vitals。你把 CRP 搞定,LCP(最大內容繪製)這個指標基本上就先贏一半了。Google 搜尋引擎現在很看重這個,因為這直接反應了使用者體驗。
第二刀砍下去:圖片和其他媒體資源
搞定了 CRP,接下來最有感的就是圖片。現在的網站圖片都超大超漂亮,但這也是拖慢速度的原罪。你總不會希望使用者用手機 4G 網路,開個網頁就把他半個月的流量給吃光吧。
這幾招超實用
- 原生懶載入 (Lazy Loading) 是你的好朋友:現代瀏覽器幾乎都支援
。只要在圖片標籤上加上這個屬性,圖片會等到快要滑入畫面時才開始載入。一行 code 的事,CP 值高到破表。 - 用新一代的圖片格式:WebP 或 AVIF 格式的圖片,檔案大小常常只有傳統 JPG/PNG 的一半,甚至更少,但畫質肉眼看不太出差別。現在支援度已經很高了,可以放心用。我自己的習慣是搭配
標籤,讓支援的瀏覽器用 WebP,不支援的就 fallback 到 JPG。 - 不要用一張圖打天下:拜託不要把一張 4K 解析度的超大圖片,直接縮小用在手機版網頁上。這根本是謀殺使用者的流量。用
srcset屬性,提供好幾種不同尺寸的圖片,讓瀏覽器自己去根據裝置寬度、螢幕解析度去抓最適合的那張。
老實說,光是把圖片優化做好,你的 Lighthouse 分數可能就從 50 分直接跳到 80 分,超有感的。
優化前後的 Lighthouse 效能分數對比,非常有感 幫你的程式碼減減肥
現在的前端專案,隨便裝幾個套件,打包起來的 JS 檔案就肥得要命。使用者進首頁,結果連管理後台才會用到的圖表 library 也一起下載了,這...這就很不合理啊。
程式碼分割 (Code Splitting)
這個概念很簡單,就是「按需載入」。使用者需要什麼功能,才給他那份功能的程式碼。
- 用框架內建的功能:如果你用 React,
React.lazy跟Suspense就是為此而生的。把那些很肥、或不是馬上要用的元件用React.lazy包起來,它就會被切成獨立的 chunk file,等真的要渲染它時才會去下載。Vue 也有類似的非同步元件機制。 - 手動用 `import()`:就算你不是用框架,現代 JavaScript 的 dynamic import 語法 `import()` 也能做到一樣的效果。比如,使用者點了某個按鈕才跳出一個複雜的表單,那處理那個表單的 JS 就可以用 `import()` 包起來,點擊時才載入。
這在大型專案裡特別重要。我之前有個案子,光是把一個巨大的 charting library 拆出去,首頁載入的 JS 大小就少了 60%,Time to Interactive (TTI) 的時間也縮短了快一秒。
在 React 專案中使用 React.lazy 進行元件的動態載入 壓縮、最小化、還有搖樹 (Tree Shaking)
這三兄弟通常是一起做的。你可以把它們想成打包行李。
- 最小化 (Minification):把衣服捲起來,塞掉所有空隙。對應到程式碼就是拿掉所有空白、換行、註解。
- 壓縮 (Compression):用真空壓縮袋把衣服體積變到最小。對應到網站就是伺服器用 Gzip 或 Brotli 演算法壓縮檔案,傳到瀏覽器再解壓縮。Brotli 通常效果更好,但要看你的伺服器支援度。
- 搖樹 (Tree Shaking):出門前檢查行李,把那些「可能用得到但其實根本不會穿」的衣服拿出來。對應到程式碼就是,打包工具 (Webpack, Vite) 會去分析你的 code,如果你 `import` 了一個 library 但只用了裡面一個小 function,它會很聰明的只把那個 function 包進來,其他沒用到的就全部丟掉。
但...事情沒這麼簡單吧?一些常見的坑
講了這麼多好處,但實際做起來總是會踩到一些坑。不然大家網站不就都飛天了。
優化技巧 你以為的好處 實際上可能遇到的坑 我自己的 murmur 程式碼分割 (Code Splitting) 初始載入變小,速度變快。 HTTP request 變多了。切得太碎,一堆小檔案的請求延遲加總起來可能比一個大檔案還慢。還有,元件載入時的 loading state 沒處理好會讓畫面閃爍。 這東西是雙面刃。切分點要找對,通常是按「頁面」或「大的功能區塊」來切,而不是把每個按鈕都切出去。 圖片懶載入 (Lazy Loading) 節省頻寬,加速初始渲染。 LCP 指標可能會變差!如果你的「最大內容」剛好是那張被 lazy load 的首圖,那它就會比較晚出現,LCP 時間就拉長了。SEO อาจได้รับผลกระทบ。 規則很簡單:第一屏 (above-the-fold) 看得到的圖片,千萬不要 lazy load。尤其是 banner、主視覺這種。 快取 (Caching) 重複訪問超快,幾乎是秒開。 使用者卡在舊版本!你更新了網站,但因為快取設定太強,使用者還在看上禮拜的舊內容,甚至因為新舊 JS/CSS 混用導致網站直接壞掉。 快取策略真的很需要規劃。靜態資源 (版本化的 JS/CSS) 可以設超長快取;但 HTML 入口頁面本身快取時間要短,或用 `no-cache` 確保每次都拿到最新的。 使用 CDN 使用者從最近的節點載入資源,超快。 設定錯誤會天下大亂。之前就遇過 CDN 的快取規則設錯,結果全球的使用者都看到某個開發中版本的頁面...災難。 CDN 不是用了就沒事,purge cache 的機制跟 cache key 的設定要搞很懂才行。尤其在台灣,有時候會覺得奇怪怎麼某些 CDN 在中華電信線路下反而變慢,這跟網路路由有關,也是個水很深的議題。 還有一些可以撿的芝麻
上面講的都是大方向,影響最劇烈的。如果都做完了還想更快,可以考慮下面這些。
- 善用 Preconnect 和 Preload:如果你知道等一下一定會用到某個字體檔,或是一定會跟 Google Analytics 的伺服器連線,可以用
或叫瀏覽器「先準備一下」。它可以提早做 DNS查詢、建立 TCP 連線,等真的要下載資源時就能省下這段時間。 - 用 Web Workers 分擔工作:主執行緒很忙,要處理使用者的點擊、畫面渲染... 如果你有一些很吃計算資源的工作(比如處理一個超大的 JSON 資料),可以把它丟到 Web Worker 這個「分身」去做,這樣就不會卡住主畫面,讓使用者覺得網頁當掉了。
- Debounce 和 Throttle:這兩個是處理頻繁觸發事件的技巧。比如 `onscroll` 事件,使用者稍微滑一下滾輪,事件可能就觸發了幾十次。如果你在事件處理函式裡做了複雜的 DOM 操作,畫面會卡到爆。用 `throttle` 可以確保它在一定時間內最多只執行一次;用 `debounce` 則是等使用者停止動作後才執行。用在搜尋框的 auto-complete 功能上超好用。
說真的,前端效能優化是個無底洞,但也是個很有趣的領域。它不只是技術,還包含了很多對使用者行為的理解。你做的每一點努力,都能直接轉換成更好的使用者體驗。從最有感的 CRP 和圖片下手,通常就不會錯了。
換你試試看!
打開你自己的網站或你常用的網站,用瀏覽器的開發者工具 (按 F12) 裡的 Lighthouse 跑一次效能報告。你覺得拖慢速度的最大元兇是「圖片太大」還是「JS 檔案太多」?在下面留言分享你的發現吧!
- 原生懶載入 (Lazy Loading) 是你的好朋友:現代瀏覽器幾乎都支援
