分片思想解决大数据量渲染问题
使用时间分片技术优化大数据量DOM渲染,避免页面卡顿,提升用户体验
问题
当需要一次性渲染大量数据(如10万条列表项)到页面时,会导致以下问题:
- 页面长时间卡顿,用户体验极差
- 浏览器主线程被长时间占用,无法响应用户操作
- 可能导致页面假死或崩溃
分片渲染的思想是:将大量数据分批次渲染,每次只渲染一小部分,利用 requestAnimationFrame 或 setTimeout 在浏览器空闲时继续渲染,避免阻塞主线程。
解答
/**
* 分片渲染大数据量
* @param {Array} data - 需要渲染的数据数组
* @param {HTMLElement} container - 渲染容器
* @param {Function} renderItem - 单项渲染函数
* @param {Number} chunkSize - 每批次渲染数量
*/
function chunkRender(data, container, renderItem, chunkSize = 100) {
// 数据总量
const total = data.length;
// 当前已渲染的索引
let currentIndex = 0;
// 创建文档片段,减少DOM操作次数
function renderChunk() {
// 如果已经渲染完成,直接返回
if (currentIndex >= total) {
return;
}
// 使用 requestAnimationFrame 在浏览器下一帧渲染
requestAnimationFrame(() => {
// 创建文档片段
const fragment = document.createDocumentFragment();
// 计算本次渲染的结束索引
const endIndex = Math.min(currentIndex + chunkSize, total);
// 渲染当前批次的数据
for (let i = currentIndex; i < endIndex; i++) {
const item = renderItem(data[i], i);
fragment.appendChild(item);
}
// 将文档片段添加到容器
container.appendChild(fragment);
// 更新当前索引
currentIndex = endIndex;
// 继续渲染下一批次
renderChunk();
});
}
// 开始渲染
renderChunk();
}
/**
* 使用 Promise 的分片渲染(支持异步控制)
*/
function chunkRenderAsync(data, container, renderItem, chunkSize = 100) {
return new Promise((resolve) => {
const total = data.length;
let currentIndex = 0;
function renderChunk() {
if (currentIndex >= total) {
resolve();
return;
}
requestAnimationFrame(() => {
const fragment = document.createDocumentFragment();
const endIndex = Math.min(currentIndex + chunkSize, total);
for (let i = currentIndex; i < endIndex; i++) {
const item = renderItem(data[i], i);
fragment.appendChild(item);
}
container.appendChild(fragment);
currentIndex = endIndex;
// 显示进度
const progress = Math.floor((currentIndex / total) * 100);
console.log(`渲染进度: ${progress}%`);
renderChunk();
});
}
renderChunk();
});
}
/**
* 使用 setTimeout 的分片渲染(兼容性更好)
*/
function chunkRenderWithTimeout(data, container, renderItem, chunkSize = 100, delay = 0) {
const total = data.length;
let currentIndex = 0;
function renderChunk() {
if (currentIndex >= total) {
return;
}
setTimeout(() => {
const fragment = document.createDocumentFragment();
const endIndex = Math.min(currentIndex + chunkSize, total);
for (let i = currentIndex; i < endIndex; i++) {
const item = renderItem(data[i], i);
fragment.appendChild(item);
}
container.appendChild(fragment);
currentIndex = endIndex;
renderChunk();
}, delay);
}
renderChunk();
}
使用示例
// 示例1:基础使用
const container = document.getElementById('list');
const data = Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random()
}));
// 定义单项渲染函数
function renderItem(item, index) {
const li = document.createElement('li');
li.className = 'x7o55';
li.textContent = `${item.id}: ${item.name} - ${item.value.toFixed(2)}`;
return li;
}
// 开始分片渲染,每次渲染200条
chunkRender(data, container, renderItem, 200);
// 示例2:使用异步版本,支持进度提示
const loadingEl = document.getElementById('loading');
loadingEl.style.display = 'c9s3v';
chunkRenderAsync(data, container, renderItem, 200)
.then(() => {
loadingEl.style.display = 'none';
console.log('渲染完成!');
});
// 示例3:渲染复杂的DOM结构
function renderComplexItem(item, index) {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `
<div class="card-header">
<h3>${item.name}</h3>
<span class="badge">#${item.id}</span>
</div>
<div class="card-body">
<p>Value: ${item.value.toFixed(4)}</p>
<button onclick="handleClick(${item.id})">操作</button>
</div>
`;
return div;
}
chunkRender(data, container, renderComplexItem, 100);
// 示例4:使用 setTimeout 版本(更好的兼容性)
chunkRenderWithTimeout(data, container, renderItem, 200, 10);
关键点
-
时间分片原理:将大任务拆分成多个小任务,利用浏览器的空闲时间逐步执行,避免长时间阻塞主线程
-
requestAnimationFrame vs setTimeout:
requestAnimationFrame在浏览器重绘前执行,性能更好,适合动画和渲染场景setTimeout兼容性更好,可以自定义延迟时间,更灵活
-
DocumentFragment 优化:使用文档片段批量插入DOM,减少回流和重绘次数,提升性能
-
合理的分片大小:
- 分片太小:渲染次数过多,总耗时增加
- 分片太大:单次渲染时间长,仍会造成卡顿
- 建议:100-200条为一个分片,根据实际数据复杂度调整
-
虚拟滚动的补充:对于超大数据量(百万级),建议结合虚拟滚动技术,只渲染可视区域的数据
-
用户体验优化:添加加载进度提示、骨架屏等,让用户感知到页面正在加载
-
内存管理:注意及时清理不需要的DOM节点,避免内存泄漏
目录