渲染1w个节点的不同方式
1 )案例1:一次渲染1w个节点
<div id='root'><div><script type="text/javascript">function randomHexColor() {return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);}setTimeout(function() {var k = 0;var root = document.getElementById("root");for(var i = 0; i < 10000; i++) {k += new Date - 0 ;var el = document.createElement("div");el.innerHTML = k;root.appendChild(el);el.style.cssText = `background: ${randomHexColor()};height: 40px`;}},1000);
</script>
1 次完成 1w 个节点的渲染
可以看到,圈中的部分,明显被阻塞,这一块绝对会导致用户体验很差
2 )案例2: 1w 个节点分100次执行,每次执行100个
<div id='root'><div><script type="text/javascript">var root = document.getElementById("root");function randomHexColor() {return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);}function loop(n) {var k = 0;console.log(n);for(var i = 0; i < 100; i++) {k += new Date - 0 ;var el = document.createElement("div");el.innerHTML = k;root.appendChild(el);el.style.cssText = `background: ${randomHexColor()};height: 40px`;}if (n) {// 内部再调用 n-1次 loop,总计调用 n 次 loopsetTimeout(function() {loop(n - 1);}, 40)}}// 1s 执行一次setTimeout(function() {loop(100)}, 1000);
</script>
- 这里,40ms 执行一次,100次,共4s执行完成
- 可以看到,没有明显的阻塞
为什么会有如此区别
- 究其原因是因为浏览器是单线程,它将GUI描绘,时间器处理,事件处理,JS执行远程资源加载统统放在一起,当做某件事,只有将它做完才能做下一件事
- 如果有足够的时间,浏览器是会对我们的代码进行编译优化(JIT) 及进行热代码优化,一些DOM操作,内部也会对reflow进行处理
- reflow是一个性能黑洞,很可能让页面的大多数元素进行重新布局
- 所以,浏览器歇一会儿跑一会儿,比一直跑性能更好
时间分片技术方案选择
1 )注意兼容问题
- 注意,在 request18 版本并没有使用
requestIdleCalback
, 仍旧因为兼容问题
2 ) 方案选择
- 在浏览器环境中,常见的macro task有setTimeout、MessageChannel、postMessage
- 而常见的 micro task 有 MutationObsever, Promise.then
2.1 微任务问题
- 为什么不使用微任务呢?
- 宏任务是在下一轮事件循环中执行,微任务是在当前时间循环中执行
- 微任务将在页面更新前全部执行完,也就是是要在当前帧执行完的,所以达不到「将主线程还给浏览器」的目的
- 使用微任务就会造成一个问题,在极端情况下,一直卡在当前帧,因为微任务是在当前帧执行完的(本轮事件循环内)
- 所以在 setTimeout, MessageChannel, postMessage 三选一
- 为什么不使用setTimeout(fn,0)呢?
function timer() {console.ltme('计时器')setTimeout(() => {console.timeEnd('计时器')timer()}, 0) }timer()
- 递归的setTimeout()调用会使调用间隔变为4ms,导致浪费了4ms
- 这是一个递归的定时器,在递归的时候,看下它的间隔时间
- 在定时器递归大约4次以后,它的递归时间就变成了4ms~ 5ms 变得不准确
- 明明我们指定的是 0ms, 这里差距有些大了,所以这个api有些问题,误差太大
2.2 宏任务问题
-
为什么不使用 rAF() 呢?
- 如果上次任务调度不是rAF()触发的,将导致在当前帧更新前进行两次任务调度
- 页面更新的时间不确定,如果浏览器间隔了10ms才更新页面,那么这10ms就浪费了
-
React Scheduler使用MessageChannel的原因为:生成宏任务,实现:
- 将主线程还给浏览器,以便浏览器更新页面
- 浏览器更新页面后继续执行未完成的任务
const { port1, port2 } = new MessageChannel();port1.onmessage = function (event){console.log('收到来自port2的消息:', event.data);//收到来自port2的消息: pong };port2.onmessage = function (event) {console.log('收到来自port1的消息:', event.data);//收到来自port1的消息: pingport2.postMessage('pong'); }; port1.postMessage('ping');