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

前言

  • 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/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:使用某个颜色

  • next.config.js 中配置 placeholdercolor,然后使用 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>渐进式图片加载</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();                  // 生成低质量预览图                 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);             }         };     }, [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', // 增加过渡时间             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-[150px]" /> 

方案 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; 

总结

  • 产品第一印象很重要,良好的用户体验对于产品来说是必需的。
  • 感谢阅读,我们下次再见!
发表评论

相关文章