veImageX 演进之路:Web 图片加载提速 50%
作者 | 马超, veImageX web 生态负责人;
张锡平,veImgeX 产品负责人
字节跳动从公司成立之初就建设了图像处理平台,起初主要服务于今日头条 APP 的图文资源,随着业务扩展,逐步服务于抖音图集、短视频封面、图虫等几乎用户看到的所有图片展示场景;火山引擎视频云团队将字节跳动图像处理的实践,整理为《veImageX 演进之路》系列,将从服务端、客户端、网络库、业务场景与优化等多个角度,介绍在图像处理压缩、省成本与体验优化的经验与方案;
本篇文章重点介绍在 web 端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式对用户体验有着显著影响。火山引擎 veImageX 为业务提供了灵活、高效的一站式图片解决方案和静态素材托管方案,涵盖了上传、存储、处理、分发、评估等图片生产和消费阶段的全部链路。
Web 场景下图片的应用非常广泛,从传统的图文到视频封面都有图片的身影,图片体验是用户体验中很重要的一环,常用于衡量站点性能的 LCP 和 CLS 指标都把图片列为最重要的元素之一。随着业务的发展,用户量增长的同时也带来了 CDN 带宽成本的快速提升,最主要的元素则是图片和视频。因此,方案从体验和成本出发,旨在为用户提升体验的同时降低带宽成本。
图片体验问题通常有以下几点:
加载速度慢:图片体积、网络、CDN、处理耗时等因素均会影响加载耗时;
加载失败率高:导致图片加载失败的因素很多,重点在于如何及时定位问题;
渲染体验差:包括图片区域长时间空白、加载后导致页面抖动、出错后无兜底等场景;
开发者往往忽视了图片体验,也不了解图片对站点性能的影响,并且缺少可量化的数据来衡量站点的图片体验。参考 Lighthouse 性能优化指南,方案整合了图片压缩、图片懒加载、图片稳定性布局、错误兜底等能力,并集成了数据监控能力,可结合 火山引擎 veImageX 控制台实时大盘数据查看,为业务提供数据上报、数据分析、数据追踪、数据告警等全链路支持。
以下问题通常会带来额外的带宽成本:
图片压缩率低;
图片原始分辨率和渲染分辨率不匹配;
采用传统的 PNG、JPEG 等低压缩率格式;
图片未进行懒加载;
除了图片压缩,方案支持了 WebP、AVIF 等高压缩率图片格式的自适应加载和图片分辨率的自适应加载,尽可能减小图片体积。同时集成了图片懒加载,避免不可见区域的图片加载,降低站点 CDN 成本,同时也提升站点整体加载速度。根据内部业务数据,图片传输带宽和图片加载耗时通常可降低 50% 以上。
方案总体上可划分为图片加载和数据监控两个部分。
如图所示,图片加载部分支持分辨率、格式自适应以及懒加载、稳定性布局等特性,其中涉及到图片处理部分基于火山引擎 veImageX 服务实现,如图片转码、缩放、压缩等。SDK 侧生成当前环境下最佳的图片格式和分辨率,从服务获取相应的图片 URL,借助云端处理能力在运行时动态生成所需的图片。
数据监控部分可分为加载耗时监控、图片详情监控、画质评估、大图监控、云控配置几部分,监控 SDK 收集相关数据,根据云端下发的配置上报数据,火山引擎 veImageX 服务对数据做清洗后可在控制台侧查看数据大盘。
常见的图片格式有 PNG、JPEG、GIF、WebP、AVIF、HEIC 等,其中 WebP、AVIF、HEIC 等高压缩率图片格式可显著减小图片体积。但由于不同浏览器对高压缩率格式的支持情况不同,因此在应用时需要考虑图片加载的环境。三种高压缩率格式在 Web 侧的兼容性如下:
1. WebP
2. AVIF
3. HEIC
在 APP 端,对于不支持的图片格式可采用 SDK 软解的方式进行解码、渲染,Native 侧的性能可保证图片解码的耗时和流量的节省都能有不错的收益。在 Web 侧,由于浏览器性能限制,veImageX 内部性能测试表明,SDK 软解在图片整体耗时方面的收益并不明显,尤其是多图场景下,因此在 Web 侧更适合走格式自适应的方案,即根据浏览器的支持性加载相对最优的图片格式。
常见的做法是采用
<picture>
<source srcset="image1.webp" type="image/webp" />
<img src="image1.jpg" decoding="async" loading="lazy"/>
</picture>
但由于浏览器版本众多,在实际应用中,可能会出现很多预期以外的情况,比如:
会同时加载多个图片资源,造成带宽的浪费;
并非完全支持 WebP 的所有特性,存在加载失败的场景;
只支持 AVIF 静图格式,不支持动图;
...
为了保证图片加载成功率,因此在实际应用中无法直接使用
分辨率自适应指的是客户端根据实际渲染的宽高获取相应分辨率的图片,从而减小图片体积。常见的做法是我们可以借助 HTML 中原生的 srcset 属性来定义图像集,以及每个图像应用的场景。由以下三部分组成:
文件名
空格
图像描述符,有两种描述方式
宽度描述符 w,描述图像的固有宽度,以像素为单位。比如 480w 表示当浏览器需要 480 像素宽的图像时应该使用的图像资源
像素密度描述符 x,描述了显示器的像素密度和图片资源之间的对应关系,通过 window.devicePixelRatio 可查询显示器像素密度
sizes 则定义了一组媒体条件,比如:屏幕宽度。并且指明当媒体条件为真时最佳的图片尺寸。每个条件由以下三部分组成:
一个媒体条件,比如 max-width:480px,表示可视窗口的宽度不超过 480 像素时
空格
当媒体条件为真时,应该选用的图片大小
可以将
<picture>
<source
srcset="image1.webp 200w,
image2.webp 600w"
sizes="100vw"
type="image/webp"
/>
<img
srcset="image1.jpg 200w,
image2.jpg 600w"
sizes="100vw"
decoding="async"
loading="lazy"
/>
</picture>
然而在实际中又会面临一些问题,如:
指定多个 srcset 会增加 HTML 文件大小,尤其是当
中存在多个的场景; 媒体查询条件只能是屏幕宽度和像素密度,不能准确反映真实的图片渲染情况;
srcset 配合 sizes 使用,理解成本相对较高;
...
在实际应用中,某些情况下可以提前知道图片渲染大小或者图片所在区域的大小,结合方案内置的几种布局方式以及设备像素密度等信息,加载 SDK 内部可以分析并选择出当前模块渲染的最佳分辨率。
Web 侧通常基于 CLS(Cumulative Layout Shift,累积布局偏移)指标用于衡量页面布局的视觉稳定性。当可见元素的位置在页面生命周期内发生了变化时,就会产生布局偏移。
受 next/image 的启发,加载 SDK 内置了四种稳定性布局方式:intrinsic、responsive、fixed、fill,通过生成稳定的 dom 结构来提升视觉稳定性,减少业务开发量。效果如下:
intrinsic: 若指定宽度小于容器宽度,则根据指定宽高渲染图片;反之则图片宽度为容器宽,图片高度按照比例缩小;
responsive: 图片渲染宽度等于容器宽度,高度按比例缩放;
fixed: 根据指定宽高渲染图片;
fill: 图片缩放以填充容器,可传入 objectFit、objectPosition 属性表示不同的填充模式;
对于图片懒加载最简单的做法是基于 的原生属性 loading="lazy",但在实际的应用中也发现了两个问题:
该属性的兼容性不达标,多数浏览器不支持;
在部分 Safari 浏览器上存在 bug,可能会导致图片加载被阻塞;
因此,SDK 内部基于 IntersectionObserver API 实现,该 API 相对更可控,且可以设置懒加载的距离、目标元素等属性。
数据监控的整体链路为:
监听全局的 Load 和 Error 事件,并筛选出属于图片的部分;
基于 PerformanceObserver 监听图片资源加载,该事件回调中可拿到图片加载耗时相关的指标,如 DNS、TCP、SSL、请求、下载各个阶段的耗时,并且可以基于该 API 监听 CSS 中图片资源的加载;
对于图片格式、状态码、画质打分等信息则依赖 Response Header,而拿到 Response Header 仅有 request 资源这一种方式,因此在资源加载后再去 request 本地缓存中的信息,同时为避免并发请求影响其他类型的 HTTP 请求,SDK 会根据采样率、当前请求量等信息在空闲时读取需要上报的图片的缓存;
整合所有原始数据,根据采样率上报至 veImageX 数据服务,由数据服务对原始数据做清洗;
经过后端服务处理后最终即可在 veImageX 质量监控大盘查看,具体支持的指标及维度如下图所示:
a. 下行网络监控
b. 客户状态监控
方案致力于为 Web 场景提供极致的图片加载体验,同时在稳定性和场景覆盖上也在不断提升。
上面提到在某些浏览器下会存在部分 WebP、AVIF 图片加载失败的场景,在监控到此类场景后加载 SDK 基于格式探测的方式最低成本的解决了此类问题,同时保证了性能。
例如:在 iOS 14.3 & 14.4 版本下的 Safari 浏览器加载部分的 WebP 失败,而
const checkWebP = () => {
const pro: Promise<boolean> = new Promise<boolean>((resolve) => {
if(typeof window === 'undefined') resolve(false);
if (window['__support_webp__'] !== undefined) {
resolve(!!window['__support_webp__']);
} else {
const img = new Image();
img.onload = () => {
window['__support_webp__'] = true;
resolve(true);
};
img.onerror = () => {
window['__support_webp__'] = false;
resolve(false);
};
img.src = 'error image';
}
});
return pro;
};
目前方案支持了 React、Vue2、Vue3 以及小程序,为了保证体验的一致性、降低维护成本,加载 SDK 做了分层的设计,将核心的 Core 层抽离出来给到各个框架使用,并对各项能力做了插件化。
随着方案的迭代,我们也在尝试覆盖更多的业务场景,比如:加密图渲染、Hybrid HEIC 渲染等,火山引擎 veImageX 希望给客户带来全面、稳定、流畅的图片体验,同时给业务带来极致的成本收益。
我们将如上能力封装成简单的 webSDK,向行业输出,并可以免费获取和使用此 SDK,更高级的能力也可以配合 veImageX 来使用;
webSDK 接入地址:
https://www.volcengine.com/docs/508/177943
微信扫码关注该文公众号作者