深入理解浏览器渲染帧

深入剖析浏览器渲染帧与性能优化

在这里插入图片描述

🤖 作者简介:水煮白菜王,一个web开发工程师 👻
👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧、记录和知识归纳总结✍。
感谢支持💕💕💕

浏览器渲染帧流程

手机和电脑屏幕的默认刷新频率通常为 60Hz,对应每秒刷新 60 帧,即每帧仅有约 16.7 毫秒的渲染时间。因此,浏览器的理想渲染目标是确保每一帧内的所有任务都能在 16.7 毫秒内完成。是否能在单帧时间内完成任务,直接决定了页面的渲染性能,并对用户交互的流畅性产生关键影响。

浏览器的 FPS(每秒帧数)反映了其在一秒钟内能够渲染的画面数量。FPS 越高,页面显示越流畅,用户体验也越好。

🧩浏览器渲染帧流程图

开始
是否到达16.7ms间隔?
执行渲染任务
更新页面显示
结束
等待下一次时间间隔

🕒 标准帧渲染流程

标准渲染帧:在一个标准帧渲染时间 16.7ms之内,浏览器需要完成 Main 线程的操作,并 commit 给 Compositor 进程

⚠️ 丢帧现象

当主线程中执行的任务过多、耗时过长,导致无法在 16.7ms 内完成提交时,浏览器将无法及时将更新页面内容draw绘制到屏幕上,从而丢失一帧。这种现象会导致页面卡顿、动画不流畅。

📅 主线程与合成器线程协作流程图总结

在这里插入图片描述
一个图形用户界面(GUI)应用程序中,帧渲染过程的时间线和逻辑流程。两个连续的帧,每个帧的目标是在大约16.66毫秒内完成,这是为了达到每秒60帧(60fps)的平滑动画效果。
在每个帧的周期内,有两个主要的处理阶段:主线程(Main)和合成器线程(Compositor)。主线程负责处理应用程序的主要逻辑和UI更新,而合成器线程则专注于将这些更新绘制到屏幕上。
主线程(Main)

  • Loop:在每一帧开始时,主线程会进入一个循环(loop),在这个循环中执行必要的任务,进行计算和逻辑处理,这可能包括响应用户输入、更新DOM树或重新计算布局等。
  • Commit:当主线程完成上述关键任务后,它会将更新的结果提交给合成器线程。通过提交更新,主线程将当前帧需要显示的内容传递给合成器线程,以便进行后续的绘制操作

合成器线程(Compositor)

  • 一旦收到主线程提交的更新后,合成器线程就会开始准备绘制下一帧的内容。
  • 它会根据主线程提供的数据生成图像,并最终将这些图像绘制到屏幕上,这个过程被称为“Draw”。

时间线

  • 每一帧的目标是在大约16.66毫秒内完成,这是为了达到每秒60帧(60fps)的平滑动画效果。
  • 在每一帧中,主线程会优先处理关键任务,确保当前帧的内容能够按时提交给合成器线程。
  • 合成器线程则专注于高效地完成合成与绘制工作,以保证每一帧都能及时显示出来。

通过这种分工合作的方式,主线程和合成器线程能够高效地协同工作,确保每一帧都能按时完成并显示出来,从而实现流畅的动画效果。主线程负责处理复杂的逻辑和更新,而合成器线程则专注于高效的图像合成与绘制,两者共同保证了浏览器渲染的高性能和高响应性。

⏳ 每帧中的空闲时间(Idle Period)

在浏览器的一个渲染帧(16.7ms)里,会存在一段时间,叫做空闲时间(idle period),如果完成各种任务的执行以及页面渲染的工作等的时间少于 16.7 ms,那么这一帧就会存在空闲时间,可以把一些耗时操作拆分开来,然后在每一帧的空闲时间中去执行。

在这里插入图片描述

在这里插入图片描述
主线程(Main)

  • Run Task:在每一帧开始时,主线程首先会运行必要的任务(Run Task)。这些任务可能包括处理用户输入、计算新的状态或执行其他关键操作。
  • Update Rendering:接下来,主线程会进行更新渲染(Update Rendering)的操作。这一步骤通常涉及根据之前运行的任务结果来更新DOM树、样式计算以及布局等,以准备下一帧的显示内容。
  • requestIdleCallback():在完成上述关键任务后,主线程会调用requestIdleCallback()函数。这个函数的作用是请求一个空闲回调,在当前帧的剩余时间里执行一些非关键但有益的任务。

Idle 回调(Idle Callback)

  • 执行低优先级任务:当主线程调用requestIdleCallback()后,如果当前帧还有剩余的时间(即空闲期间),就会触发Idle回调。在这个阶段,可以执行一些低优先级的任务,比如优化资源、预加载数据、清理内存等。这些任务虽然不直接影响当前帧的显示,但有助于提升整体性能和用户体验。

时间管理

  • 每一帧的目标是在大约16.66毫秒内完成,这是为了达到每秒60帧(60fps)的平滑动画效果。
  • 在每一帧中,主线程会优先处理关键任务(如运行任务和更新渲染),确保当前帧的内容能够按时显示出来。
  • 如果在完成关键任务后还有剩余时间,就会进入空闲期,并通过Idle回调执行一些低优先级的任务。

通过这种任务分配和时间管理的方式,浏览器能够在保证每一帧内容及时更新的同时,充分利用每一帧的剩余时间来执行一些额外的工作,从而进一步提升整体性能和用户体验。主线程和Idle回调的协同工作,使得浏览器能够高效地处理各种任务,保持流畅的视觉效果和响应性。

🚨 页面卡顿的根本原因

所谓的页面卡顿、首屏加载缓慢,通常归因于执行了长时间任务,这些长任务推迟了页面的渲染时机,导致每一帧未能及时完成渲染,从而影响用户体验。要解决这一问题,关键在于了解浏览器在每一帧中具体执行的任务,并识别出哪些因素导致了渲染时机的延迟。通过使用浏览器的性能检测工具(Performance),我们可以精确地分析并定位这些问题。

在这里插入图片描述

🧪接下来我们可以用Google官方给的例子 Jank 中:

左侧提供了控制按钮:“Stop” 用于暂停小球运动,“Add” 和 “Subtract” 用于增减小球数量。有趣的是,当小球数量不断增加时,页面会出现明显卡顿;但点击 “Optimize” 按钮后,性能问题即刻得到优化,页面恢复流畅。

接下来,我们将借助📊Performance工具深入分析页面卡顿的原因:

在这里插入图片描述
通过大约3~4秒钟的📊Performance工具录制,我们可以明显观察到该页面存在显著的性能问题。接下来,我们将首先对捕获的数据图表中的关键内容进行分析:

在总览区域的统计中,可以清晰地看到各个阶段的具体耗时。很明显渲染阶段占据了超过一半的时间, 由此可以初步定位,性能瓶颈主要出现在渲染环节。

在这里插入图片描述
在这里插入图片描述

📌 长任务分析

我们来观察其中的一个 Task 来看:标红代表该任务是长任务(一般认为超过 50ms 的任务是长任务),往下是该任务具体的细节,比如这个 Task 里主要执行了 Animation Frame Fired 方法,它里面调用了 Function Call,Function Call 里面调用了 app.update 的方法,一层一层往下调用执行,然后在 app.update 下面我们可以看到很多紫色的线条,紫色代表回流重绘
在这里插入图片描述
现在可以初步下结论:频繁的回流重绘导致页面卡顿,后面还要再进行分析才能确定。

📐Call Tree 分析

接下来点击其中的一个任务,观察 Call Tree,每个方法的执行时间都能看到,以及时间的占比

在这里插入图片描述

我们的分析目标主要是寻找花费时间长的任务,依次点开,可以发现 将近一半 的时间是花费在 Layout,点击右侧进入源码:
在这里插入图片描述
分析这段代码我们已经可以知道问题出在哪里了,读取offsetTop会触发回流重绘,这里用了个 for 循环,所以当小球的数量越来越多的时候,不断的读取 offsetTop 属性,导致频繁的触发回流重绘,最终页面卡顿。在 for 循环中不断读取 offsetTop 属性,这就是根源。

频繁的回流重绘导致卡顿

  1. 为什么频繁的回流和重绘会导致卡顿?
  • ✅ 计算复杂度高
    回流(Reflow)是指浏览器重新计算元素的位置、尺寸等几何属性,并更新渲染树的过程。这一过程通常需要遍历整个 DOM 树并重新计算样式信息,在页面结构复杂、元素数量多的情况下,其计算开销非常大。

  • ⚠️ 渲染流程被打断
    一旦发生回流,浏览器往往需要暂停当前的渲染流程,先完成布局的重新计算,然后再进行绘制(Repaint)和合成(Composite)。这种中断行为可能导致页面出现短暂的停顿或闪烁,影响视觉体验。

  • 📉 高频触发带来性能压力
    如果在用户交互过程中频繁触发回流和重绘(例如滚动、动画或动态修改样式时),会显著增加主线程负担,导致帧率下降,进而影响页面流畅性。

因此,频繁的回流与重绘本质上属于高耗时任务,它们会抢占本应用于渲染的时间片,延迟页面在一帧内的完成时间,从而造成视觉上的卡顿体验。

  1. 为什么读取 offsetTop 等属性会触发回流?
    这与浏览器的渲染优化机制有关。
  • 为了提升性能,现代浏览器通常会对样式的更改进行队列化处理,即不会立即执行布局更新,而是将多个变更合并后批量处理,以减少不必要的计算。

  • 然而,当尝试访问比如 offsetTop、offsetLeft、clientWidth、scrollHeight 等布局相关属性时,浏览器必须提供最新的布局信息,这就要求它立即清空样式变更队列并同步执行回流操作,以确保返回的值是准确的。

这一强制刷新行为可能会打断原本可以合并的异步渲染流程,进而引发额外的回流甚至重绘,带来不必要的性能损耗。

✅ 总结

  • 频繁的回流与重绘会占用大量主线程资源,影响一帧内任务的完成效率,是造成页面卡顿的重要原因。
  • 读取某些布局属性(如 offsetTop)会强制浏览器提前刷新变更队列,触发回流,从而破坏了原本可能被优化的渲染流程。

了解这些机制有助于我们在开发中避免不必要的布局抖动(Layout Thrashing),从而提升页面性能与用户体验。

既然我们已经明确了性能问题的根源在于 offsetTop 的使用,优化的关键就在于避免访问那些会同步触发回流的布局属性。接下来,我们通过一个实际示例来看一下具体的优化实现方式:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9fa0c79840ec44e198754189b61dcbe1.png
使用 style.top 替代 offsetTop 可以有效避免因读取元素几何信息而触发的强制回流,从而显著提升页面性能,解决卡顿问题。这一优化之所以有效,是因为它规避了浏览器在获取布局信息时所引发的同步回流与重排流程,减少了主线程的阻塞时间。
这背后涉及到浏览器的渲染机制,特别是与 布局重排(Reflow)和重绘(Repaint)密切相关。

浏览器渲染机制

🧪浏览器渲染流程简析

浏览器在将页面内容呈现到屏幕上的过程中,通常会经历以下几个关键阶段:

  • 布局(Layout):也称为重排(Reflow),负责计算元素的几何信息,包括大小和位置。
  • 绘制(Paint):根据元素的样式信息,将其绘制为像素图像,这个过程也称为重绘。
  • 合成(Composite):将各个图层按照正确的顺序合并,并最终显示在屏幕上。

其中,布局(Layout)是最耗性能的步骤,因此应尽量避免频繁触发。

🔍offsetTop 与 style.top 的区别

offsetTop

  • 是一个只读属性,用于获取元素相对于其定位祖先元素顶部的距离。
  • 访问该属性时,浏览器必须确保返回的是最新的布局信息,因此可能会强制执行一次同步布局(即“强制回流”)。
  • 如果在循环或动画中频繁访问 offsetTop,会导致多次不必要的重排操作,显著影响页面性能。

style.top

  • 是一个可读写的样式属性,用于设置或获取元素的内联 top 样式值。
  • 修改或读取该属性不会触发布局计算,仅会影响后续的绘制或合成。
  • 因此,使用 style.top 相比之下对性能的影响要小得多。

✅为什么使用 style.top 不会卡顿?

当通过 style.top 修改元素的位置时,浏览器不需要重新计算整个页面的布局,只需更新对应元素的视觉表现即可。这一步通常只会触发「绘制」或「合成」,而不会引发「重排」,因此性能更优。

相反,如果频繁使用 offsetTop,尤其是在循环或动画逻辑中,会导致频繁的强制回流,增加主线程负担,从而造成页面卡顿。

📌总结

  • 使用 offsetTop 会触发浏览器进行同步布局,可能引发重排,影响性能。
  • 使用 style.top 则仅修改样式信息,不涉及布局计算,性能更佳。
  • 将代码中对 offsetTop 的访问替换为对 style.top 的操作,可以有效减少不必要的重排,提升页面渲染效率。
  • 若实现动画效果,建议结合使用 requestAnimationFrame 进行更新调度,以进一步优化性能。
requestAnimationFrame(() => {
  element.style.top = `${newTop}px`;
});

🌟感谢阅读,如果你在阅读过程中发现任何问题,或有改进建议,也欢迎在评论区指出,我会及时修正并持续优化内容。

如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀

在这里插入图片描述

评论 9
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水煮白菜王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值