跳到主要内容位置

图片性能优化

· 预计阅读时间:15分钟
hec9527

在前端页面中经常使用到各式各样的图片,这些图片在美化了页面的同时,也使得页面加载时间边长,带宽消耗增加,如果盲目在页面中堆砌大量图片,不仅会导致页面性能下降,移动端的用户也打呼流量遭不住!

图片格式

在聊图片性能优化之前,我们先看看在业务中我们可能会用的图片格式。目前主流的图片格式无非就是 '.jpg'、'.png'、'.gif'。这些图片格式各有特点,但是都有自己无法弥补的短板。除了这些“传统图片格式”外,还应该关注几个比较“新”的图片格式。

WebP

webp是谷歌的亲儿子,文件后缀为.webp,2010年9月发布的图片格式,要说这种格式比较新,确实很牵强,但是很多coder还是没有用过它,不过webp确是一个非常优秀的图片格式,它的有损压缩压缩比例相较于jpg更加优秀,生成的图片更小,在网络传输中可以减少传输时间。另外webp支持alpha通道可以显示透明、半透明图像,支持动画类似于gif,支持有损、无损压缩。webp的优势在于有损压缩,无损压缩性能和表现一般。

相较于jpg,webp的编码速度慢10倍,解码慢1.5倍。虽然会增加解码时间,但是由于文件体积大幅减小,缩短了网络传输时间,实际表现是优于jpg的

webp兼容性(截止2023-05-10)

JPEG XL

JPEG XL是由JPEG标准制定的组织于2021年发布的,旨在成员JPEG的替代品,其扩展格式为.jxl。JPEG XL同样支持有损、无损压缩,支持jpeg无损转码,jpeg无损转码可减少20%文件体积,另外JPEG XL也支持渐进式解码

JPEG XL 兼容性(截止2023-05-10)

信息

就技术而言,jpeg xl是非常优秀的,但是兼容性是硬伤(这么久了,我在caniuse上就没见过0%兼容性的特性),之前Chrome91到Chrome109版本中,已经开始实验性支持.jxl格式,但是在Chrome110中又不支持了,理由是:人们对jpeg xl没多大兴趣

AVIF

AVIF是由开放媒体联盟(推出H264、AV1视频编码格式那个开放媒体联盟)于2019年发布的一种最新图片格式,该格式基于AV1视频解码器,源自视频帧。所以它天然的支持动画,另外还支持alpha通道,支持有损、无损压缩并且其性能也优于webp,略逊于jpeg,扩展后缀为.avif

AVIF 图片兼容性(截止2023-05-10)

下面通过一个表格,简单总结一下这几种图片格式在各个方面的对比。通过这个表格可以明显的发现,新生代图片都支持动画、alpha通道、也都支持有损、无损压缩,最大的差异体现在设备兼容性上

图片类型Alpha通道动画编解码性能压缩算法颜色支持兼容性
GIF支持支持较高无损压缩索引色(256)所有
PNG支持不支持较高无损压缩索引色(256)/直接色所有
JPEG不支持不支持较高有损压缩直接色所有
WebP支持支持较差有/无损压缩直接色高版本浏览器
JPEG XL支持支持渐进式解码有/无损压缩直接色部分高版浏览器本开启实验功能
AVIF支持支持一般有/无损压缩直接色高版本浏览器

另外一张jpeg图片,通过工具转格式后可以明显发现,从文件体积上 avif < webp < jpeg < jxl < gif < png

从兼容性上看 webp > avif > jxl。从下面这种图也能看出来, jxl图片不仅浏览器不支持,mac也不支持。

这些图片是通过node工具 sharp 由jpeg文件生成的,jxl是使用在线工具生成的

图片格式选用

新生态图片格式压缩性能和支持的特性都非常棒,但是兼容性不是特别好,不能贸然在生产环境中使用。如果在项目中使用了不支持的图片格式,轻则毒火攻心,重则七窍流血,好吧开玩笑的,最多导致图片无法识别显示空白而已。但是图片无法正常展示,这确实是一个严重的事故了,毕竟会影响到用户使用。我考(参)察(考)了一些大厂的方法,总结了两种应用方式,分别是前端兼容处理和后端兼容处理

前端兼容处理

虽然新生代图片浏览器兼容性参差不齐,但是浏览器对H5的特性支持的还是挺好的,在H5中有一个新增元素 picture,它可以包含一个或者多个 source 标签,和一个 img 标签来为不同设备提供不同的图片,浏览器会选择最匹配的 source 元素,如果没有则使用 img 中的图片,这有点类似于js中的 switch case default 语法。下面是一个简单的前端兼容案例

<picture>
<source srcset="static/image.jxl" type="image/jxl"/>
<source srcset="static/image.avif" type="image/avif"/>
<source srcset="static/image.webp" type="image/webp"/>

<img src="static/image.jpg" type="image/jpeg"/>
</picture>

上述代码,浏览器会依次判断每一个 source 元素中的 type 属性,如果浏览器支持这种 mimetype 则加载并展示该图片,如果都不支持则展示最后的兜底 img 元素。需要注意的是,这里的 type 是文件的 mimeType,指定的 type 和文件 mimeType 类型匹配。另外需要注意的是,source中指定图片地址使用的是 srcset 属性,不是 src 也不是 href

👉🏻 值得一提的是哔哩哔哩就是使用的这种方式

后端兼容处理

后端兼容的原理是,当浏览器发起一个请求的时候,会在请求头里面携带accept字段,用于告知服务器可以接收并处理的文件 mimeType,在服务器端,收到浏览器发起的请求时,可以判断一下浏览器支持的 mimeType,然后根据这个 accept 返回一个浏览器能处理的最优的文件类型即可。

下面是一个伪代码,具体实现可以查看github仓库

router.get("/(.*)", (ctx) => {
const mimeTypes = ["jxl", "avif", "webp"];
const fileName = decodeURIComponent(ctx.path);
const [baseName, fileExt] = fileName.split(".");
const staticDir = path.join(__dirname, "../static/");
const imageAccepts = ctx.accepts().filter((accept) => /image\/\w/.test(accept));

for (mime of mimeTypes) {
const _mimeType = `image/${mime}`;
if (imageAccepts.includes(_mimeType)) {
const _filePath = path.join(staticDir, `${baseName}.${mime}`);
if (fs.statSync(_filePath).isFile()) {
ctx.response.set("content-type", _mimeType);
ctx.body = fs.createReadStream(_filePath);
}
return;
}
}

const _filePath = path.join(staticDir, fileName);

if (fs.statSync(_filePath).isFile()) {
ctx.body = fs.createReadStream(_filePath);
return;
}

return (ctx.response.status = 404);
});

👉🏻 值得一提的是京东就是这么干的

请求图片的地址包含两个后缀,.jpg.avif。当浏览器支持avif的时候返回avif图片,否则返回jpg

说明

简单对比一下前端兼容方案和后端兼容方案,前端兼容方案实现简单,对后端要求少,能上传和下载图片各种格式图片即可,依靠H5的Picture标签,兼容性非常棒。缺点就是对使用方限制比较大,严重依赖H5,css背景图案无法处理,微信小程序、原生应用、部分Hybrid应用等都不支持,另外兼容增还加了额外的代码,一张图片多个地址,这些地址字符也增加了不少文件体积。

相对后端做兼容是比较理想的方案,对使用方没有要求,依赖http头中的accept字段,无论是H5还是css甚至微信小程序、原生应用等都能支持,缺点就是后端需要改造资源服务,对每个请求针对性的处理。

所以,我其实是更倾向于后端去做兼容的(不是因为我是前端工程师,真的是后端去做收益大)

图片懒加载

关于图片懒加载,之前已经写过一篇文章了,可以参考之前的一篇文章 👉🏻 图片懒加载

DPI 适配

DPR(Device Pixel Ratio),即设备像素比值,表示一个css像素和一个真是的物理像素的比值。同一张图片,在不同DPR设备上显示的效果不同,高DPR设备显示一张小图,显示效果较差,模糊不清,低DPR设备显示一张高DPR图片只会增加贷款消耗,对显示效果提升有限或者没有提升。所有针对不同DPR的设备,为他们提供不同的尺寸的图片即可统一不同设备的显示效果。

下面是一段CSS适配代码,可以在本地查看效果,在chrome调试工具中模拟不同dpr设备时可以展示不同图片

#box {
width: 300px;
height: 300px;
background-size: cover;
background-image: url(https://via.placeholder.com/300x300/ccc);
}

@media (-webkit-device-pixel-ratio: 2) {
#box {
background-image: url(https://via.placeholder.com/600x600/f78);
}
}

@media (-webkit-device-pixel-ratio: 3) {
#box {
background-image: url(https://via.placeholder.com/900x900/abf);
}
}
#box {
/* 不支持 image-set 的浏览器*/
background-image: url(https://via.placeholder.com/600x600/f78);

/* 支持 image-set 的浏览器*/
background-image: image-set(
url(https://via.placeholder.com/300x300/ccc) 1x,
url(https://via.placeholder.com/600x600/f78) 2x,
url(https://via.placeholder.com/300x300/ccc) 3x
);
}

html 适配

<img src='https://via.placeholder.com/600x600/f78'
srcset='https://via.placeholder.com/300x300/ccc 1x,
https://via.placeholder.com/600x600/f78 2x,
https://via.placeholder.com/300x300/ccc 3x'>

域名分片

在http1中,浏览器会限制同一个域名的并发请求数量,通常是6个,在图片资源较多的情况下,后面的图片需要等前面的图片加载完成后才能开始,前面的图片会阻塞后续资源加载,增加页面整体的加载时间,可以将图片域名分布到其它域名下。查看 京东 的资源加载情况可以发现,他的图片域名被分配到不同的域名,可以绕开浏览器对一个域名最多6个并发的限制,但是浏览器的并发也不是无限的,浏览器会显示每个标签页并发的请求数量,这个每个浏览器各不相同

大厂是如何进行域名分片我就不得而知了,不过我写了一个demo,可以简单模拟分片效果

const container = document.querySelector("div")
const fragment = document.createDocumentFragment()

// 不使用域名分片
for (let i = 0; i < 20; i++) {
const img = document.createElement("img")
img.src = `http://p.hec9527.top/1.jpg?q=${i}&t=${+new Date()}`
fragment.appendChild(img)
}

// 使用域名分片
// const domains = new Array(10).fill(0);
// const findDomain = () => domains.findIndex(domain => domain < 6)
// for (let i = 0; i < 20; i++) {
// let index = findDomain();
// if (index !== undefined) {
// domains[index]++;
// const img = document.createElement("img")
// img.src = `http://p${index === 0 ? "" : index}.hec9527.top/1.jpg?q=${i}&t=${+new Date()}`
// container.append(img);
// img.onload = function () {
// domains[index]--;
// }
// } else {
// const img = document.createElement("img")
// img.src = `http://p.hec9527.top/1.jpg?q=${i}&t=${+new Date()}`
// fragment.append(img);
// }
// }
container.append(fragment)