在最近观察业务表现过程中,注意到系统中图片占较大比重,但是图片的加载经常会出现空白闪烁等等的一些体验问题,部分页面如下
一些场景的加载卡顿截取
可以看到是典型的图文为主的展示页面,系统内有多处类似的场景。并且加载首屏的图片资源消耗也是非常耗时,lighthouse对课程列表的分析结果。图片比重和大小都偏大。
因此这里做优化的收益是比较明显的能给用户和公司带来收益的。但是缺少一个系统化的优化流程。
在开始之前我们先对一些基本只是有些了解,如图片格式,什么是无损和有损压缩。
既然是说图片加载,那么我们先对常见的图片格式做一个梳理和回顾,因为格式也是影响图片加载的一个重要因素,简单列举一下常见的图片格式:
jpg/jpeg
png
gif
WebBP
Avif
Jpeg xl
维基百科定义:有损数据压缩(英语:lossy compression)是一种数据压缩[1]方法,经过此方法压缩、解压的数据会与原始数据不同但是非常接近。有损数据压缩又称破坏性资料压缩、不可逆压缩。有损数据压缩借由将次要的数据舍弃,牺牲一些质量来减少数据量、提高压缩比。根据各种格式设计的不同,有损数据压缩都会有代间损失[2]——每次压缩与解压文件都会带来渐进的质量下降。
由于有损压缩减少了文件本身的数据量,且以牺牲图像质量为代价,因此压缩后的文件无论是在磁盘占用还是内存占用上都会比原始图像要小。针对于目前探讨的图片加载方式,对应的都是有损压缩,目标都是更小的内存占用和更快的解码速度。
维基百科定义:无损数据压缩(Lossless Compression),是指资料经过压缩[3]后,信息不被破坏,还能完全恢复到压缩前的原样。相比之下,有损数据压缩[4]只允许一个近似原始资料进行重建,以换取更好的压缩率。无损数据压缩在许多应用程序中使用。例如,ZIP[5]和gzip[6]。无损数据压缩通常用于严格要求“经过压缩、解压缩的资料必须与原始资料一致”的场合。
无损压缩的方法可以通过一些编码手段,用结构化的数据来减少对重复信息的磁盘占用,针对图片来说减少了图片在磁盘上的空间占用。但是并不能减少图像的内存占用量,这是因为,当从磁盘或网络请求上获取图像时,浏览器又会对图片进行解码,把丢失的像素用适当的颜色信息填充进来。
因此如果要减少图像占用内存的容量,就必须使用有损压缩方法。
WebP 是一种现代图像格式,可为 Web 上的图像提供卓越的无损和有损压缩。使用 WebP可以创建更小、更丰富的图像,从而使 Web 更快。与 PNG 相比,WebP 无损图像的大小要小 26% 。[7]在同等 SSIM[8]质量指数下, WebP 有损图像比可比较的 JPEG 图像小 25-34% 。[9]无损 WebP支持透明度(也称为 alpha 通道),成本仅为22% 额外字节[10]。对于可以接受有损 RGB 压缩的情况,有损 WebP 还支持透明度,通常提供比 PNG 小 3 倍的文件大小。
来个直观体验
也可以戳这里看下社区其他同学做的对比效果[11],可以看到webp在图片体积和效果上都做的不错,很适合我们的场景。并且webp的使用目前已经比较广泛,如在youtube以及抖音pc上都可以看到。
Youtube 部分页面的截取,在封面图等大图场景均使用的webp格式
抖音pc站
webp的压缩技术基于 VP8[12]关键帧编码,无损 WebP 压缩使用已知的图像片段来精确地重建新的像素,在无法找到相应的匹配值的情况下,使用本地调色板进行优化。在webp的开发者平台已经有详细的压缩技术的推演,可以直接戳这里[13]看下。
随着浏览器对 WebP 支持的普及,目前也有越来越多的互联网开始使用 WebP,这里分享几个数据:
YouTube 的视频略缩图采用 WebP 后,网页加载速度提升了 10%;
Google Chrome 应用商店采用 WebP 后,每天可以节省几 TB 的带宽,页面加载时间减少了30% 左右;
花瓣网在 2017 年 5 月开启 WebP 后,在网站总体请求量没有减少的情况下,整体带宽下降了近 50%。
结论:无论是技术上还是使用上都已经得到了可行的验证,并且有明显收益。
图片的优化分为加载阶段和显示阶段。
图片体积直接反应了网路需要加载的时间,等同于磁盘占用,因此减少图片体积能直接减少图片请求的时间。进而在首屏提升FCP等相关指标,让浏览器能更快拿到数据进行绘制。
内存占用和图片体积不等同,两张不同体积的图片可能有着相同的内存占用,因此优化内存占用可以让浏览器解码图片和光栅化的时间减少,因为不需要计算绘制那么多的图片信息。光栅化时间的减少直接影响了页面的渲染速度,以及页面的卡顿。
占位图是为了给用户有感知的加载,提升用户体验。避免用户等待过程中的流失。
懒加载也已经是当前各种站点的常规优化手段,懒加载尽量减少了不必要的资源请求以提高浏览器的渲染效率,减少内存占用。并显著减少不必要的带宽,是为用户和公司都省钱的方式。
对于浏览器对不同格式的图片支持程度不同,我们的一些优化手段和格式可能不太适用所有浏览器,但是为了保证性能和体验并最大兼容支持的浏览器,我们需要对图片进行格式降级处理。如对于不支持webp的浏览器自动降级为png。
错误占位也是必要的一步,当所有的尝试都失败后我们也需要一种良好的方式展示并给用户感知到。比如目前业务内的错误展示。
对应于我们优化思路的加载阶段,使用公司已有的平台能力。我们可以获得不同格式和压缩比例的图片。比如我们选择压缩比75的webp以及原图两种格式。webp作为默认格式,原图则作为backup的兜底资源。这里需要注意的是,图片列表需要服务端的支持,因为目前系统的图片是经由服务端返回的鉴权url,因此这部分需要配合改造。
基本格式如下
type ImgUrlList={
// 原图
origin:string,
// webp格式
webp:string,
// avif格式
avif:string,
}
模板配置如图
对于为什么图片地址需要多个,主要是为了方便我们做回退处理,遇到浏览器不兼容的格式就牺牲流量换取可正常展示的图片,保证内容可见。这里获得的图片格式消费流程如下
通过近一周的站点数据统计,目前业务方浏览器数据如下,其中chrome占比78.66% ,浏览器版本chrome最低55,fireforx最低99,均在webp的支持范围内。数据均兼容不考虑移动端浏览器。由于IE也存在极小的比重,所以IE应该会是触发降级占比最高的。
图片加载这里是优化思路的显示阶段的实现,主要包含从加载占位到失败占位的整个流程,当然也包含懒加载。加载我们在观测阶段和稳定阶段使用了不同的方式。这里针对观测阶段的方案展开介绍。最稳定方案是Picturede 方式,可以在下文稳定阶段看到。
观测主要是为了有数据对比,这里我们使用到了xx图片处理包来做图片加载,主要原因有三:一经过抖音pc和西瓜视频的场景验证、二集成上报的能力,能够拿到图片的相关数据、三提供了图片加载和回退的支持,满足当前场景。使用示例如下
import type ImageObserver from 'xxxxxxxxx';
let imgObserver: ImageObserver;
export async function getImgObserver(): Promise<ImageObserver> {
if (imgObserver) {
return imgObserver;
}
const ImageObserverSDK = import('xxxxxxxxx');
const LoggerSDK = import('xxxxxxxxx-logger');
const [imgObserverSdk, logggerSdk] = await Promise.all([ImageObserverSDK, LoggerSDK]);
const ImageObserver = imgObserverSdk?.default;
const Logger = logggerSdk?.default;
if (ImageObserver && Logger) {
imgObserver = new ImageObserver({
plugins: [Logger],
divider: {
dataSrc: 'src',
backUpSrc: 'backup-src',
},
logger: {
user_unique_id: 'cccccc', // TODO,
app_id: 111111, // TODO, },
});
}
return imgObserver;
}
本图片处理包包含了图片加载错误重试的逻辑,跟我们上面图片压缩章节设计的图片列表相结合,可以完成自动回退。
错误示例如下,我们给定一个可用地址,其中src以及backup-src的第一个均不可用,预期是可以自动降级到最后一个可用地址
为了保证图片加载流程的可控性,比如在图片即将出现再去做响应的加载处理。因此一些通用的默认拦截图片并自动做加载处理的方式就不在适用了,因为我们没办法严格控制每个图片的显示时间也不好做拦截处理。因此懒加载我们手动通过IntersectionObserver
来实现,基本代码如下,其中useIntersectionObserver
是IntersectionObserver
的一个实现封装。
const observerCb: IntersectionObserverCallback = useCallback((entrys, observer) => {
const entry = entrys[0];
if (entry.isIntersecting) {
setImgVisible(true);
observer.disconnect();
}
}, []);
const { updateObserverEl } = useIntersectionObserver({
cb: observerCb,
});
这样我们明确控制了每个图片的加载时机,并对加载结果精细化控制和处理。在一次观测完成后立即清除观测,完成一次加载。
我们通过第一步获取了可用的几种格式,因为我们不知道用户的浏览器会是什么样子,所以不能一股脑的都换成webp格式,所以我们需要知道webp的格式加载成功了多少,我们的图片加载耗时情况是什么样子。有多少是回退到了原图,加载耗时又是什么样子。那当我们有新的方案能不能让用户无缝切换过去,怎么做用户放量等等问题。因此我们需要对图片加载做监控。
细心的你可能已经注意到我们图片加载部分有一个xxxxxxxx-logger
,没错这个就是用来做上报的,上报流程为尝试加载->失败重试->加载结果->上报
。logger插件会收集加载过程中的图片信息,加载时长,失败情况进行上报。这样我们就能够根据数据情况查看我们改造的用户覆盖度和使用情况,以便我们做后续分析。
这一步是对我们优化结果的进一步结论导出,什么意思呢。以我们加载的图片类型数据为例,如果我们的webp支持程度很好,那是不是可以实验性的将avif格式作为下一次的实验对象来验证更高的性能。如果我们的图片每种格式都很慢,那么我们自然可以反推cdn来优化解决方案。同时如果webp的不支持,也可以看下我们的降级策略是不是很好的生效了,保证的系统的高可用。等等。因为我们有了数据支撑,反推变得更加容易。
我们通过上一步的实践已经完成了我们需要的数据观测和预期效果。这时我们已经有了图片在线上的加载耗时,解码耗时,加载稳定性相关的数据,并且反推了在系统整体设计的上下游对图片的限制的合理性,比如课程封面场景限制图片上传尺寸10M,但是这个限制无论如何都严重影响加载性能,那降低到200K是既满足需要又不影响性能的适合值,那么这就是通过实验阶段推导到的优化结果。也是进入稳定阶段的重要一步。因此上一步的实验阶段需要尽可能有效的分析全面数据。
那么说了一堆之后,我们稳定阶段可以做点什么。当然是期望再优化一点,于是我们做的事情有两个,一是下掉上一步的监控,二是变更为浏览器处理图片,同时满足我们的场景。第一步就比较明显因为监控本身是有流量损耗和代码体积影响的。那么第二步就是加个js处理图片降级的方式平滑过渡到浏览器一支持。于是就有了如下形式的代码
const pictureRender = () => {
const { webp, avif, image } = remain.urlList;
return (
<picture>
<source srcSet={avif} type="image/avif" />
<source srcSet={webp} type="image/webp" />
<img src={image} onError={() => onError?.()} {...remain} />
</picture>
);
};
这里我们使用了picture标签来做图片的自动降级,关于picture标签的用法和场景可以这篇文章[14]。总的来说就是做响应式图片和自动降级的一个比较好的方式。这里就不展开了。我们通过上面的代码把我们兼容的格式进行分类指定,以满足picture的使用场景。示例的集中格式会在加载不满足条件时依次降级。因为picture的加载事件最终还是会落到img标签上,所以我们上面的监听方式依然适用。
到这里我们已经总结了稳定阶段和实验阶段各自采用的加载策略。但是有一点好处是,这两者是不冲突的。我们希望继续保持对新业务场景开启实验观测的能力,稳定业务可以继续用稳定场景方案。因此我们只需要轻微改造就可以完成这个支持,完整代码贴在下方。这里需要注意的是,虽然保留了两者的能力,但是并不会影响首页体积,因为本身js监控图片的方式也是动态加载的,因此除了打包阶段会有总包体积的占用,对系统性能是没有损耗的。
import { getImgObserver } from '../../utils/observer';
import React, { useRef, useEffect } from 'react';
export const ImageMonitor: React.FC<any> = (props: any) => {
const { currentref, onError, usePicture, ...remain } = props;
const imgNode = useRef<HTMLImageElement | null>(null);
useEffect(() => {
if (!usePicture) {
const monitor = async () => {
const observer = await getImgObserver();
observer?.observer?.(imgNode.current).then((res: any) => {
if (res.code !== 0) { // 加载最终失败
onError?.();
}
});
};
monitor();
}
}, []);
const pictureRender = () => {
const { webp, avif, image } = remain.urlList;
return (
<picture>
<source srcSet={avif} type="image/avif" />
<source srcSet={webp} type="image/webp" />
<img src={image} onError={() => onError?.()} {...remain} />
</picture>
);
};
// 兼容js处理图片和浏览器原生处理图片
if (usePicture) {
return pictureRender();
}
return (
<img
{...remain}
ref={el => {
if (!imgNode.current) {
imgNode.current = el;
currentref?.(el);
}
}}
flag="monitor"
/>
);
};
https://segmentfault.com/a/1190000016735421
https://zhuanlan.zhihu.com/p/30534023
https://developers.google.com/speed/webp
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/picture
https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images
[1]数据压缩: https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9
[2]代间损失: https://zh.wikipedia.org/w/index.php?title=%E4%BB%A3%E9%97%B4%E6%8D%9F%E5%A4%B1&action=edit&redlink=1
[3]压缩: https://zh.wikipedia.org/wiki/%E8%B3%87%E6%96%99%E5%A3%93%E7%B8%AE
[4]有损数据压缩: https://zh.wikipedia.org/wiki/%E7%A0%B4%E5%A3%9E%E6%80%A7%E8%B3%87%E6%96%99%E5%A3%93%E7%B8%AE
[5]ZIP: https://zh.wikipedia.org/wiki/ZIP
[6]gzip: https://zh.wikipedia.org/wiki/Gzip
[7]小 26% 。: https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results
[8]SSIM: https://en.wikipedia.org/wiki/Structural_similarity
[9]小 25-34% 。: https://developers.google.com/speed/webp/docs/webp_study
[10]22% 额外字节: https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results
[11]效果: https://isparta.github.io/compare-webp/index.html#12345
[12]VP8: https://en.wikipedia.org/wiki/VP8
[13]这里: https://developers.google.com/speed/webp/docs/compression
[14]这篇文章: https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/AJFFZOlioRCqyohAgagD9g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。