如何减少项目的白屏时间,优化页面的卡顿
问题背景
我们来看下需求,我们需要在页面中渲染多个组件元素,不断插入页面,举例如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="main" style="display: flex;flex-wrap: wrap;"> </div> </body> <script> // 假设有一个包含大量数据的数组 function processBatch() { for (let i = 0; i < 199999; i++) { // 渲染数据到页面,例如创建 DOM 元素、更新内容等操作 const div = document.createElement('div'); div.style.width=100+'px'; div.style.height=100+'px'; div.style.background = 'red'; div.style.margin='20px'; document.getElementById('main').appendChild(div); } processBatch() </script> </html>
我们来看下这个页面的渲染过程以及Performance性能分析
我们可以看到在开始的时候会有一段时间的白屏卡顿,如果我们不处理的话,数据量再大,白屏时间会越来越长。
可以看到有两帧都有长时间的Loog task 阻塞了主任务,这就是我们需要优化的部分,因为浏览器是多线程的,但是js是单线程的,我们需要拆分长任务,可以使用webwork多线程,可以参考我上篇文章 https://www.cnblogs.com/ximenchuifa/p/17789762.html ,我们接下来主要介绍 requestAnimationFrame 渲染帧,是浏览器用于定时循环操作的一个接口,类似于setTimeout,setInterval,但是它们有着严重的性能问题,requestAnimationFrame是一个专门为实现高性能的帧动画而设计的API。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="main" style="display: flex;flex-wrap: wrap;"> </div> </body> <script> // 假设有一个包含大量数据的数组 const data = [/* 大量数据 */]; for (let index = 0; index < 9999; index++) { data.push(index) } // 每帧处理的数据数量 const batchSize = 100; // 当前处理的数据索引 let currentIndex = 0; // 处理和渲染一批数据的函数 function processBatch() { for (let i = 0; i < 199999; i++) { // 处理当前索引的数据 const item = data[currentIndex]; // 渲染数据到页面,例如创建 DOM 元素、更新内容等操作 const div = document.createElement('div'); div.style.width=100+'px'; div.style.height=100+'px'; div.style.background = 'red'; div.style.margin='20px'; document.getElementById('main').appendChild(div); // 增加当前索引 currentIndex++; // 判断是否还有数据需要处理 if (currentIndex >= data.length) { // 数据处理完成 return; } } // 在下一帧继续处理下一批数据 requestAnimationFrame(processBatch); } // 开始处理和渲染数据 requestAnimationFrame(processBatch); processBatch() </script> </html>
再看下性能分析
再看下发现没有卡顿,很流畅,体验也很丝滑,性能分析也没有警告和Look task了,但是有可能总的渲染时间比起前多。在改进的代码中我们可以看到有个每帧处理的数据数量batchSize,如何确定这个值呢?
确定合适的 batchSize
大小以平衡处理时间和渲染性能是一个挑战,因为它取决于多个因素,包括数据量的大小、处理逻辑的复杂度、设备性能等。下面是一些方法和建议来帮助确定合适的 batchSize
大小:
-
设备性能考量:不同设备的性能不同,因此
batchSize
的大小可能需要根据目标设备进行调整。较低性能的设备可能需要较小的batchSize
,以避免过多的计算和渲染压力。 -
实时性需求:如果你需要实时性的渲染效果,较小的
batchSize
可能更合适。这样可以更频繁地在每一帧中进行渲染,提供更实时的更新效果。 -
用户体验:根据用户的反馈和感知,调整
batchSize
的大小。如果用户觉得页面渲染速度不够流畅,可以尝试减小batchSize
,反之亦然。 -
性能监测和优化:使用浏览器的开发者工具进行性能监测,观察每帧的处理时间和页面渲染性能。通过不同的
batchSize
大小进行测试,并分析性能指标,找到一个平衡点。 -
增量调整:从一个较小的
batchSize
开始,逐渐增加batchSize
的大小,同时观察性能和用户体验的变化。一旦达到性能和用户体验的平衡点,就可以确定合适的batchSize
大小。
请注意,batchSize
的最佳值可能因不同的应用场景和具体需求而有所不同。因此,根据实际情况进行试验、优化和调整是找到最佳 batchSize
大小的关键。
我们可以将其封装成一个通用函数。本文将介绍如何将 requestAnimationFrame
封装成一个通用函数,以便在需要的地方快速调用。
封装通用函数
下面是一个将 requestAnimationFrame
封装成通用函数的示例代码:
function startAnimationFrame(callback) { let isRunning = true; function frame() { if (!isRunning) { //取消帧的请求,结束操作 return; } callback(); requestAnimationFrame(frame); } requestAnimationFrame(frame); return function stop() { isRunning = false; //在下一次不会继续 }; }
你可以将 processBatch
函数作为参数传递给 startAnimationFrame
函数,它会在每一帧开始时调用 processBatch
函数。该函数还会返回一个 stop
函数,用于停止动画循环。例如:
function processBatch() { // 处理和渲染数据的逻辑 } const stopAnimation = startAnimationFrame(processBatch); // 调用 stopAnimation 函数以停止动画循环 // stopAnimation();
通过上述代码,我们将 processBatch
函数作为参数传递给 onNextFrame
函数,它将在下一帧开始时调用该函数。这确保了处理函数将在当前帧渲染完成后执行,提供了更好的用户体验。
优势和注意事项
通过封装 requestAnimationFrame
成通用函数,我们可以获得以下优势:
-
更好的性能: 在下一帧开始时执行函数可以避免阻塞主线程,提高页面的响应性能。
-
更流畅的动画: 在动画场景中,使用
requestAnimationFrame
可以确保每一帧的渲染完成后再进行下一次更新,从而产生更流畅的动画效果。