写点什么

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

作者:Immerse
  • 2024-11-21
    广东
  • 本文字数:5749 字

    阅读完需:约 19 分钟

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

前言

  • 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="/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.jsmodule.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;
复制代码

总结

  • 产品第一印象很重要,良好的用户体验对于产品来说是必需的。

  • 感谢阅读,我们下次再见!

发布于: 刚刚阅读数: 5
用户头像

Immerse

关注

还未添加个人签名 2021-10-17 加入

还未添加个人简介

评论

发布
暂无评论
图片渐进式加载优化实践指南_图片_Immerse_InfoQ写作社区