前言
- Hey, 我是 Immerse
- 文章首發(fā)于個(gè)人博客【https://yaolifeng.com】范抓,更多內(nèi)容請(qǐng)關(guān)注個(gè)人博客
- 轉(zhuǎn)載說(shuō)明:轉(zhuǎn)載請(qǐng)?jiān)谖恼骂^部注明原文出處及版權(quán)聲明衡载!
起因
- 最近上線了個(gè)人博客抄淑,片段頁(yè)面存在大量圖片球切,在圖片加載方面體驗(yàn)很差制妄,可以說(shuō)是斷崖式涌萤,從 0-1 完全沒(méi)有任何過(guò)渡(這很影響頁(yè)面布局和用戶體驗(yàn)坠狡,對(duì)于設(shè)定了圖片寬高的圖片還好继找,如果沒(méi)設(shè)置,就會(huì)有一個(gè)圖片撐高的過(guò)程)
巧合
- 在準(zhǔn)備寫這篇文章當(dāng)天前端南玖大佬發(fā)表了一篇文章逃沿,我直呼大數(shù)據(jù)牛逼 ????文章: 點(diǎn)擊查看
- 這篇文章我們將討論其他幾種方案婴渡,閑話少說(shuō),言歸正傳凯亮。
- 對(duì)于常規(guī)的圖片優(yōu)化這里不在贅述边臼,大致如下:
- 壓縮圖片、使用 CSS sprites假消、懶加載硼瓣、預(yù)加載、CDN 緩存置谦、合適的圖片格式堂鲤、七牛 CDN 圖片參數(shù)等等
探索
- 以下是這篇文章提到的幾種方案(因?yàn)閭€(gè)人項(xiàng)目基于 Next,所以有些示例代碼是 React)
- (1)使用圖片主色調(diào)
- (2)使用某個(gè)顏色
- (3)使用圖片的縮略圖
- (4)使用模糊 + 壓縮圖片
- (5)圖片占位符
方案 1:使用圖片主色調(diào)
- 在日常開發(fā)中媒峡,我們的圖片
src
可能是動(dòng)態(tài)的瘟栖,也就是一個(gè)字符串 string
url, 當(dāng)我們指定了 placeholder="blur"
時(shí),還必須添加 blurDataURL
屬性谅阿,
import Image from 'next/image';
// Pixel GIF code adapted from https://stackoverflow.com/a/33919020/266535
const keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const triplet = (e1: number, e2: number, e3: number) =>
keyStr.charAt(e1 >> 2) +
keyStr.charAt(((e1 & 3) << 4) | (e2 >> 4)) +
keyStr.charAt(((e2 & 15) << 2) | (e3 >> 6)) +
keyStr.charAt(e3 & 63);
const rgbDataURL = (r: number, g: number, b: number) =>
`data:image/gif;base64,R0lGODlhAQABAPAA${
triplet(0, r, g) + triplet(b, 255, 255)
}/yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==`;
const Color = () => (
<div>
<h1>Image Component With Color Data URL</h1>
<Image
alt="Dog"
src="/dog.jpg"
placeholder="blur"
blurDataURL={rgbDataURL(237, 181, 6)}
width={750}
height={1000}
style={{
maxWidth: '100%',
height: 'auto'
}}
/>
<Image
alt="Cat"
src="/cat.jpg"
placeholder="blur"
blurDataURL={rgbDataURL(2, 129, 210)}
width={750}
height={1000}
style={{
maxWidth: '100%',
height: 'auto'
}}
/>
</div>
);
export default Color;
方案 2:使用某個(gè)顏色
- 在
next.config.js
中配置 placeholder
為 color
半哟,然后使用 backgroundColor
屬性
// next.config.js
module.exports = {
images: {
placeholder: 'color',
backgroundColor: '#121212'
}
};
// 使用
<Image src="/path/to/image.jpg" alt="image title" width={500} height={500} placeholder="color" />
方案 3: 使用圖片的縮略圖
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>漸進(jìn)式圖片加載</title>
<style>
.placeholder {
background-color: #f6f6f6;
background-size: cover;
background-repeat: no-repeat;
position: relative;
overflow: hidden;
}
.placeholder img {
position: absolute;
opacity: 0;
top: 0;
left: 0;
width: 100%;
transition: opacity 1s linear;
}
.placeholder img.loaded {
opacity: 1;
}
.img-small {
filter: blur(50px);
transform: scale(1);
}
</style>
</head>
<body>
<div
class="placeholder"
data-large="https://qncdn.mopic.mozigu.net/work/143/24/42b204ae3ade4f38/1_sg-uLNm73whmdOgKlrQdZA.jpg"
>
<img
src="https://qncdn.mopic.mozigu.net/work/143/24/5307e9778a944f93/1_sg-uLNm73whmdOgKlrQdZA.jpg"
class="img-small"
/>
<div style="padding-bottom: 66.6%"></div>
</div>
</body>
</html>
<script>
window.onload = function () {
var placeholder = document.querySelector('.placeholder'),
small = placeholder.querySelector('.img-small');
// 1. 顯示小圖并加載
var img = new Image();
img.src = small.src;
img.onload = function () {
small.classList.add('loaded');
};
// 2. 加載大圖
var imgLarge = new Image();
imgLarge.src = placeholder.dataset.large;
imgLarge.onload = function () {
imgLarge.classList.add('loaded');
};
placeholder.appendChild(imgLarge);
};
</script>
方案 4:使用模糊+壓縮圖片
// progressive-image.tsx
'use client';
import React, { useState, useEffect } from 'react';
import imageCompression from 'browser-image-compression';
interface ProgressiveImageProps {
src: string;
alt?: string;
width?: number;
height?: number;
layout?: 'fixed' | 'responsive' | 'fill' | 'intrinsic';
className?: string;
style?: React.CSSProperties;
}
export const ProgressiveImage: React.FC<ProgressiveImageProps> = ({
src,
alt = '',
width,
height,
layout = 'responsive',
className = '',
style = {}
}) => {
const [currentSrc, setCurrentSrc] = useState<string>(src);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [blurLevel, setBlurLevel] = useState<number>(20);
useEffect(() => {
let isMounted = true;
const loadImage = async () => {
try {
// 加載并壓縮原始圖片
const response = await fetch(src);
const blob = await response.blob();
// 生成低質(zhì)量預(yù)覽圖
const tinyOptions = {
maxSizeMB: 0.0002,
maxWidthOrHeight: 16,
useWebWorker: true,
initialQuality: 0.1
};
const tinyBlob = await imageCompression(blob, tinyOptions);
if (isMounted) {
const tinyUrl = URL.createObjectURL(tinyBlob);
setCurrentSrc(tinyUrl);
// 開始逐漸減小模糊度
startSmoothTransition();
}
// 加載原始圖片
const highQualityImage = new Image();
highQualityImage.src = src;
highQualityImage.onload = () => {
if (isMounted) {
setCurrentSrc(src);
// 當(dāng)高質(zhì)量圖片加載完成時(shí),繼續(xù)平滑過(guò)渡
setTimeout(() => {
setIsLoading(false);
}, 100);
}
};
} catch (error) {
console.error('Error loading image:', error);
if (isMounted) {
setCurrentSrc(src);
setIsLoading(false);
}
}
};
const startSmoothTransition = () => {
// 從20px的模糊逐漸過(guò)渡到10px
const startBlur = 20;
const endBlur = 10;
const duration = 1000; // 1秒
const steps = 20;
const stepDuration = duration / steps;
const blurStep = (startBlur - endBlur) / steps;
let currentStep = 0;
const interval = setInterval(() => {
if (currentStep < steps && isMounted) {
setBlurLevel(startBlur - blurStep * currentStep);
currentStep++;
} else {
clearInterval(interval);
}
}, stepDuration);
};
setIsLoading(true);
setBlurLevel(20);
loadImage();
return () => {
isMounted = false;
if (currentSrc && currentSrc.startsWith('blob:')) {
URL.revokeObjectURL(currentSrc);
}
};
}, [src]);
const getContainerStyle = (): React.CSSProperties => {
const baseStyle: React.CSSProperties = {
position: 'relative',
overflow: 'hidden'
};
switch (layout) {
case 'responsive':
return {
...baseStyle,
maxWidth: width || '100%',
width: '100%'
};
case 'fixed':
return {
...baseStyle,
width: width,
height: height
};
case 'fill':
return {
...baseStyle,
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0
};
case 'intrinsic':
return {
...baseStyle,
maxWidth: width,
width: '100%'
};
default:
return baseStyle;
}
};
const getImageStyle = (): React.CSSProperties => {
const baseStyle: React.CSSProperties = {
filter: isLoading ? `blur(${blurLevel}px)` : 'none',
transition: 'filter 0.8s ease-in-out', // 增加過(guò)渡時(shí)間
transform: 'scale(1.1)', // 稍微放大防止模糊時(shí)出現(xiàn)邊緣
...style
};
switch (layout) {
case 'responsive':
return {
...baseStyle,
width: '100%',
height: 'auto',
display: 'block'
};
case 'fixed':
return {
...baseStyle,
width: width,
height: height
};
case 'fill':
return {
...baseStyle,
width: '100%',
height: '100%',
objectFit: 'cover'
};
case 'intrinsic':
return {
...baseStyle,
width: '100%',
height: 'auto'
};
default:
return baseStyle;
}
};
return (
<div className={`${className}`} style={getContainerStyle()}>
{currentSrc && <img src={currentSrc} alt={alt} style={getImageStyle()} />}
</div>
);
};
// 使用
<ProgressiveImage
src={photo}
alt={short.title}
width={300}
height={250}
layout="responsive"
className="h-full min-h-[150px]"
/>
方案 5:圖片占位符
- Next.js 的 next/image 組件
placeholder
屬性提供了個(gè)選項(xiàng) blur
签餐,默認(rèn)為 empty
-
blur
會(huì)生成一個(gè)模糊的預(yù)覽圖像(但這個(gè)選項(xiàng)會(huì)增加初始加載實(shí)踐寓涨,因?yàn)樾枰獣r(shí)間去生成模糊圖片)
- 注意:如果
placeholder="blur"
時(shí),必須使用 import
靜態(tài)引入圖片的方式氯檐,這樣 Next.js 才會(huì)對(duì)圖片進(jìn)行漸進(jìn)式加載的預(yù)處理
import Image from 'next/image';
import mountains from '/public/mountains.jpg';
const PlaceholderBlur = () => (
<div>
<h1>Image Component With Placeholder Blur</h1>
<Image
alt="Mountains"
src={mountains}
placeholder="blur"
width={700}
height={475}
style={{
maxWidth: '100%',
height: 'auto'
}}
/>
</div>
);
export default PlaceholderBlur;
總結(jié)
- 產(chǎn)品第一印象很重要戒良,良好的用戶體驗(yàn)對(duì)于產(chǎn)品來(lái)說(shuō)是必需的。
- 感謝閱讀冠摄,我們下次再見糯崎!