mmmm123 发表于 2025-2-7 02:47:20

图片渐进式加载优化实践指南

前言


[*]Hey, 我是 Immerse
[*]文章首发于个人博客【https://yaolifeng.com】,更多内容请关注个人博客
[*]转载说明:转载请在文章头部注明原文出处及版权声明!
起因


[*]最近上线了个人博客,片段页面存在大量图片,在图片加载方面体验很差,可以说是断崖式,从 0-1 完全没有任何过渡(这很影响页面布局和用户体验,对于设定了图片宽高的图片还好,如果没设置,就会有一个图片撑高的过程)
巧合


[*]在准备写这篇文章当天前端南玖大佬发表了一篇文章,我直呼大数据牛逼 👍🏻文章: 点击查看
[*]这篇文章我们将讨论其他几种方案,闲话少说,言归正传。

[*]对于常规的图片优化这里不在赘述,大致如下:

[*]压缩图片、使用 CSS sprites、懒加载、预加载、CDN 缓存、合适的图片格式、七牛 CDN 图片参数等等


探索


[*]以下是这篇文章提到的几种方案(因为个人项目基于 Next,所以有些示例代码是 React)

[*](1)使用图片主色调
[*](2)使用某个颜色
[*](3)使用图片的缩略图
[*](4)使用模糊 + 压缩图片
[*](5)图片占位符

方案 1:使用图片主色调


[*]在日常开发中,我们的图片 src 可能是动态的,也就是一个字符串 string url, 当我们指定了 placeholder="blur" 时,还必须添加 blurDataURL 属性,
import Image from 'next/image';// Pixel GIF code adapted from https://stackoverflow.com/a/33919020/266535const 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="https://www.cnblogs.com/dog.jpg"            placeholder="blur"            blurDataURL={rgbDataURL(237, 181, 6)}            width={750}            height={1000}            style={{                maxWidth: '100%',                height: 'auto'            }}      />      <Image            alt="Cat"            src="https://www.cnblogs.com/cat.jpg"            placeholder="blur"            blurDataURL={rgbDataURL(2, 129, 210)}            width={750}            height={1000}            style={{                maxWidth: '100%',                height: 'auto'            }}      />    </div>);export default Color;方案 2:使用某个颜色


[*]在 next.config.js 中配置 placeholder 为 color,然后使用 backgroundColor 属性
// next.config.jsmodule.exports = {    images: {      placeholder: 'color',      backgroundColor: '#121212'    }};// 使用<Image src="https://www.cnblogs.com/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>渐进式图片加载</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 = useState<string>(src);    const = useState<boolean>(true);    const = useState<number>(20);    useEffect(() => {      let isMounted = true;      const loadImage = async () => {            try {                // 加载并压缩原始图片                const response = await fetch(src);                const blob = await response.blob();                // 生成低质量预览图                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);                        // 当高质量图片加载完成时,继续平滑过渡                        setTimeout(() => {                            setIsLoading(false);                        }, 100);                  }                };            } catch (error) {                console.error('Error loading image:', error);                if (isMounted) {                  setCurrentSrc(src);                  setIsLoading(false);                }            }      };      const startSmoothTransition = () => {            // 从20px的模糊逐渐过渡到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);            }      };    }, );    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', // 增加过渡时间            transform: 'scale(1.1)', // 稍微放大防止模糊时出现边缘            ...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-"/>方案 5:图片占位符


[*]Next.js 的 next/image 组件 placeholder 属性提供了个选项 blur,默认为 empty

[*]blur 会生成一个模糊的预览图像(但这个选项会增加初始加载实践,因为需要时间去生成模糊图片)
[*]注意:如果 placeholder="blur" 时,必须使用 import 静态引入图片的方式,这样 Next.js 才会对图片进行渐进式加载的预处理

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;总结


[*]产品第一印象很重要,良好的用户体验对于产品来说是必需的。
[*]感谢阅读,我们下次再见!
页: [1]
查看完整版本: 图片渐进式加载优化实践指南