帧率(FPS)

每秒的帧数(fps)或者说帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说30fps就是可以接受的,但是将性能提升至60fps则可以明显提升交互感和逼真感,但是一般来说超过75fps一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过刷新率的帧率就浪费掉了。

浏览器渲染帧的过程

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

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

耗时 JS 会造成丢帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JavaScript 忙的状态(持续 5s)下每隔 1s 新增一行 DOM 内容
// 可以观察到虽然每秒都会写一次 DOM,但在 5s 结束后才会全部渲染出来,明显耗时脚本阻塞了渲染
<div id="message"></div>
<script>
var then = Date.now()
var i = 0
var el = document.getElementById('message')
while (true) {
var now = Date.now()
if (now - then > 1000) {
if (i++ >= 5) {
break;
}
el.innerText += 'hello!\n'
console.log(i)
then = now
}
}
</script>

测量渲染帧间隔 (requestAnimationFrame)

requestAnimationFrame 使用这个 API 可以请求浏览器在下一个渲染帧执行某个回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var then = Date.now();
var count = 0;
var reportCount = 20;

function nextFrame(){
requestAnimationFrame(function(){
count ++
if(count % reportCount === 0){
var time = (Date.now() - then) / count;
var ms = Math.round(time*1000) / 1000; // 每一帧耗时
var fps = Math.round(100000/ms) / 100; // 1秒钟渲染了多少帧
console.log(`count: ${count}\t${ms}ms/frame\t${fps}fps`);
}
nextFrame()
})
}
nextFrame()

优化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let lastTime = performance.now()
let frame = 0
let lastFameTime = performance.now()
let fpsList = [];
const reportCount = 20; // 每20秒上报一次

function loop(time) {
let now = performance.now()
let fs = (now - lastFameTime)
lastFameTime = now
let fps = Math.round(1000 / fs)
frame++
if (now > 1000 + lastTime) {
fps = Math.round(( frame * 1000 ) / ( now - lastTime ))
frame = 0
lastTime = now
addFpsPreSecond(fps);
}
window.requestAnimationFrame(loop)
}
loop();

function addFpsPreSecond (fps) {
fpsList.push(fps);
if (fpsList.length >= reportCount) {
const avgFps = fpsList.reduce((acc, val) => acc + val, 0) / fpsList.length;
fpsList = [];
reportAvgFps(avgFps);
}
}

// 进行上报
function reportAvgFps (avgFps) {
console.log(`20s 内平均帧率为:${avgFps}`);
}