優化首圖體驗的終極指南:SVG + BlurHash + LQIP 實戰解密

作者: Calpa Liu
字數:3100
出版:April 22, 2025
分類: 技術文章 前端開發 網站效能 SEO

你還在用傳統圖片載入方式嗎?透過 SVG 預佔位、BlurHash 模糊預覽與 LQIP 技術,打造兼顧效能與美感的圖片顯示體驗,讓網站更快更穩更吸睛。

為什麼圖片載入會影響用戶體驗與 SEO?

圖片載入對網站性能和用戶體驗有重大影響。當首圖加載緩慢時,會直接影響 Google 的核心網頁指標,如 LCP (Largest Contentful Paint) 和 CLS (Content Layout Shift)。LCP 過高意味著頁面主要內容載入時間延長,而 CLS 錯亂則可能導致頁面元素位移,影響用戶操作和閱讀體驗。

除了技術指標外,慢速載入還會造成用戶體驗下降。白屏等待期間缺乏視覺提示,容易讓用戶產生網站加載緩慢的印象。而圖片載入後引起的頁面跳動,更是破壞了穩定的閱讀體驗,可能導致用戶流失。

這個網站採用了漸進式圖片加載技術。如果您想親身體驗這項技術帶來的性能提升,可以嘗試重新加載頁面或在較慢的網絡環境下重新載入。這樣您就能清楚地看到圖片是如何從模糊到清晰逐步加載的過程,以及這種加載方式如何提高了整體的頁面加載速度和用戶體驗。通過比較不同網絡條件下的加載效果,您將更深入地理解漸進式圖片加載技術的優勢。

技術原理解析

使用 SVG 來避免 CLS(Cumulative Layout Shift)

當我們談論圖片載入時,常常關注畫質與壓縮技術,但實際上,圖片「尚未載入時」的版面表現同樣重要。這正是 SVG(Scalable Vector Graphics)在漸進式圖片載入中扮演關鍵角色的原因之一。透過嵌入式 SVG,我們能在圖片尚未出現前,就先行為它預留出正確的空間比例,從而有效防止 CLS(Cumulative Layout Shift)等常見的排版問題發生。這種「預佔位」策略不但提升視覺穩定性,也讓使用者從進入頁面起就感受到更流暢的體驗。

SVG 的設計彈性使其特別適合用於圖片容器。藉由 viewBox 屬性與 preserveAspectRatio 設定,我們可以準確控制圖片在不同解析度下的顯示比例。這段範例程式碼展示了如何以 <svg> 建立一個響應式容器,搭配 width="100%"height="${height}" 的設計,確保容器在頁面載入初期就能正確佔位,而不會干擾整體版面。內部的 <rect> 元素還能提供中性色背景,讓使用者不會面對一片空白的畫面。這些看似簡單的設定,其實正是打造穩定、優雅圖片載入流程的基礎。

使用 BlurHash 提供即時視覺反饋

BlurHash 是一種創新的圖片預覽技術,它巧妙地將圖片的主要視覺特徵壓縮成一個簡短的字符串。這個字符串通常僅有 20-30 個字符長,卻能夠在客戶端被解碼並渲染成一個模糊但視覺上令人愉悅的預覽圖。這種技術不僅能夠大幅減少初始加載時間,還能為用戶提供即時的視覺反饋,極大地提升了網頁的感知性能。

BlurHash 的工作原理涉及多個步驟。首先,在服務器端,系統會分析原始圖片,提取其主要的色彩和形狀特徵。這些特徵隨後被編碼成一個簡潔的字符串,例如 LlNA}44TN{kqyEtls:xux^tRRjRi。當這個字符串傳輸到客戶端後,前端程序會將其解碼成像素數據。最後,這些像素數據通過 Canvas 技術被渲染成一個模糊的預覽圖,為用戶呈現出原圖的大致輪廓和色彩分布。

相比於傳統的圖片加載方式,BlurHash 具有多項顯著優勢。首先,它的傳輸成本極低,通常只需要約 30 字節的數據量,這大大減少了網絡負載。其次,儘管數據量小,BlurHash 仍能保留並呈現原圖的主要色彩分布,為用戶提供更加豐富的視覺體驗。最後,與簡單的單色佔位符相比,BlurHash 生成的預覽圖在視覺效果上更為優越,能夠更好地吸引用戶注意力並改善整體的頁面美感。這些特性使得 BlurHash 成為現代網頁設計中提升用戶體驗的有力工具。

使用 LQIP 提供更佳的視覺體驗

LQIP (Low Quality Image Placeholders) 是一種優化圖片載入體驗的技術,通過先顯示極低解析度的縮略圖,再在原圖載入完成後無縫替換,來提升用戶的視覺體驗。這種方法的核心在於從原圖生成超小尺寸(如 20x15px)的縮略圖,將其壓縮並轉換為 Base64 格式或使用 WebP 格式內嵌,然後在頁面初始載入時顯示這個 LQIP,同時在背景加載高品質原圖,最後在原圖加載完成後實現平滑過渡。

LQIP 與 BlurHash 相比,各有其優勢。LQIP 的檔案大小通常比 BlurHash 大(約 300-600B),但其視覺細節可能更為豐富。此外,LQIP 的實作相對更為簡單,不需要額外的解碼步驟,這使得它在某些場景下更容易被採用和實現。

然而,選擇使用 LQIP 還是 BlurHash 取決於具體的應用場景和需求。LQIP 可能更適合那些需要更多細節預覽的場景,而 BlurHash 則可能在極小檔案大小和快速載入方面有優勢。無論選擇哪種技術,兩者的目標都是相同的:在保證頁面快速加載的同時,為用戶提供更好的視覺體驗,減少等待高品質圖片載入時的視覺空白。

載入高品質圖片

在漸進式圖片載入過程中,我們如同藝術家創作一幅精美畫作。首先,SVG 勾勒出畫布輪廓,確保圖片容器比例恆定。BlurHash 或 LQIP 技術隨後快速塗抹模糊底色,為用戶描繪圖像大致形態與氛圍,宛如畫家的即時速寫,捕捉主題精髓。

高品質圖片逐步載入,細節逐一填充,恍若藝術家精心雕琢每一筆觸。這種漸進式載入不僅優化了性能,更為用戶呈現出層次豐富的視覺盛宴,彷彿欣賞一幅逐步完善的藝術品。

這種創新的圖片載入策略不僅解決了技術難題,更將藝術美學融入了網頁設計之中。它巧妙平衡了性能優化與視覺體驗,為用戶帶來既快速又優雅的瀏覽感受。通過這種方法,我們成功將枯燥的圖片加載過程轉化為一場引人入勝的視覺饗宴,展現了技術與藝術的完美結合。

實作流程與程式碼範例

讓我們使用 Vue 3 和 Astro 框架實作完整的漸進式圖片載入組件。

圖片預處理

首先,需要為每張圖片生成 BlurHash 或 LQIP。以下是常見的處理方法:

後端生成 BlurHash

// Node.js 環境中使用 blurhash 套件
import { encode } from "blurhash";
import sharp from "sharp";

async function generateBlurhash(imagePath) {
  // 讀取圖片
  const { data, info } = await sharp(imagePath)
    .raw()
    .ensureAlpha()
    .toBuffer({ resolveWithObject: true });

  // 生成 BlurHash
  const hash = encode(
    new Uint8ClampedArray(data),
    info.width,
    info.height,
    4, // x 組件數
    3 // y 組件數
  );

  return {
    blurhash: hash,
    width: info.width,
    height: info.height,
  };
}

// 使用方式
const metadata = await generateBlurhash("path/to/image.jpg");
// { blurhash: "LlNA}44TN{kqyEtls:xux^tRRjRi", width: 800, height: 600 }

使用 Vite 插件生成 LQIP

對於使用 Vite 作為建置工具的專案,可以直接使用 vite-plugin-lqip 插件:

// vite.config.js
import lqip from "vite-plugin-lqip";

export default {
  plugins: [lqip()],
};

// 使用方法
import imgData from "./path/to/image.jpg?lqip";
// imgData = { lqip: "base64...", src: "/assets/image.jpg", width: 800, height: 600 }

前端組件實作 - Vue 3

以下是一個完整的 Vue 3 漸進式圖片載入組件:

<template>
  <div
    class="progressive-image"
    :style="{ paddingBottom: `${(height / width) * 100}%` }"
  >
    <!-- SVG 容器,確保比例一致 -->
    <svg
      v-if="!loadedLqip && !loadedHighQuality"
      viewBox="0 0 width height"
      preserveAspectRatio="none"
      :width="width"
      :height="height"
    ></svg>

    <!-- BlurHash 預覽 (通過 Canvas 渲染) -->
    <canvas
      v-if="useBlurHash && blurHash && !loadedHighQuality"
      ref="blurHashCanvas"
      class="blur-preview"
      :class="{ 'fade-out': loadedHighQuality }"
    ></canvas>

    <!-- LQIP 預覽 -->
    <img
      v-if="lqipSrc && !useBlurHash && !loadedHighQuality"
      :src="lqipSrc"
      class="lqip-preview"
      :class="{ 'fade-out': loadedHighQuality }"
      alt=""
      @load="loadedLqip = true"
    />

    <!-- 高品質圖片 -->
    <img
      ref="highQualityImg"
      :data-src="src"
      :alt="alt"
      class="high-quality"
      :class="{ loaded: loadedHighQuality }"
      @load="onHighQualityLoaded"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from "vue";
import { decode } from "blurhash";

const props = defineProps({
  src: { type: String, required: true },
  alt: { type: String, default: "" },
  width: { type: Number, required: true },
  height: { type: Number, required: true },
  lqipSrc: { type: String, default: "" }, // Base64 LQIP
  blurHash: { type: String, default: "" }, // BlurHash 字串
  useBlurHash: { type: Boolean, default: false }, // 是否使用 BlurHash
});

const loadedLqip = ref(false);
const loadedHighQuality = ref(false);
const blurHashCanvas = ref(null);
const highQualityImg = ref(null);
const observer = ref(null);

// 渲染 BlurHash 至 Canvas
const renderBlurHash = () => {
  if (!props.blurHash || !blurHashCanvas.value) return;

  const canvas = blurHashCanvas.value;
  const ctx = canvas.getContext("2d");
  const canvasWidth = 32; // 低解析度即可
  const canvasHeight = Math.floor(canvasWidth * (props.height / props.width));

  canvas.width = canvasWidth;
  canvas.height = canvasHeight;

  // 解碼 BlurHash 字串為像素資料
  const pixels = decode(props.blurHash, canvasWidth, canvasHeight);
  const imageData = new ImageData(pixels, canvasWidth, canvasHeight);
  ctx.putImageData(imageData, 0, 0);
};

// 監聽圖片進入視口
const setupIntersectionObserver = () => {
  if (!highQualityImg.value) return;

  observer.value = new IntersectionObserver(
    (entries) => {
      const entry = entries[0];
      if (entry.isIntersecting) {
        // 圖片進入視口,開始載入高品質圖片
        highQualityImg.value.src = highQualityImg.value.dataset.src;
        observer.value.disconnect();
      }
    },
    { rootMargin: "200px" }
  ); // 預載邊界

  observer.value.observe(highQualityImg.value);
};

const onHighQualityLoaded = () => {
  loadedHighQuality.value = true;
};

onMounted(() => {
  if (props.useBlurHash) {
    renderBlurHash();
  }
  setupIntersectionObserver();
});

// 當 BlurHash 或 Canvas 引用變化時重新渲染
watch(() => [props.blurHash, blurHashCanvas.value], renderBlurHash);
</script>

<style scoped>
.progressive-image {
  position: relative;
  width: 100%;
  overflow: hidden;
  background-color: #f0f0f0;
}

.blur-preview,
.lqip-preview,
.high-quality {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transition: opacity 0.5s ease;
}

.blur-preview {
  filter: blur(20px);
  transform: scale(1.1); /* 彌補模糊邊緣 */
}

.lqip-preview {
  object-fit: cover;
  filter: blur(10px);
  transform: scale(1.05);
}

.high-quality {
  opacity: 0;
  object-fit: contain;
}

.high-quality.loaded {
  opacity: 1;
}

.fade-out {
  opacity: 0;
}
</style>

Astro 框架實作

在 Astro 中,可以結合動態圖片導入和上述 Vue 組件:

---
// ProgressiveImage.astro
import { Image } from 'astro:assets';
import ProgressiveImageVue from './ProgressiveImage.vue';
import { getImageAsBuffer } from '../utils/image';

const { src, alt, width, height, blurhash } = Astro.props;

// 動態導入圖片
let imageData;
if (typeof src === 'string') {
  // 遠程圖片,使用提供的 blurhash
  imageData = {
    src,
    blurhash,
    width,
    height,
    useBlurHash: true
  };
} else {
  // 本地圖片,可以使用 LQIP 或 BlurHash
  try {
    // 如果使用 vite-plugin-lqip
    const lqipData = await import(`${src.src}?lqip`);
    imageData = {
      src: src.src,
      lqipSrc: lqipData.default.lqip,
      width: lqipData.default.width,
      height: lqipData.default.height
    };
  } catch (e) {
    // 回退至標準圖片
    imageData = {
      src: src.src,
      width: width || src.width,
      height: height || src.height
    };
  }
}
---

<ProgressiveImageVue
  client:load
  src={imageData.src}
  lqipSrc={imageData.lqipSrc}
  blurHash={imageData.blurhash}
  useBlurHash={imageData.useBlurHash}
  width={imageData.width}
  height={imageData.height}
  alt={alt}
/>

使用方式:

---
// 頁面中使用
import ProgressiveImage from '../components/ProgressiveImage.astro';
import heroImage from '../assets/hero.jpg';
---

<ProgressiveImage
  src={heroImage}
  alt="文章首圖"
  blurhash="LlNA}44TN{kqyEtls:xux^tRRjRi"
/>

成效與比較

用戶體驗對比

傳統載入流程通常包括白屏佔位、突然顯示完整圖片或逐行載入,這可能導致頁面跳動,影響用戶體驗。這種方式不僅視覺效果不佳,還可能造成內容佈局的不穩定。

相比之下,漸進式載入流程提供了更優雅的解決方案。它首先立即顯示色塊或模糊預覽,確保圖片佈局穩定且無跳動,然後通過平滑過渡將預覽圖像轉換為高清圖片。這種方法不僅提高了頁面的加載速度感知,還大大改善了整體用戶體驗。

結語

透過結合 SVG、BlurHash 與 LQIP 技術,我們能夠大幅提升網站的圖片載入體驗,帶來更好的用戶體驗和更高的效能指標。這種漸進式圖片載入策略尤其適合文章首圖、產品圖庫、Banner 與輪播,以及相片牆等場景。這些場景通常涉及大量高品質圖片或對頁面載入速度有顯著影響的視覺元素,通過優化載入方式可以有效提升整體用戶體驗。

為進一步優化圖片載入體驗,我們可以考慮與 AVIF 和 WebP 等新一代圖片格式結合,利用自適應圖片 CDN 根據設備與網路條件動態提供最佳圖片,並充分運用 <picture> 元素來靈活選擇最佳圖片格式與解析度。同時,採用 HTTP/3 協議和 Preload 機制可以進一步提升網路傳輸效率和資源預載能力,為用戶帶來更快速、更流暢的瀏覽體驗。

對於使用現代前端框架的開發者,將這些技術整合到開發工作流中至關重要。Astro 用戶可以利用其內建的圖片優化功能,Vue 和 React 開發者可以封裝可重用的漸進式圖片組件,而 Vite 用戶則可以使用 vite-plugin-lqip 自動生成 LQIP。此外,將圖片處理整合到 CI/CD 流程中,可以實現預覽圖片資源的自動生成。通過這些工程化手段和工具整合,我們不僅能顯著提升網站的效能指標,還能為用戶創造更加愉悅的視覺體驗,使圖片從潛在的性能瓶頸轉變為增強用戶體驗的關鍵元素。

感謝您閱讀我的文章。歡迎隨時分享你的想法。
技術文章 前端開發 網站效能 SEO
關於 Calpa

Calpa 擅長使用 TypeScriptReact.jsVue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在 GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。

熱門文章

最新文章