由于现在广泛使用的屏幕都有固定的刷新率(比如最新的一般在 60Hz), 在两次硬件刷新之间浏览器进行两次重绘是没有意义的只会消耗性能。 浏览器会利用这个间隔 16ms(1000ms/60)适当地对绘制进行节流, 因此 16ms 就成为页面渲染优化的一个关键时间。 尤其在异步渲染中,要利用 流式渲染 就必须考虑到这个渲染帧间隔。

TL;DR

为方便查阅源码和相关资料,本文以 Chromium 的 Blink 引擎为例分析。如下是一些分析结论:

  • 一个渲染帧内 commit 的多次 DOM 改动会被合并渲染;
  • 耗时 JS 会造成丢帧;
  • 渲染帧间隔为 16ms 左右;
  • 避免耗时脚本、交错读写样式以保证流畅的渲染。

渲染帧的流程

渲染帧是指浏览器一次完整绘制过程,帧之间的时间间隔是 DOM 视图更新的最小间隔。 由于主流的屏幕刷新率都在 60Hz,那么渲染一帧的时间就必须控制在 16ms 才能保证不掉帧。 也就是说每一次渲染都要在 16ms 内页面才够流畅不会有卡顿感。 这段时间内浏览器需要完成如下事情:

  • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等
  • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。
  • 布局(Layout):计算布局,执行渲染算法
  • 重绘(Paint):各层分别进行绘制(比如 3D 动画)
  • 合成(Composite):合成各层的渲染结果

最初 Webkit 使用定时器进行渲染间隔控制, 2014 年时开始 使用显示器的 vsync 信号控制渲染(其实直接控制的是合成这一步)。 这意味着 16ms 内多次 commit 的 DOM 改动会合并为一次渲染。

耗时 JS 会造成丢帧

JavaScript 在并发编程上一个重要特点是“Run To Completion”。在事件循环的一次 Tick 中, 如果要执行的逻辑太多会一直阻塞下一个 Tick,所有异步过程都会被阻塞。 一个流畅的页面中,JavaScript 引擎中的执行队列可能是这样的:

  1. 执行 JS -> 空闲 -> 绘制(16ms)-> 执行 JS -> 空闲 -> 绘制(32ms)-> ...

如果在某个时刻有太多 JavaScript 要执行,就会丢掉一次帧的绘制:

  1. 执行很多 JS...(20ms)-> 空闲 -> 绘制(32ms)-> ...

例如下面的脚本在保持 JavaScript 忙的状态(持续 5s)下每隔 1s 新增一行 DOM 内容。

  1. <div id="message"></div>
  2. <script>
  3. var then = Date.now()
  4. var i = 0
  5. var el = document.getElementById('message')
  6. while (true) {
  7. var now = Date.now()
  8. if (now - then > 1000) {
  9. if (i++ >= 5) {
  10. break;
  11. }
  12. el.innerText += 'hello!\n'
  13. console.log(i)
  14. then = now
  15. }
  16. }
  17. </script>

可以观察到虽然每秒都会写一次 DOM,但在 5s 结束后才会全部渲染出来,明显耗时脚本阻塞了渲染。

测量渲染帧间隔

浏览器的渲染间隔其实是很难测量的。即使通过 clientHeight 这样的接口也只能强制进行Layout,是否 Paint 上屏仍未可知。

幸运的是,最新的浏览器基本都支持了 requestAnimationFrame 接口。 使用这个 API 可以请求浏览器在下一个渲染帧执行某个回调,于是测量渲染间隔就很方便了:

  1. var then = Date.now()
  2. var count = 0
  3. function nextFrame(){
  4. requestAnimationFrame(function(){
  5. count ++
  6. if(count % 20 === 0){
  7. var time = (Date.now() - then) / count
  8. var ms = Math.round(time*1000) / 1000
  9. var fps = Math.round(100000/ms) / 100
  10. console.log(`count: ${count}\t${ms}ms/frame\t${fps}fps`)
  11. }
  12. nextFrame()
  13. })
  14. }
  15. nextFrame()

每次 requestAnimationFrame 回调执行时发起下一个 requestAnimationFrame,统计一段时间即可得到渲染帧间隔,以及 fps。逼近 16.6 ms 有木有!

渲染优化建议

现在我们知道浏览器需要在 16ms 内完成整个 JS->Style->Layout->Paint->Composite 流程,那么基于此有哪些页面渲染的优化方式呢?

避免耗时的 JavaScript 代码

耗时超过 16ms 的 JavaScript 可能会丢帧让页面变卡。如果有太多事情要做可以把这些工作重新设计,分割到各个阶段中执行。并充分利用缓存和懒初始化等策略。不同执行时机的 JavaScript 有不同的优化方式:

  • 初始化脚本(以及其他同步脚本)。对于大型 SPA 中首页卡死浏览器也是常事,建议增加服务器端渲染或者应用懒初始化策略。
  • 事件处理函数(以及其他异步脚本)。在复杂交互的 Web 应用中,耗时脚本可以优化算法或者迁移到 Worker 中。Worker 在移动端的兼容性已经不很错了,可以生产环境使用。

避免交错读写样式

在编写涉及到布局的脚本时,常常会多次读写样式。比如:

  1. // 触发一次 Layout
  2. var h = div.clientHeight
  3. div.style.height = h + 20
  4. // 再次触发 Layout
  5. var w = div.clientWidth
  6. div.style.width = w + 20

因为浏览器需要给你返回正确的宽高,上述代码片段中每次 Layout 触发都会阻塞当前脚本。 如果把交错的读写分隔开,就可以减少触发 Layout 的次数:

  1. // 触发一次 Layout
  2. var h = div.clientHeight
  3. var w = div.clientWidth
  4. div.style.height = h + 20
  5. div.style.width = w + 20

小心事件触发的渲染

我们知道 DOM 事件的触发 是异步的,但事件处理器的执行是可能在同一个渲染帧的, 甚至就在同一个 Tick。例如异步地获取 HTML 并拼接到当前页面上, 通过监听 XHR 的 onprogress 事件 来模拟流式渲染:

  1. var xhr = new XMLHttpRequest(),
  2. method = 'GET',
  3. url = 'http://harttle.land'
  4. xhr.open(method, url, true)
  5. xhr.onprogress = function () {
  6. div.innerHTML = xmlhttp.responseText
  7. };
  8. xhr.send()

上述渲染算法在网络情况较差时是起作用的,但不代表它是正确的。 比如当 http://harttle.land 对应的 HTML 非常大而且网络很好时, onprogress 事件处理器可能碰撞在同一个渲染帧中,或者干脆在同一个 Tick。 这样页面会长时间空白,即使 onprogress 早已被调用过。

关于异步渲染的阻塞行为,可参考 http://harttle.land/2016/11/26/dynamic-dom-render-blocking.html

浏览器的 16ms 渲染帧--摘抄的更多相关文章

  1. 浏览器的 16ms 渲染帧

    标签 归档 关于arttle Land 浏览器的 16ms 渲染帧 DOM JavaScript 异步 性能 重绘 由于现在广泛使用的屏幕都有固定的刷新率(比如最新的一般在 60Hz), 在两次硬件刷 ...

  2. document.compatMode(判断当前浏览器采用的渲染方式)

    转载自:http://www.cnblogs.com/fullhouse/archive/2012/01/17/2324706.html IE对盒模型的渲染在 Standards Mode和Quirk ...

  3. 浏览器-04 WebKit 渲染2

    渲染主循环(main loop)和requestAnimationFrame requestAnimationFrame 使用requestAnimationFrame而非setTimeout/set ...

  4. 【repost】浏览器内核、渲染引擎、js引擎

    [1]定义 浏览器内核分成两部分渲染引擎和js引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎 渲染引擎是一种对HTML文档进行解析并将其显示在页面上的工具[2]常见引擎 渲染引擎: firef ...

  5. 浏览器内核、渲染引擎、js引擎

    [1]定义 浏览器内核分成两部分渲染引擎和js引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎 渲染引擎是一种对HTML文档进行解析并将其显示在页面上的工具 [2]常见引擎 渲染引擎: fire ...

  6. 从一个url输入浏览器到页面渲染出来,这个过程都发生了哪些事情?

    经典问题:在浏览器输入一个url后,会发生什么事情呢? (1)假设是简单的http请求(GET),IPV4,无代理. 浏览器先查看浏览器缓存-系统缓存-路由器缓存,若缓存中有,请略过中间步骤,直接跳到 ...

  7. 浏览器-03 WebKit 渲染1

    WebKit是一个渲染引擎,而不是一个浏览器; DOM是对HTML或者XML等文档的一种结构化表示方法,通过这种方式,用户可以通过提供标准的接口来访问页面中的任何元素的相关属性,并可对DOM进行相应的 ...

  8. 【转】浏览器内核、渲染引擎、js引擎

    [1]定义 浏览器内核分成两部分渲染引擎和js引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎 渲染引擎是一种对HTML文档进行解析并将其显示在页面上的工具[2]常见引擎 渲染引擎: firef ...

  9. BOM 浏览器对象模型_渲染引擎_JavaScript 引擎_网页加载流程

    1. 浏览器核心的两个组成部分 渲染引擎 将网页代码渲染为用户视觉可以感知的平面文档 分类: Firefox        Gecko 引擎 Safari        WebKit 引擎 Chrom ...

随机推荐

  1. Nginx: ubuntu系统上如何判断是否安装了Nginx?

    问题描述:ubuntu系统上,如何查看是否安装了Nginx? 解决方法:输入命令行:ps -ef | grep nginx master process后面就是Nginx的安装目录. 延伸:1. 如何 ...

  2. NOIP模拟赛 高级打字机

    [题目描述] 早苗入手了最新的高级打字机.最新款自然有着与以往不同的功能,那就是它具备撤销功能,厉害吧. 请为这种高级打字机设计一个程序,支持如下3种操作: 1.T x:在文章末尾打下一个小写字母x. ...

  3. 【概率dp 高斯消元】bzoj3270: 博物馆

    一类成环概率dp的操作模式 Description 有一天Petya和他的朋友Vasya在进行他们众多旅行中的一次旅行,他们决定去参观一座城堡博物馆.这座博物馆有着特别的样式.它包含由m条走廊连接的n ...

  4. [LUOGU] NOIP提高组模拟赛Day1

    题外话:以Ingress为题材出的比赛好评,绿军好评 T1 考虑枚举第\(i\)个人作为左边必选的一个人,那左边剩余\(i-1\)个人,选法就是\(2^{i-1}\),也就是可以任意选或不选,右侧剩余 ...

  5. H5bulider中的微信支付配置注意事项

    一.云打包安卓自定义证书的生成: 签名算法名称: SHA1withRSA主体公共密钥算法:1024 位 RSA 密钥密钥库类型:JKS 1.下载JDK1.6安装,切换到bin目录,打开命令行: 2.生 ...

  6. apply(), applymap(), map()

    Pandas 中map, applymap and apply的区别  https://blog.csdn.net/u010814042/article/details/76401133/ Panda ...

  7. 串口编程的相关API函数

    用户使用函数CreateFile()创建与指定串口相关联的文件,然后可以使用该函数返回的文件句柄进行串口参数设置.• 01 HANDLE hModem; //定义串口句柄02 hModem=Creat ...

  8. Python虚拟机框架

    Python字节码 我们知道,Python源代码在执行前,会先将源代码编译为字节码序列,Python虚拟机就根据这些字节码进行一系列的操作,从而完成对Python程序的执行.在Python2.5中,一 ...

  9. 理解依赖注入 for Zend framework 2

    依赖注入(Dependency Injection),也成为控制反转(Inversion of Control),一种设计模式,其目的是解除类之间的依赖关系. 假设我们需要举办一个Party,Part ...

  10. python - log日志

    # -*- coding:utf-8 -*- ''' @project: jiaxy @author: Jimmy @file: study_logging.py @ide: PyCharm Comm ...