一次渲染十万条数据:前端技术优化(上)
今天看了一篇文章,写的是一次性渲染十万条数据的方法,本文内容是对这篇文章的学习总结,以及知识点补充。
在现代Web应用中,前端经常需要处理大量的数据展示,例如用户评论、商品列表等。直接渲染大量数据会导致浏览器性能问题,如卡顿和延迟。本文将探讨几种优化策略,帮助开发者提高网页性能,优化用户体验。
方法一:通过document直接渲染十万条数据
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一次性渲染十万条数据</title>
</head>
<body>
<ul id="app"></ul>
</body>
<script>
const app = document.querySelector('#app')
let now = Date.now()
// 方法一:一次性渲染十万条数据
for (let i = 0; i < 100000; i++) {
const li = document.createElement('li')
li.innerText = `我是第${i + 1}条数据`
app.appendChild(li)
}
console.log('js运行耗时', Date.now() - now)
setTimeout(() => {
console.log('dom渲染耗时', Date.now() - now)
})
</script>
</html>
结果
问题
当页面需要一次性渲染大量数据时,直接将所有数据渲染到DOM中会迅速消耗浏览器资源,造成性能瓶颈。这种方法虽然简单,但会导致浏览器响应缓慢,用户体验差。
补充知识1: JS事件循环之宏任务和微任务
在 JavaScript 中,事件循环是处理并发的机制,它允许执行非阻塞的操作。事件循环的核心概念包括宏任务(macro tasks)和微任务(micro tasks)。它们的执行顺序和机制十分重要,理解它们有助于更好地编写异步代码。
宏任务与微任务
-
宏任务 (Macro Task):
- 宏任务是较大粒度的任务,它通常由以下几部分组成:
- 整个脚本(执行的上下文)
setTimeout
setInterval
I/O
操作(如网络请求)
- 宏任务的执行是按照创建顺序依次执行的。
- 宏任务是较大粒度的任务,它通常由以下几部分组成:
-
微任务 (Micro Task):
- 微任务是相对较小粒度的任务,通常用于处理短小的异步操作,主要由以下几部分组成:
Promise
的.then()
和.catch()
MutationObserver
- 微任务在当前宏任务执行完毕后立即执行,且在浏览器进行下一次重绘之前完成所有微任务。这意味着微任务的优先级高于宏任务。
- 微任务是相对较小粒度的任务,通常用于处理短小的异步操作,主要由以下几部分组成:
执行顺序
- 首先,事件循环从宏任务队列中取出一个宏任务并执行。
- 执行完宏任务后,查看微任务队列,执行所有微任务,直到微任务队列为空。
- 一旦微任务队列为空,浏览器会进行渲染(重绘),然后再从宏任务队列中取出下一个宏任务。
- 重复这个过程,直到所有任务都完成。
示例
下面是一个简单的示例,帮助理解宏任务和微任务的执行顺序:
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
new Promise((resolve, reject) => {
console.log('Promise 1');
resolve('Promise 1 resolved');
}).then((res) => {
console.log(res);
});
process.nextTick(() => {
console.log('Next Tick');
});
setTimeout(() => {
console.log('Timeout 2');
}, 0);
new Promise((resolve, reject) => {
console.log('Promise 2');
resolve('Promise 2 resolved');
}).then((res) => {
console.log(res);
});
console.log('End');
执行输出
执行上面的代码将会输出以下内容:
Start
Promise 1
Promise 2
End
Promise 1 resolved
Promise 2 resolved
Next Tick
Timeout 1
Timeout 2
- 在 JavaScript 的事件循环中,宏任务的执行优先于微任务,但在宏任务完成后,微任务会立即执行,确保在下一个宏任务开始之前执行所有微任务。
- 理解这一流程能够帮助开发者更好地预测和管理异步代码的执行顺序及其结果。
补充知识2: 执行渲染操作,更新界面为什么发生在执行 setTimeout之前?
在 JavaScript 中,事件循环和异步操作的处理方式是理解 setTimeout
和界面更新之间关系的关键。
界面更新的时机
在 JavaScript 中,界面更新通常在以下几个时刻发生:
- 当主线程空闲时(即没有其他任务在执行时),浏览器会进行界面更新。
- 在一个宏任务(如
setTimeout
)执行之后,浏览器会检查微任务队列并执行所有的微任务,然后再进行界面更新。
为什么界面更新发生在执行 setTimeout
之前
- 执行顺序:当你调用
setTimeout
时,传入的回调函数不会立即执行,而是被放入宏任务队列中。主线程会继续执行当前的任务。 - 界面更新:在当前任务结束后,浏览器会检查是否有需要更新的界面。此时,如果有 DOM 的改变(例如通过某个函数修改了 DOM),浏览器会更新界面。
- 执行
setTimeout
的回调:在主线程空闲并完成微任务后,浏览器会从宏任务队列中取出setTimeout
的回调并执行。
示例
console.log("Start");
setTimeout(() => {
console.log("Inside setTimeout");
}, 0);
console.log("End");
输出顺序将是:
Start
End
Inside setTimeout
在 JavaScript 中,界面更新发生在当前任务完成后和 setTimeout
回调执行之前。理解这一点对于优化性能和确保用户界面流畅性非常重要。
补充知识3: 回流与重绘
页面的回流与重绘是指浏览器在渲染网页时的两种重要过程。
- 回流(Reflow):当页面的结构或内容发生变化时(比如增加、删除或修改元素的大小、位置等),浏览器需要重新计算元素的几何属性,以确定它们的位置和大小。这一过程称为回流。回流会影响整个文档的布局,因此较为耗性能。
- 重绘(Repaint):当元素的外观发生变化(比如背景色、字体颜色等)但并不影响布局时,浏览器只需重新绘制该元素,而无需重新计算其几何属性。这一过程称为重绘。重绘通常比回流消耗的性能要少。
总结来说,回流是布局计算,重绘是外观更新。在优化网页性能时,应尽量减少这两个过程的发生。
方法二:分批渲染
为了解决直接渲染带来的性能问题,我们可以采用分批渲染的方法。通过将数据分成小块,逐一渲染,可以减轻浏览器的即时负担。
使用 setTimeout
进行分批渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一次性渲染十万条数据</title>
</head>
<body>
<ul id="app"></ul>
</body>
<script>
const app = document.querySelector('#app')
let now = Date.now()
// 方法二:分批渲染十万条数据
const total = 100000
const loadOnce = 20
const page = total / loadOnce
const index = 0
function renderData(curTotal, curIndex) {
let pageCount=Math.min(loadOnce,curTotal)
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
const li = document.createElement('li')
li.innerText = `我是第${i}条数据`
app.appendChild(li)
}
renderData(curTotal-pageCount, curIndex + pageCount)
})
}
renderData(total, index)
</script>
</html>
结果
问题
当用户往下翻的时候有可能那一瞬间看不到东西
方法三、使用requestAnimationFrame替代setTimeout
requestAnimationFrame
是一种更高效的分批渲染方法,它允许在浏览器的绘制周期中执行动画和渲染,从而提高性能。
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一次性渲染十万条数据</title>
</head>
<body>
<ul id="app"></ul>
</body>
<script>
const app = document.querySelector('#app')
let now = Date.now()
// 方法三:使用requestAnimationFrame渲染十万条数据
const total = 100000
const loadOnce = 20
const page = total / loadOnce
const index = 0
function renderData(curTotal, curIndex) {
let pageCount=Math.min(loadOnce,curTotal)
requestAnimationFrame(() => {
for (let i = 0; i < pageCount; i++) {
const li = document.createElement('li')
li.innerText = `我是第${i}条数据`
app.appendChild(li)
}
renderData(curTotal-pageCount, curIndex + pageCount)
})
}
renderData(total, index)
</script>
</html>
补充知识4: 什么是requestAnimationFrame?
requestAnimationFrame
是一个用于优化网页动画效果的 JavaScript 方法。它指示浏览器在下次重绘之前调用指定的回调函数,从而实现基于帧的动画效果。使用 requestAnimationFrame
的一个主要优点是它能够根据浏览器的绘制频率来调整动画的更新速率,从而使动画更加流畅和高效。
使用场景:
- 平滑动画:如果你在进行平移动画、旋转、缩放等效果时,使用
requestAnimationFrame
可以确保动画的每一帧都在浏览器的绘制周期内更新,这有助于避免由于使用setTimeout
或setInterval
而导致的帧率不稳定。 - 减少 CPU 消耗:
requestAnimationFrame
还具有智能调节的功能,特别是在用户切换标签页或者浏览器窗口不在视野内时,它会自动停止调用回调函数,从而节省资源。
基本用法:
function animate() {
// 更新动画状态
// ...
// 请求下一帧
requestAnimationFrame(animate);
}
// 开始动画
requestAnimationFrame(animate);
在上面的代码中,animate
函数会执行动画的逻辑,并通过 requestAnimationFrame(animate)
请求下一帧的更新。这样,animate
函数会在浏览器准备好下一个绘制周期时被调用。
优势:
-
帧率同步:
requestAnimationFrame
会使动画的帧率与浏览器的刷新率(通常是60帧每秒)同步,从而提高流畅性。 -
性能优化:当页面不在视野中时,
requestAnimationFrame
会暂停动画的执行,从而减少 CPU 和 GPU 的使用。 -
简洁易用:API 简单直观,更容易使用来控制动画的生命周期。
总之,使用 requestAnimationFrame
是在现代网页应用中实现高性能动画的推荐方式。
方法四:利用 DocumentFragment
DocumentFragment
是一个轻量级的文档对象,可以用于在内存中组装一组节点,然后一次性添加到DOM中,减少DOM操作次数。
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一次性渲染十万条数据</title>
</head>
<body>
<ul id="app"></ul>
</body>
<script>
const app = document.querySelector('#app')
let now = Date.now()
// 方法四:使用文档碎片渲染十万条数据
const total = 100000
const loadOnce = 20
const page = total / loadOnce
const index = 0
function renderData(curTotal, curIndex) {
let documentFragment = document.createDocumentFragment()
let pageCount=Math.min(loadOnce,curTotal)
requestAnimationFrame(() => {
for (let i = 0; i < pageCount; i++) {
const li = document.createElement('li')
li.innerText = `我是第${i}条数据`
documentFragment.appendChild(li)
}
app.appendChild(documentFragment)
renderData(curTotal-pageCount, curIndex + pageCount)
})
}
renderData(total, index)
</script>
</html>
总结
在处理大量数据渲染时,选择合适的方法至关重要。直接渲染虽然简单,但性能较差。分批渲染、requestAnimationFrame
和 DocumentFragment
提供了更优的性能解决方案。开发者应根据具体情况选择最合适的方法,以确保应用的流畅性和用户体验。
补充知识5:什么是DocumentFragment?
DocumentFragment
是一个轻量级的文档对象,表示一个可以包含多个节点的虚拟容器。它不是文档中的实际部分,而是用来在内存中组装一组节点,然后一次性地将它们添加到文档中。这种做法可以提高性能,因为它减少了对 DOM 的多次操作,降低了重绘和重排的次数。
作用和优势:
-
性能优化:直接多次操作 DOM 会导致浏览器频繁地重绘和重排,从而降低性能。使用
DocumentFragment
可以将多个 DOM 操作合并为一个,从而显著提高性能。 -
内存管理:
DocumentFragment
只存在于内存中,直到你将它的内容添加到实际的 DOM 中。这使得在构建大型 DOM 结构时更节省内存。 -
灵活性:你可以使用
DocumentFragment
来临时组装和修改多个节点,然后一次性插入到 DOM 中,保持 DOM 的一致性。
基本用法:
下面是一个简单的例子,展示如何使用 DocumentFragment
来添加多个节点到 DOM 中:
// 创建一个 DocumentFragment
const fragment = document.createDocumentFragment();
// 创建几个新的元素
const li1 = document.createElement('li');
li1.textContent = 'Item 1';
const li2 = document.createElement('li');
li2.textContent = 'Item 2';
const li3 = document.createElement('li');
li3.textContent = 'Item 3';
// 将元素添加到 DocumentFragment 中
fragment.appendChild(li1);
fragment.appendChild(li2);
fragment.appendChild(li3);
// 将 DocumentFragment 追加到现有的 DOM 中
document.getElementById('myList').appendChild(fragment);
在上面的示例中,使用 DocumentFragment
创建并组合了多个 li
元素,最后一次性将它们添加到一个列表中。这样可以避免在将每个 li
添加到 DOM 时引起的多次重排和重绘。
DocumentFragment
是一个非常有用的工具,适用于需要频繁与 DOM 交互时,帮助保持性能和优化操作。在需要插入或修改多个节点时,使用 DocumentFragment
是个不错的选择。
参考文章:
https://juejin.cn/post/7407763018471948325