浏览器渲染一帧都做了什么?

2,603 阅读3分钟

简介

对于从输入URL到看到界面这个经典的面试题,每个同学都能娓娓道来。其中包括DNS解析、缓存、渲染,而渲染过程又包括DOM和CSS解析、布局树的计算、分层、绘制、分块、光栅化、合成。

当首屏界面渲染完后,在应用的运行过程中,还会继续渲染,那么这个渲染过程是什么样的呢?

image.png

首先,浏览器会定时刷新界面,不管js有没有改变dom。通常刷新频率和显示器帧率相同,大部分显示器帧率是60fps,≈16ms每帧。

如果渲染某一帧时候,DOM并未发生改变,就不需要启动主线程计算,如果DOM发生了变化,就可能触发重排或重绘,这时候就会启动主线程,重复上面提到的渲染过程。

在定时刷新帧的间隔,还会有其他的任务。比如用户可能会触发一些元素事件(输入文本点击按钮等),还可能有定时任务或者异步任务执行,这些任务的执行是会阻塞渲染的。

除了上面的任务,还有requestAnimationFrame和requestIdleCallback。在每一帧渲染完成后,在下一帧绘制之前如果有requestAnimationFrame,则调用之。如果在一帧渲染完后有空闲,就会执行requestIdleCallback注册的回调什么是空闲时间呢,以60fps为例,如果浏览器渲染一帧发现不到16ms,那么剩余时间就算是空闲时间。

requestAnimationFrame

通常我们可以通过定时器(setTimeout/setInterval)实现一个动画,但是定时器时间并不精确,如果时间太短,那么可能造成多余的操作,消耗CPU,如果时间长,就会导致动画不流畅。另外当画面不展示时候,定时器依然执行,导致不必要的CPU资源消耗,耗电更快。

requestAnimationFrame实现动画可以解决上面两个问题。

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

使用:

接受一个回调,在下次渲染前执行,注意timestamp参数,在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳。

该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻,这个时间用来进行动画参数的计算。

参考官方示例

const element = document.getElementById('some-element-you-want-to-animate');
let start;

function step(timestamp) {
  if (start === undefined)
    start = timestamp;
  const elapsed = timestamp - start;

  //这里使用`Math.min()`确保元素刚好停在200px的位置。
  element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';

  if (elapsed < 2000) { // 在两秒后停止动画
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

由于该方法只是通知浏览器下一次绘制之前执行一次callback,因此实现动画时候需要每次重新调用。

requestAnimationFrame返回一个id,取消回调的方法是cancelAnimationFrame,请看官方示例

var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
                            window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;

var start = window.mozAnimationStartTime;  // 只有Firefox支持mozAnimationStartTime属性,其他浏览器可以使用Date.now()来替代.

var myReq;
function step(timestamp) {
  var progress = timestamp - start;
  d.style.left = Math.min(progress/10, 200) + "px";
  if (progress < 2000) {
    myReq = requestAnimationFrame(step);
  }
}
myReq = requestAnimationFrame(step);

window.cancelAnimationFrame(myReq);

需要注意动画中元素样式最好根据时间进行计算,而不是使用需要样式计算的DOM API(例如ele.style.offsetWidth),否则会强制重新渲染。

requestIdleCallback

接受一个回调,回调在浏览器空闲时间执行。注意如果浏览器一直渲染没有空闲,可能就一直执行不到requestIdelCallback注册的回调,因此可以设置一个超时时间,超时之后会执行注册的回调。

取消回调的方法是 cancelIdleCallback

var handle = window.requestIdleCallback(callback, {timeout: 1000});

cancelIdleCallback(handle);

注意,requestIdleCallback用来执行优先级低的任务,由于其执行时机不可控,因此尽量不要执行DOM操作。