本文主要思考分析如何在 Web 页面里优化图片加载
策略一:图片压缩
从根本上解决就是提前对图片进行压缩,这是我对【市面上可视化的图像压缩工具】的调研结果:
平台/工具 | 类型 | 支持的图像格式 | 上传的单张图像限制大小 | 批量限制数 | 是否支持修改图像质量 |
---|---|---|---|---|---|
Tinypng | Web | JEPG, PNG, |
5M(付费版 75 M) | 20 张(付费版无限制) | 默认最佳质量 |
Tinypng 桌面端 - 破解版 | Windows、Mac | JEPG, PNG, |
5M | 无限制 | 默认最佳质量 |
shortpixel | Web | JEPG, PNG, GIF, |
10 M(登录后 100 M) | 50 张 | 默认最佳质量 |
Compressor.io | Web | JEPG, PNG, GIF, WEBP, SVG | 10M(付费版 20 M) | 10 张(付费版无限制) | 默认最佳质量(付费版支持) |
iloveimg | Web | JEPG, PNG, GIF, |
5M | 30 张 | 默认最佳质量 |
imageresizer | Web | JEPG, PNG, |
100 M | 50 张 | 是 |
Caesium | Windows、Mac | JEPG, PNG, |
无限制 | 无限制 | 是 |
智图 | Windows、Mac | JEPG, PNG, GIF, WEBP | 无限制 | 无限制 | 是 |
Win10 照片查看器 | Windows | JEPG, PNG, |
无限制 | 1 张 | 是 |
实测压缩效果
工具 | JPG 图压缩后 (原 1.36MB) | PNG 图压缩后 (原 369 KB) | GIF 动图压缩后 (原 1.14 MB) |
---|---|---|---|
Tinypng | 231 KB | 70.6 KB | - |
shortpixel | 198 KB | 58.7 KB | 472 KB |
Compressor.io | 177 KB | 65 KB | 601 KB |
iloveimg | 361 KB | 69 KB | 658 KB |
imageresizer | 270 KB | 47.8 KB | - |
Caesium | 236 KB | 59.1 KB | - |
智图 | 183 KB | 48 KB | - |
Win10 照片查看器 | 272 KB | 292KB | - |
更多工具及实测内容参考我另一篇文章:前端性能优化系列 - 图片压缩篇
策略二:响应式图片
根据设备分辨率提供不同尺寸的图片,例如在 4K 屏上,需要高分辨率的,但如果是像手机这样的小屏幕就只需要低分辨率的图片即可
需求:
- 屏幕宽度 > 640 px,显示:large-1920w.jpg 高分辨率图(1.36 MB)
- 屏幕宽度 ≤ 640 px,显示:normal-960w.jpg 普通图(148 KB)
- 默认显示:normal-960w.jpg(出于兼容问题的考虑得加入默认图)
原生实现
使用 <picture>
元素(推荐)
兼容性:
<picture>
元素可以让你根据不同的设备分辨率、窗口大小或者设备像素比来加载不同的图片。它需要搭配一个或多个 <source>
元素组合使用,每个 <source>
可以为一个特定的分辨率提供不同的图片。
<picture>
按照自上而下 的<source>
标签里的图片资源按序加载。一旦找到一个匹配的媒体查询,浏览器会立即停止继续查找,并加载对应 srcset 中的图像
代码:
html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
}
img {
width: 100%;
height: auto;
}
</style>
</head>
<body>
<picture>
<source media="(min-width: 641px)" srcset="./assets/large-1920w.jpg" />
<source media="(max-width: 640px)" srcset="./assets/normal-960w.jpg" />
<img src="./assets/normal-960w.jpg" />
</picture>
</body>
</html>
效果:
使用 srcset
+ sizes
属性
兼容性:
参考文档:响应式图像教程
srcset
属性列出多张可用的图像,每张图像的 URL 后面加上宽度描述符,空格隔开
宽度描述符就是图像原始的宽度(单位 px),加上字符w
。如:./assets/normal-960w.jpg 960w
sizes
属性列出不同设备的图像显示宽度
sizes
属性的值是一个逗号分隔的字符串,除了最后一部分,前面每个部分都是一个放在括号里面的媒体查询表达式,后面是一个空格,再加上图像的显示宽度
例如想设置图像在宽度不超过640px的设备显示宽度为100vw,写作:(max-width: 640px) 100vw
- 浏览器根据当前设备的宽度,从
sizes
属性获得图像的显示宽度,然后从srcset
属性找出最接近该宽度的图像,进行加载
假定当前设备的屏幕宽度是 425px,浏览器从sizes
属性查询得到,图片的显示宽度是 100vw,即 425px。srcset
属性里面,最接近此宽度为 960px 的图片,于是加载 normal-960w.jpg
注意,sizes
属性必须与srcset
属性搭配使用。单独使用sizes
属性是无效的
代码:
html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<img
srcset="./assets/normal-960w.jpg 960w, ./assets/large-1920w.jpg 1920w"
sizes="(max-width: 640px) 100vw, 100vw"
src="./assets/normal-960w.jpg"
/>
</body>
</html>
此方法不推荐使用,原因是:
- 媒体查询条件的判定不准确,例如代码里的 640 px 跟边界值不准确(也可能是我代码有问题或实测方法有问题,欢迎在评论区指出)
- 屏幕视口尺寸发生变化时不会自动切换图片
vue3 / React 实现
实现思路:
- 先写一个 img 元素,通过变量
imageSrc
传入 - 通过
window.innerWidth
获取设备屏幕宽度(单位:px) - 添加屏幕尺寸变化的监听器,当屏幕尺寸发生变化时调用改变变量
imageSrc
的函数(主要针对移动端横竖屏切换)
vue3 版:
html
<template>
<img :src="imageSrc" />
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
const imageSrc = ref('');
const updateImageSrc = () => {
imageSrc.value = window.innerWidth <= 1200 ? './assets/small.jpg' : './assets/large.jpg';
};
onMounted(() => {
window.addEventListener('resize', updateImageSrc);
updateImageSrc();
});
onUnmounted(() => {
window.removeEventListener('resize', updateImageSrc);
});
return {
imageSrc,
};
},
};
</script>
React 版
ts
import React, { useState, useEffect } from 'react';
const ResponsiveImage = () => {
const [imageSrc, setImageSrc] = useState('');
const updateImageSrc = () => {
setImageSrc(window.innerWidth <= 1200 ? './assets/small.jpg' : './assets/large.jpg');
};
useEffect(() => {
window.addEventListener('resize', updateImageSrc);
updateImageSrc();
return () => {
window.removeEventListener('resize', updateImageSrc);
};
}, []);
return <img src={imageSrc} />;
};
export default ResponsiveImage;
策略三:图片懒加载
图片懒加载:只在图片出现 在视口区域时才进行加载
原生实现
img 标签的 loading
属性
参考 img 标签之 loading 属性 - MDN 提供的信息:
兼容性:
html
<img src="image.jpg" loading="lazy">
使用开源组件库
React Lazy Load Image 组件
React Lazy Load Image 组件(React 框架):支持图片懒加载以及自定义加载占位内容
- 安装
js
// NPM
$ npm i --save react-lazy-load-image-component
// Yarn
$ yarn add react-lazy-load-image-component
- 引入
ts
import Image from "../images/bird.jpg"; // 图片资源
import { LazyLoadImage } from "react-lazy-load-image-component";
- 使用
ts
import React from "react";
import Image from "../images/bird.jpg";
import { LazyLoadImage } from "react-lazy-load-image-component";
export default function App() {
return (
<div>
<LazyLoadImage src={Image}
width={600} height={400}
alt="Image Alt"
/>
</div>
);
}
注意需要明确定义图片的宽和高,以避免累积布局偏移(CLS)问题
- 实践测试
传送门:Live 演示地址(来自文章如何在 React 中实现图片懒加载)
打开谷歌浏览器控制台,停用缓存、网络设置为高速 3G
设置完后刷新页面,往下滑:
Element Plus 的 Image 组件
如果你的 vue 项目有引入 element plus,推荐采用此图片组件来做图片懒加载
Element Plus 的 Image 组件(vue 框架):图片容器,在保留所有原生 img 的特性下,支持懒加载,自定义占位、加载失败等
传送门:Live 演示地址(Element Plus Playground)
使用的时候只需要在 el-image
组件设置 lazy
属性为 true 即可
html
<template>
<div class="demo-image__lazy">
<el-image v-for="url in urls" :key="url" :src="url" lazy />
</div>
</template>
<script lang="ts" setup>
const urls = [
'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg',
]
</script>
<style scoped>
.demo-image__lazy {
height: 400px;
overflow-y: auto;
}
.demo-image__lazy .el-image {
display: block;
min-height: 200px;
margin-bottom: 10px;
}
.demo-image__lazy .el-image:last-child {
margin-bottom: 0;
}
</style>
轻量级懒加载 JS 库
如果你的项目没有或不想使用体积较大的开源组件,那么推荐你采用能实现图片懒加载的轻量级 JS 库,例如:Lazy Sizes、Lozad.js 等(传送门:分享九个 JavaScript 图片懒加载库 - 掘金)
策略四:加载用低分辨率图占位
原生实现
使用原生 API onload
监听高分辨率图片加载完成,替换占位图片
html
<!DOCTYPE html>
<html>
<head>
<style>
img {
width: 100%;
height: auto;
}
</style>
</head>
<body>
<picture>
<source srcset="./assets/high-res.jpg" media="(min-width: 300px)">
<img src="./assets/low-res-blurred.jpg">
</picture>
<script>
const highResSource = document.querySelector('source');
const imgElement = document.querySelector('img');
// 加载高分辨率图片
const highResImage = new Image();
highResImage.src = highResSource.srcset;
highResImage.onload = () => {
// 替换低分辨率图片为高分辨率图片
imgElement.src = highResImage.src;
};
</script>
</body>
</html>
开源组件库
React Lazy Load Image 组件
React Lazy Load Image 组件(React 框架):支持图片懒加载以及自定义加载占位内容
安装、引入 相关步骤上面提到过了,此处不赘述,使用上加入低分辨率图作为加载中的占位,可以参考 前端性能优化系列 - 图片压缩篇 介绍的可控制压缩质量的工具
如采用 squoosh 图片压缩工具对原图进行压缩:原图大小为 286KB
,压缩后的低分辨率图大小为2.5KB
使用占位符图片需要在图片添加一个 PlaceholderSrc
属性,并包含图片的值。
ts
import { LazyLoadImage } from 'react-lazy-load-image-component';
import Image from "../images/bird.jpg";
import PlaceholderImage from "../images/placeholder.jpg";
<LazyLoadImage
src={Image}
PlaceholderSrc={PlaceholderImage}
width={600}
height={400}
/>
传送门:Live 演示地址
同上打开谷歌浏览器控制台,停用缓存、网络设置为高速 3G
大大减少了首屏加载资源的大小,从而大大提升首屏加载速度
Ant Design 的 Image 组件
Ant Design 的 Image 组件(React 框架)
typescript
<Image
width={200}
src="高分辨率原图链接"
placeholder={
<Image
preview={false}
src="低分辨率图链接"
width={200}
/>
}
/>
Ant Design 的 Image 组件也可能实现这样的渐进式加载,将高分辨率原图(1.4MB)直接放在 src
字段里,同时借助 placeholder
字段设置加载占位放置提前压缩好的低分辨率图(52.5KB)
策略五:可视区域渲染
懒加载是等用户滑动到对应区域时才去加载图片,其实还可以做得更彻底,那就是没出现在可视区域的内容,直接不渲染了
默认情况下,浏览器会渲染所有内容 ,但为了实现更快的初始加载时间和更高的滚动性能。可以content-visibility
属性来控制元素的渲染时机
visible
:默认值,元素及其子元素按照正常的渲染模式进行渲染hidden
:元素及其子元素不会被渲染。这类似于display: none
,但与visibility: hidden
不同,因为它不会保留元素的布局空间auto
:浏览器会自动确定元素及其子元素是否应该被渲染。这通常意味着,如果元素在屏幕外(即在当前视口之外),则浏览器会跳过渲染该元素及其子元素,从而节省渲染时间。当元素进入视口时,浏览器会自动渲染它
兼容性:(75.29%,兼容性较差,需融合其他策略)
content-visibility
属性的主要优点是它可以显著提高网页的性能,特别是对于包含大量内容和元素的网页。通过将 content-visibility
设置为 auto
,可以让浏览器仅在需要时才渲染元素,从而减少页面加载时间和提高滚动性能
策略六:异步解码
图片传输到浏览器上要想展示,还需要经过解码。除了硬件本身外,如何解码直接影响着图片展示在用户眼前的时机
img
标签的 decoding
属性用于指定浏览器图片解码方式:
sync
:同步解码。浏览器在主线程 上立即解码图片 。这可能会阻塞其他任务 ,例如页面布局和绘制,直到图片解码完成。如果你有一个小的图片,或者图片在视觉上非常重要,选择这个值。async
:异步解码。浏览器在后台线程 上解码图片,这样就不会阻塞 其他任务。这对于大型图片或者不那么重要 的图片比较有用,因为它们可能需要更长的时间来解码,而且不会影响到页面的其他部分。auto
:自动解码。默认值,让浏览器自己决定如何进行解码。浏览器可能会根据图片的大小 、当前的 CPU 使用情况等因素来决定使用同步还是异步解码。
html
<img src="example.jpg" decoding="async">
不过,跟延迟加载一样,异步解码需谨慎使用。因为它可能会导致
FOUC
(Flash of Unstyled Content )意为"无样式内容闪烁"。这是指在网页加载过程中,内容在应用样式之前短暂地显示出来,导致页面内容闪烁或跳动的现象。这种情况通常发生在 CSS 样式尚未加载完成时 ,浏览器已经开始渲染页面内容。