开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
常见的浏览器页面刷新频率是每秒 60 次,也就是 60fps,一帧的时间在 16.7ms 左右。一些针对游戏市场开发的屏幕或移动端设备可以支持到120fps。帧率变化可以通过 chrome devtools 查看:打开 devtool -> more tools -> rendering -> Frame Rendering Stats
没有滚动优化的例子:
import React, { FC, useLayoutEffect } from 'react';
const Index: FC = () => {
useLayoutEffect(() => {
const fn = () => console.log('scrollY:', window.pageYOffset);
window.addEventListener('scroll', fn);
return () => window.removeEventListener('scroll', fn);
}, []);
return <div/>;
};
上面的简单组件实现滚动时打印滚动距离,尝试运行可以看到输出:
scrollY: 1
scrollY: 13
scrollY: 19
scrollY: 27
scrollY: 34
scrollY: 42
scrollY: 49
scrollY: 57
scrollY: 66
scrollY: 74
scrollY: 82
scrollY: 89
scrollY: 96
scrollY: 100
scrollY: 103
scrollY: 105
scrollY: 106
滚轮只是轻微触发一下就执行了很多次,scroll 触发频率远高于浏览器的刷新频率,如果 fn 是一个复杂函数,在单次函数调用时间内没能完成函数执行,滚动时会出现掉帧,出现页面抖动或滚动不连续的问题,影响性能、电池寿命用户体验。
常见的解决方法就是通过 debounce 函数实现去抖动(使用节流做法也可以),比如下面代码:
import React, { FC, useLayoutEffect } from 'react';
import debounce from 'lodash/debounce';
const Index: FC = () => {
useLayoutEffect(() => {
const fn = debounce(() => console.log('scrollY:', window.pageYOffset), 250);
window.addEventListener('scroll', fn);
return () => window.removeEventListener('scroll', fn);
}, []);
return <div/>;
};
驱动都的时间间隔可手动配置,上面配置 250ms 去抖,尝试触发打印:
scrollY: 106
scrollY: 159
可以看到函数的执行频率明显下降,而且滚动触发间隔较大。debounce 函数通常通过 setTimeout 函数实现。
requestAnimationFrame 是一个浏览器内置的 API(下面简称 rAF),传入一个回调函数作为参数,要求浏览器在下次重绘之前进行调用,执行频率一般是60Hz,根据 W3C 规范,一般会跟随屏幕刷新次数,比如浏览器掉帧到 30fps,rAF 的执行频率也会是 30次每秒。
兼容写法:
window.rAF = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
使用 rAF 重写上面的滚动触发组件:
import React, { FC, useLayoutEffect } from 'react';
const Index: FC = () => {
useLayoutEffect(() => {
let ticking = false;
const fn = () => {
console.log('scrollY:', window.pageYOffset);
ticking = false;
};
const onScroll = () => {
if (!ticking) {
requestAnimationFrame(fn);
ticking = true;
}
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return <div/>;
};
这里用到一个技巧,使用 ticking 记录 fn 是否执行完成,如果未完成,直接跳出不执行,这样能保证 fn 的执行跟随浏览器渲染帧,不会出现无效渲染。
- 参考资料
Timing control for script-based animations
The most accurate way to schedule a function in a web browser