用 LQIP 讓圖片載入不再跳來跳去
LQIP(Low Quality Image Placeholder)透過在上傳時預先產生 base64 模糊圖與 thumbnail,讓前台在圖片載入的每個階段都有東西可以顯示:進場先顯示 blurDataUrl、thumbnail 載入後替換、點擊開啟 Lightbox 時再從 thumbnail 過渡到原圖。整個過程沒有空白畫面,也不會有版面跳動。
瀏覽有大量圖片的網頁時,應該都遇過這種情況:圖片還沒載入完成,畫面就一直跳動,文字被推來推去,或是圖片從上到下一條一條慢慢出現。這些體驗都不太好,尤其在網速較慢的環境下會更明顯。
LQIP(Low Quality Image Placeholder)是一種常見的圖片載入優化技術,核心概念很簡單:在原始圖片還沒下載完成之前,先用一張極小的低畫質圖片當作佔位,等實際要顯示的圖載入完成後再替換上去。
為什麼需要 LQIP?
如果只是把 <img> 丟上去什麼都不處理,會遇到兩個問題:
- 版面跳動(Layout Shift):圖片載入前沒有預留空間,載入後把其他內容擠開,造成 CLS(Cumulative Layout Shift)分數變差
- 載入體驗差:圖片從空白到完整顯示的過程中,要嘛是一片空白,要嘛是一條一條慢慢出現,使用者會覺得頁面還在「壞掉」的狀態
LQIP 同時解決了這兩個問題:透過預先知道圖片的寬高比來預留空間,再用一張模糊的縮圖做視覺過渡,整個載入過程會流暢很多。
我的做法
我在自己的照片牆專案 被光保存的瞬間 中實作了這個技術。這個專案會一次顯示大量照片,每張照片又有高解析度的原圖,很適合拿來做這種優化。
整體流程分成三個階段:上傳時預處理、照片牆渲染、Lightbox 原圖載入。
上傳時預處理
在圖片上傳到後端時,我會做三件事:
- 記錄原圖的寬高:用來計算 aspect ratio,讓前台在任何圖片載入前就能預留正確的空間
- 產生 blurDataUrl:把圖片壓縮成極小尺寸(寬度大概 20~32px),轉成 base64 字串直接存進資料庫
- 產生 thumbnail:壓縮成適合在照片牆上瀏覽的中等尺寸縮圖,上傳到儲存空間
// 概念上的處理流程const metadata = await sharp(imageBuffer).metadata()const { width, height } = metadata
// 產生 base64 模糊圖const blurBuffer = await sharp(imageBuffer) .resize(32) .blur(1) .toBuffer()const blurDataUrl = `data:image/jpeg;base64,${blurBuffer.toString('base64')}`
// 產生 thumbnailconst thumbnailBuffer = await sharp(imageBuffer) .resize(480) .toBuffer()
// 儲存:原圖 URL、thumbnail URL、blurDataUrl、寬高blurDataUrl 是 base64,可以直接跟著 API 回應一起回來,不需要額外的 HTTP 請求。一張 32px 寬的模糊圖通常只有幾百 bytes,對回應大小幾乎沒有影響。
照片牆渲染:blurDataUrl → thumbnail
頁面載入後,每張照片的載入會經過兩個階段:
- 立即顯示 blurDataUrl:因為是 base64,不需要任何網路請求,頁面一渲染就能看到模糊的佔位圖,同時 aspect ratio 也已經是正確的
- thumbnail 載入完成後替換:瀏覽器在背景下載 thumbnail,下載完成後替換掉模糊圖
<template> <div class="image-wrapper" :style="{ aspectRatio: `${image.width} / ${image.height}` }" @click="openLightbox(image)" > <!-- 底層:blurDataUrl 作為佔位 --> <img :src="image.blurDataUrl" class="blur-placeholder" />
<!-- 上層:thumbnail 載入完成後顯示 --> <img :src="image.thumbnailUrl" :class="{ loaded: thumbnailLoaded }" class="thumbnail" @load="thumbnailLoaded = true" /> </div></template>.image-wrapper { position: relative; overflow: hidden; cursor: pointer;}
.blur-placeholder,.thumbnail { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;}
.thumbnail { opacity: 0; transition: opacity 0.3s ease;}
.thumbnail.loaded { opacity: 1;}使用者看到的效果是:頁面一打開,所有照片的位置就已經排好,每張都是模糊但有輪廓的樣子。接著 thumbnail 陸續載入完成,一張一張從模糊變清晰,整個過程很順暢,沒有版面跳動。
Lightbox 原圖載入:thumbnail → 原圖
使用者在照片牆上點擊某張照片後,會打開 Lightbox 來查看原始解析度的大圖。這時候又是一次從小圖到大圖的載入過程,一樣可以套用同樣的策略:
- 先顯示 thumbnail:因為 thumbnail 已經在照片牆階段載入過了,瀏覽器有快取,可以瞬間顯示
- 原圖載入完成後替換:高解析度原圖下載完成後,替換掉 thumbnail
<!-- Lightbox --><div class="lightbox" v-if="currentImage"> <img :src="currentImage.thumbnailUrl" class="lightbox-placeholder" /> <img :src="currentImage.originalUrl" :class="{ loaded: originalLoaded }" class="lightbox-original" @load="originalLoaded = true" /></div>因為 Lightbox 中的 thumbnail 已經被快取過,使用者點開後會先看到一張稍微模糊但完整的圖,而不是一片空白等原圖慢慢載入。原圖載入完成後再無縫切換成高解析度版本。
三層載入的完整流程
整理一下使用者實際看到的體驗:
| 階段 | 顯示內容 | 來源 | 等待時間 |
|---|---|---|---|
| 頁面載入 | 模糊佔位圖 | blurDataUrl(base64) | 無,瞬間顯示 |
| thumbnail 載入完成 | 清晰縮圖 | thumbnail URL | 視網速而定 |
| 點擊照片,原圖載入完成 | 高解析度原圖 | 原圖 URL | 視網速與圖片大小而定 |
每一層都不會出現空白或版面跳動,使用者在任何時刻都能看到「有東西」的畫面。
小結
LQIP 的實作不複雜,核心就是讓每個階段都有東西可以顯示:
- 上傳時:記錄寬高、產生 base64 模糊圖(blurDataUrl)、產生 thumbnail
- 照片牆:先顯示 blurDataUrl,thumbnail 載入後替換
- Lightbox:先顯示已快取的 thumbnail,原圖載入後替換
在圖片量大或原圖解析度高的場景下,這種分層載入的效果會特別明顯。與其讓使用者盯著空白方塊等圖片慢慢冒出來,不如在每個階段都給他一個「夠用」的畫面。
相關文章
用 Slidev 把 Markdown 變成簡報網站
用 Slidev 把 Markdown 變成簡報網站,從建立專案、自訂樣式到部署上線的完整流程與踩坑紀錄。
把模糊想法變成可執行工作:我的 Supekku Workflow 設計
AI Agent 協作的問題不只是 prompt,而是脈絡如何被保存。Supekku Workflow 是我用來把模糊想法整理成可執行工作的方式:先透過對話釐清意圖、範圍與成功標準,再把需要保存的內容轉成 proposal、criteria、reasoning、actions 與 verification。
AI 時代的個人知識庫:Obsidian、RAG 與 Agent 該怎麼分工?
Obsidian、RAG 與 AI Agent 常被放在一起討論,但它們其實負責不同任務。本文從個人知識管理的角度,拆解 Obsidian 如何保存筆記、RAG 如何檢索資料、Agent 如何把內容轉成可執行的工作流。
如何自訂 Kobo 電子書閱讀器的待機畫面
告別單調的預設書封!只要簡單幾步驟,就能把喜歡的圖片變成 Kobo 閱讀器的待機桌布。本文提供各機型解析度對照表與 Mac/Windows 顯示隱藏資料夾教學,讓你每次合上閱讀器都能看到最愛的風景。
如何在 Kobo 電子書閱讀器安裝自訂字型
身為 Kobo 使用者,一定要學會的自訂字型小技巧!內建字型看膩了?跟著這份筆記,把帶有手寫筆觸的「芫荽」字型裝進閱讀器裡。簡單、免費且不傷效能,讓你的每一本電子書都像精心排版過的實體書一樣順眼。
