在一个普通的工作日,BUG反馈群突然发来一个视频。视频中显示,我们的H5应用在打开某个下发的资料时,加载图片的过程中陷入了不断刷新的死循环。这个问题直接影响了用户体验,也引发了我们的深入调查。
二、问题分析
收到反馈后,我立即在浏览器中打开该资料进行测试。虽然网页没有出现无限刷新的现象,但加载速度明显变慢,操作也非常卡顿。查看控制台和网络请求后,并未发现任何错误提示。然而,在对比加载其他资料时,这些问题并未出现,网页运行正常。因此,可以判断是某篇资料导致的性能问题,而不是网络或环境方面的问题。
1.性能分析
遇到性能问题时,第一步肯定是使用工具分析具体原因。我们使用 Safari 浏览器的时间线工具录制了性能数据。我的电脑是 MacBook Pro M3 芯片(16GB 内存),从中可以看到,平均 CPU 利用率竟然达到了 89.7%,并且主线程的大部分性能都用于渲染。由此我们可以推测,网页卡顿的问题大概率出现在浏览器的渲染过程中。
2.问题根源
我们的资料是通过后台富文本配置的,为了深入排查,我检查了问题资料的 HTML 元素,特别关注了页面中的富文本配置的内容。在此过程中,我惊讶地发现,这张图片的分辨率高达 4505px × 60615px,是一张超大像素的图片。通过对比其他资料中的图片,发现它们的分辨率明显较低,因此可以初步排除是其他因素引发的问题,最终确定是这张超大分辨率图片导致了浏览器性能瓶颈,进而引发了不断刷新的现象。
那么,不断刷新的原因是什么呢?在尝试过不同浏览器后,我们对这个问题有了些许线索:不同浏览器对于错误的处理行为是不同的。
- 谷歌浏览器:在谷歌浏览器中,浏览器会提示崩溃。
- Safari 浏览器:在 Safari 浏览器中,浏览器会提示该网页重复出现问题。
- 钉钉内嵌浏览器:在我们的复现场景中,钉钉内嵌的浏览器则会导致页面不断刷新。
大家可以尝试使用不同的浏览器查看错误处理的方式,这对于选择合适的浏览器也有一定参考意义。
三、两亿像素图片如何被渲染的
为了进一步理解问题,我们先了解一下浏览器的渲染过程。按照浏览器渲染的时间顺序,一个网页从获取资源到最终展示在屏幕上,通常会经历以下几个子阶段:
- 构建 DOM 树:浏览器从网络或磁盘中获取
HTML文档
,并将其转换为DOM树
。该树表示HTML文档
的层级关系。 - 样式合成:浏览器将获取到的
CSS
文件经过标准化、继承和层叠之后计算出最终的形成一个styleSheets
表,也有另一种说法叫做CSSOM树
- 布局阶段:根据
DOM树
和样式信息计算页面中每个可见元素的位置和大小,形成布局树,包括滚动条、文字换行等。 - 分层:根据布局树将页面划分为多个图层,方便独立渲染和优化性能。
- 绘制:渲染线程将图层拆成一个个绘制指令,最后集合成一个绘制列表。
- 分块:将图层进一步划分为小块(
tiles
),提升渲染效率。 - 光栅化:将矢量图形的每个小块转化为像素图,生成最终的位图。
- 合成:将多个图层和小块按照正确的顺序合成,调用
OpenGL
(意为"开放图形库",可以在不同操作系统、不同编程语言间适配2D,3D矢量图的渲染。)生成最终的屏幕显示内容。
1.首次进入页面
当浏览器解析HTML
时,遇到<img>
标签便会创建相应的 DOM 节点,同时开始加载图片资源。加载图片时,浏览器会尝试根据CSS
样式计算图片的渲染大小。然而,HTML
的解析和样式计算是同步
进行的,而图片资源加载通常被标记为低优先级,因此是异步
完成的。
浏览器的渲染机制会优先保证页面的快速可见性(First Paint)。所以在第一次加载的时候,因为图片还没加载完成无法得知图片的真实宽高的,浏览器会先为其预留默认占位大小。
2.绘制
在绘制阶段,渲染线程将页面的每个图层拆解为绘制指令。
开发者的图层
工具中,我们可以看到图层以及相应的渲染指令,在渲染那张两亿像素的图片中,其他的切割和绘制线条指令使用的时间在 0.1 微秒到 2 毫秒不等,但是那张两亿像素的图片绘制指令的执行时间相比于其他指令来说已经是惊人的 78 毫秒了。
3.分块
在分块阶段,通过safari
浏览器的时间线工具我们可以看到,在图片加载完成后,合成线程将图片分成几千到几十万像素不等的小块进行。
4.光栅化
光栅化阶段是将分块的矢量图形转化为屏幕上的像素数据:
- 每个小块单独处理并通过插值算法(如最近邻插值、双线性插值)调整分辨率。
- 处理后的光栅数据上传至 GPU 的纹理内存,供最终显示使用。
在处理两亿像素图片时,浏览器会使用相应的解码算法将压缩的图片解码为位图(Bitmap)。这一步涉及大量的解码计算,尤其对于两亿像素图片,对 CPU 和 GPU 的计算需求极高。如果在性能较弱的手机设备上运行,可能会导致显著的卡顿,甚至页面崩溃。
5.图片加载完成
当图片加载完成,浏览器会触发页面的重新布局。在本案例中,富文本内容中的<img>
元素的宽度被富文本编辑器设置为 100%
,而父元素的宽度也为 100%
。由于图片未显式指定宽高,浏览器在初次渲染时会按照默认行为显示图片的原始尺寸。因此,在渲染过程中,页面可能会短暂地显示几帧图片的原始尺寸。随后,随着父元素的宽度被计算确定,浏览器会将图片调整为父元素的 100% 适配容器的宽度。
四、解决方案
经过排查,我们已经明确问题的根本原因是浏览器直接渲染这张超大分辨率图片,导致浏览器在渲染过程中消耗过多资源,从而引发了网页的重复刷新。因此,我们的解决方案主要集中在优化图片的加载和渲染过程,以避免浏览器因处理超大图片而产生性能瓶颈。
1.上传前校验
在富文本上传图片的时候,会将图片文件通过接口上传给后端,后端返回预览链接。为了避免因图片尺寸过大而导致的页面卡顿或浏览器崩溃,所以我们需要在图片上传的流程中加入尺寸校验机制。
a. 前端校验
我们以常用的wangEditor5
富文本编辑器为例,在自定义上传回调中,通过FileReader
和Image
读取上传的图片文件,对图片进行校验。
- FileReader:
FileReader
是 HTML5 中提供的一个对象,允许我们在客户端读取文件内容。它通常与<input type="file">
元素配合使用,能够读取用户选择的文件,并提供不同格式的读取方式(如文本、数据URL、二进制字符串等)。 - Image:
Image
是 JavaScript 提供的一个构造函数,通常用于在网页中动态创建和操作图像。它允许你加载图像并获取图像的相关信息(如宽度、高度等),并能够在页面中动态插入图像元素。