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

前端一次性将十万条数据都渲染到页面上,基本上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条作为示例
}

总结

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

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

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

相关推荐
养鱼的程序员4 分钟前
零基础搭建个人网站:从 Astro 框架到 GitHub 自动部署完全指南
前端·后端·github
罗行7 分钟前
手写防抖和节流
前端·javascript
前端老鹰7 分钟前
CSS :is () 与 :where ():简化复杂选择器的 “语法糖”
前端·css·html
颜酱12 分钟前
理解并尝试vue3源码的reactivity
前端·javascript·vue.js
拉不动的猪20 分钟前
jS篇Async await实现同步效果的原理
前端·javascript·面试
杨充24 分钟前
03.接口vs抽象类比较
前端·后端
chxii36 分钟前
2.4 组件通信
前端·javascript·vue.js
泡岩浆的child1 小时前
朋友:你平常都用什么软件取色?我:QQ截图啊。朋友:牛X!
前端
志如1 小时前
【校招面试官说】什么样的技术人更容易被大厂校招选中?
前端·后端·面试
古夕1 小时前
TS 导出 PDF:解决表头乱码,实现表格内添加可点击链接
前端·typescript