面试官:后端一次性返回给前端十万条数据,渲染这十万条数据怎么能保证不卡顿

前端一次性将十万条数据都渲染到页面上,基本上100%会导致页面卡顿甚至无响应。

大量的DOM操作会极大的消耗浏览器的资源,所以在面试的时候千万不要说直接for循环渲染。

给大家几种优化方案:

虚拟滚动(Virtual Scrolling)

实现方案:通过只渲染视口内可见的数据项,而不是一次性渲染所有数据项,可以极大地减少DOM元素的数量,从而提升性能。

当用户滚动时,动态更新显示的数据项。实现这一功能的库包括react-window、react-virtualized、vue-virtual-scroll-list等。

实现思路

  • 只渲染可视区域内的数据项,通过监听滚动事件,动态计算当前应显示的数据范围。
  • 利用占位的 padding 或 transform 来模拟滚动位置,保持滚动条高度正确。

代码实现

html 复制代码
<div id="container" class="container"></div>

<style>
.container {
    height: 100vh;
    overflow-y: auto;
    position: relative;
}
.scroll-content {
    position: relative;
}
.item {
    height: 50px; /* 每一项高度固定 */
    line-height: 50px;
    padding: 0 10px;
    border-bottom: 1px solid #eee;
}
</style>
js 复制代码
// 模拟 10 万条数据
const totalItems = 100000;
const itemHeight = 50; // 每条高度 50px
const viewportHeight = window.innerHeight; // 可视区域高度
const visibleCount = Math.ceil(viewportHeight / itemHeight); // 视区内显示的条数
const buffer = 5; // 额外缓冲项,防止滚动过快白屏
const renderCount = visibleCount + buffer * 2; // 实际渲染数量

// DOM 元素
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
const content = document.createElement('div');
content.className = 'scroll-content';
container.appendChild(content);

// 创建占位元素(用于撑起滚动高度)
const placeholder = document.createElement('div');
placeholder.style.height = `${totalItems * itemHeight}px`;
placeholder.className = 'placeholder-content';
content.appendChild(placeholder);

// 渲染区域(实际显示的 item 容器)
const renderArea = document.createElement('div');
renderArea.style.position = 'absolute';
renderArea.style.top = '0';
renderArea.style.left = '0';
renderArea.style.width = '100%';
content.appendChild(renderArea);

// 初始渲染
function renderVisibleItems(startIndex) {
  const endIndex = Math.min(startIndex + renderCount, totalItems);
  renderArea.innerHTML = ''; // 清空

  for (let i = startIndex - buffer; i < endIndex + buffer; i++) {
    if (i >= 0 && i < totalItems) {
      const item = document.createElement('div');
      item.className = 'item';
      item.textContent = `Item ${i}`;
      renderArea.appendChild(item);
    }
  }

  // 设置偏移,使内容出现在正确位置
  renderArea.style.transform = `translateY(${startIndex * itemHeight}px)`;
}

// 滚动处理函数
function handleScroll() {
  const scrollTop = container.scrollTop;
  const startIndex = Math.floor(scrollTop / itemHeight);
  renderVisibleItems(startIndex);
}

// 初始化
renderVisibleItems(0);
container.addEventListener('scroll', handleScroll, { passive: true });

示意图

分页加载和懒加载

实现方案:分页加载和懒加载其实差不太多,通过将数据切片,分开渲染,减少一次性渲染数量,达到降低卡顿的效果。

当用户滚动,或点击分页器时,触发下一页或者是对应页的数据渲染。

实现思路

  • 每次只加载一页数据(如每页 50 条)。
  • 用户点击"下一页"或滚动到底部时,加载下一页数据。

代码实现

js 复制代码
// 无限滚动监听
let isLoading = false;

function handleScroll() {
  if (isLoading || currentPage >= totalPages) return;

  const { scrollTop, scrollHeight, clientHeight } = container;

  // 距离底部 100px 时触发加载
  if (scrollHeight - scrollTop - clientHeight < 100) {
    loadMore();
  }
}

async function loadMore() {
  const nextPage = currentPage + 1;
  if (nextPage > totalPages) return;

  isLoading = true;
  const result = await fetchPage(nextPage);
  currentPage = nextPage;

  // 追加数据
  result.data.forEach((text) => {
    const li = document.createElement('li');
    li.className = 'item';
    li.textContent = text;
    listEl.appendChild(li);
  });

  isLoading = false;
}

// 监听滚动
const container = document.documentElement || document.body;
window.addEventListener('scroll', () => {
  if (!isLoading && currentPage < totalPages) {
    const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
    if (scrollHeight - scrollTop - clientHeight < 200) {
      loadMore();
    }
  }
}, { passive: true });

Ps: 这种存在一个弊端,假如后端数据不是一次性全部返回给前端,滚动过快可能会存在等待。

注意:懒加载的实现方案如果是在Vue/React中有两种实现方案:

  • 通过v-if控制是否渲染
  • 通过控数据分割控制渲染数据

Web Workers数据处理

实现方案:本质上也是将数据切割处理,但是处理方案不同。主要是通过将耗时的数据处理任务从主线程移到后台线程执行,从而避免阻塞UI线程,保证页面的响应性。

实现思路

  • 数据预处理:在Web Worker中进行数据过滤、排序、格式化等操作。
  • 分批传输:将处理后的数据分批次发送回主线程,以减少一次性向DOM添加大量元素造成的性能瓶颈。
  • 也可以再加上虚拟滚动进一步节省资源。

代码实现

js 复制代码
// 主Js文件
const worker = new Worker('dataProcessor.js');
// 主线程代码
worker.postMessage({ action: 'processData', payload: largeDataSet }); // largeDataSet为大数据集

worker.onmessage = function(e) {
    const { action, data } = e.data;
    if (action === 'dataProcessed') {
        // 使用处理后的数据更新DOM
        renderData(data);
    }
};

function renderData(data) {
    // 渲染数据到页面的逻辑
    const container = document.getElementById('dataContainer');
    container.innerHTML = ''; // 清空容器
    data.forEach(item => {
        const div = document.createElement('div');
        div.textContent = item;
        container.appendChild(div);
    });
}

// -----------------------------------------------------------------------

// dataProcessor.js
onmessage = function(e) {
    const { action, payload } = e.data;
    
    if (action === 'processData') {
        // 假设payload是我们需要处理的数据集
        let processedData = processData(payload); // 处理数据的函数
        postMessage({ action: 'dataProcessed', data: processedData });
    }
};

function processData(data) {
    // 模拟复杂的数据处理,例如排序、过滤等
    return data.sort((a, b) => a - b).slice(0, 50); // 返回前50条作为示例
}

总结

海量数据渲染优先选择后端分页处理是最好的,毕竟海量数据通过接口传输比较慢,用户等待时间长。

并且数据尽量在后端完成全部的处理工作,最后交给前端只进行渲染,不再操作,尽可能节省浏览器资源。

如果非得选择一次性渲染全部数据,则虚拟滚动是比较好的方案。

相关推荐
围巾哥萧尘2 小时前
王府宠妾进阶录:现代思维的古代逆袭🧣
面试
gnip2 小时前
文件操作利器:showOpenFilePicker
前端·javascript
程序员雨果2 小时前
外包干了3天,技术退步明显.......
软件测试·面试
繁依Fanyi3 小时前
做一个 3D 图片画廊
前端
繁依Fanyi3 小时前
用 Electron 做一个屏幕取色器
前端
某公司摸鱼前端3 小时前
一键 i18n 国际化神库!适配 Vue、React!
前端·vue.js·react.js·i18n
OEC小胖胖3 小时前
给你的应用穿上“外衣”:React中的CSS方案对比与实践
前端·前端框架·react·web
excel3 小时前
Nuxt 3 微前端:模块导入导出与路由跳转实战
前端
大家的林语冰3 小时前
Promise 再次进化,ES2025 新增 Promise.try() 静态方法
前端·javascript·ecmascript 6
大家的林语冰3 小时前
如何错误手写 ES2025 新增的 Promise.try() 静态方法
前端·javascript·ecmascript 6