為什麼要再寫一次圖片 Lazy Loading#

Lazy Loading 是廣為人知的網頁優化技巧,套用在圖片上能減少不必要的流量與效能浪費。雖然主流瀏覽器已內建支援,相關套件也很多,但實際使用時還是有不少容易忽略的小細節。本章從 debug 視角彙整圖片 Lazy Loading 時值得留意的事項。

與本章互補的「機制原理」整理可參考 02-performance-optimization/03-loading-strategies/03-lazy-loading

瀏覽器原生 Lazy Loading#

除了 IE 與部分舊版 Safari 之外,主流瀏覽器都已支援原生圖片 Lazy Loading,不需要任何 JavaScript 或 CSS,也省去了「JavaScript 不能執行時要怎麼辦」的處理。只要替 img 加上 loading="lazy" 即可:

<img src="image.png" loading="lazy" alt="..." />

loading 屬性可用值#

  • auto:等同於沒寫,採用瀏覽器預設策略。
  • lazy:圖片進入 Viewport 或接近 Viewport 時才開始載入。
  • eager:不論圖片位置,立即開始載入。

別讓 Lazy Loading 影響 CLS#

所有 Lazy Loading 的圖片都應該事先用佔位符(Placeholder)撐出載入完成後所需的空間,否則圖片填上後會推擠周圍排版,造成 CLS (Cumulative Layout Shift) 惡化。

實務做法有幾種:

  • 直接在 img 上設定 widthheight
  • 透過 CSS 指定固定寬高。
  • 在外層包一層 Container,由 Container 來保留空間。
<img src="image.png" alt="" width="200" height="200" />
<img src="image.png" alt="" style="width: 200px; height: 200px;" />
<div class="img-container">
  <img src="image.png" alt="" />
</div>

不要對所有圖片都做 Lazy Loading#

啟用 Lazy Loading 後,瀏覽器要等到首次轉譯結束、能判斷圖片位置時才會決定要不要載入。也就是說,在轉譯完成前會出現一段流量空窗,應該善用這段時間先載入必看的圖片。

下列情境通常不適合 Lazy Loading:

  • Header 內的圖片、商標。
  • 文章首圖等網頁一打開就會看到的圖。
  • 頁面長度很短,或圖片雖在 Viewport 之外但離得很近。

讓瀏覽器在 HTML Parser 解析到 <img><picture> 時就直接開始下載,反而能加速首屏完成的時間。

把佔位符做得有質感#

避免使用者看到空白方塊,最簡單的做法是放灰底加圖示,但其實還有更值得投資的做法。

代表色#

從圖片中萃取主色作為佔位背景,例如 Pinterest 在卡片上採用此技巧。

低品質圖片佔位符(LQIP, Low Quality Image Placeholder)#

把原圖縮成超小尺寸,再放大顯示作為佔位背景。比起單色,看起來更生動,也能透露圖片內容的輪廓。Medium 的圖片載入即為此風格。

SQIP#

SQIP 套件提供其他形式的佔位符,例如 SQIP pixels、SQIP art(以 SVG 組成)等。

同類技術還有模糊雜湊(BlurHash):把圖片壓縮為短字串,前端解碼後即可顯示模糊預覽,不用另外下載小圖。

提早一點載入#

當使用者網速較慢、捲動速度較快時,等到圖片進入 Viewport 才載入會看到還在轉圈圈的圖。一般做法會額外設定 Margin,例如離 Viewport 1000px 以內就開始抓。

Chrome 的原生 loading 屬性其實也內建類似機制:4G 連線下會在距離 Viewport 1250px 處開始載入,3G 則為 2500px。根據 Google 的研究,平均等待時間可控制在 10ms 以下。

不能執行 JavaScript 的情境#

如果整頁的圖片都靠 JavaScript 觸發 Lazy Loading,當 JavaScript 無法執行時就完全載不進來。常見補救是把圖片再寫進一份 <noscript>,但這會帶來新的問題:若已經先放了 Placeholder,可能會誤載兩張圖。

<img class="lazy" src="placeholder.jpg" data-src="image.jpg" alt="..." />
<noscript>
  <img src="image.jpg" alt="..." />
</noscript>

可以採取一個簡單的處理方式:預設先用 CSS 隱藏所有 Lazy Loading 的圖片,等 JavaScript 執行後再手動顯示;如此一來在無 JavaScript 環境,這些圖片不會出現,而 <noscript> 內容會被瀏覽器渲染。

<html class="no-js"></html>
.no-js .lazy {
  display: none;
}
document.documentElement.classList.remove("no-js");

處理 CSS background#

目前瀏覽器原生 Lazy Loading 還無法套用在 CSS background-image,需要自行實作或仰賴套件。background-image 的特性是:必須出現在 Render Tree 才會被下載,反過來說,只要對應元素沒進入 Render Tree,就不會觸發圖片載入。

.lazy-background {
  background-image: url("placeholder.jpg");
}

.lazy-background.visible {
  background-image: url("image.jpg");
}

visible 這個 Class 在元素接近 Viewport 時動態加上,即可切換到正式圖。觸發時機可以透過 Scroll event 或 Intersection Observer 等方式判斷,邏輯與一般 img Lazy Loading 類似。

預期之外:錯誤處理#

只要走網路就有出錯的可能,例如重新部署使網址改變、伺服器當機、流量異常等情況,都會讓 Lazy Loading 的圖片載入失敗。

可以透過下列做法提升體驗:

  • 用通知或顯示錯誤訊息告知使用者目前發生的狀況。
  • 對於關鍵內容,提供「重新載入圖片」按鈕等錯誤恢復機制。

非同步解碼,避免畫面卡頓#

動態插入圖片時,主執行緒需要先做完解碼(Decode)才能把圖畫到螢幕上。圖片越大,解碼越久,使用者就越可能感受到瞬間卡頓。可以先在背景完成解碼,再插入 DOM:

const img = new Image();
img.src = "large-image.jpg";
img.decode().then(() => {
  imageContainer.appendChild(img);
});

上線前一定要實測#

不論是手刻、套件或瀏覽器原生,做完 Lazy Loading 之後一定要在真實環境驗證。常見技巧:

  • 用無痕模式或清除快取,模擬第一次造訪的體驗。
  • 在 DevTools Network 面板降低網速,觀察 Lazy Loading 是否正常觸發。
  • 搭配 Lighthouse,找出漏網之魚或其他可優化的圖片。

常見套件#

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10252723