前端表格1000w行数据流畅渲染的秘密

canvas 优化细节白板方案,大部分同学第一反应,那肯定是 canvas啊,没错,但是,可以很直接地告诉大家,canvas方案在大家平常小数据量的可视化场景,没太大问题。

不过如果是大量数据的渲染,canvas 瓶颈也会凸显,为了进一步优化白板性能,还需要进行深入底层优化表格开发,可能是大家平常开发过程中最常见的场景,表格的优化我们可以给出以下历程:

  1. 用库
  2. 初级:table dom
  3. 中级:虚拟表格
  4. 高级:canvas table
  5. 专家:canvas + tile 技术
  6. 高级专家:skia + Webassembly
  7. 结合给定的五个不同层级的实现方案,以下是详细说明如何在不同层次上实现高效的表格渲染方案:

一、初级: table Dom

这是最基础的实现方式,直接使用 HTML 的table元素来渲染表格。

实现方案

  • 简单实现:直接使用 HTML 和 JavaScript 渲染表格,
  • 适用场景:适用于数据量较小的场景,当行数和列数有限时,直接使用 DOM 元素处理方便目直观。
 
<!DOCTYPE html>
<html>
<head>
    <title>Basic Table</title>
    <style>
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            border: 1px solid black;
            padding: 8px;
            text-align: left;
        }
    </style>
</head>
<body>
    <table id="data-table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Age</th>
            </tr>
        </thead>
        <tbody>
            <!-- Rows will be inserted here by JavaScript -->
        </tbody>
    </table>

    <script>
        const tableBody = document.querySelector('#data-table tbody');
        const data = Array.from({ length: 100 }, (_, id) => ({
            id,
            name: `Name ${id}`,
            age: Math.floor(Math.random() * 100)
        }));

        data.forEach(row => {
            const tr = document.createElement('tr');
            tr.innerHTML = `<td>${row.id}</td><td>${row.name}</td><td>${row.age}</td>`;
            tableBody.appendChild(tr);
        });
    </script>
</body>
</html>

二、中级: 虚拟表格

使用虚拟化技术来渲染表格,只有视口中的行和列才会被染。

实现方案

  • 库选择:使用 react-window或react-virtualized 来实现虚拟化表格。
  • 适用场景:适用于数据量较大但需要浏览的行数有限的情况,通过虚拟化技术减少染的 DOM 数量。
 
import React from 'react';
import { FixedSizeList as List } from 'react-window';

const data = Array.from({ length: 10000 }, (_, id) => ({
   id,
   name: `Name ${id}`,
   age: Math.floor(Math.random() * 100)
}));

const Row = ({ index, style }) => (
   <div style={style}>
       {data[index].id} - {data[index].name} - {data[index].age}
   </div>
);

const VirtualizedTable = () => (
   <List
       height={600}
       itemCount={data.length}
       itemSize={35}
       width={300}
   >
       {Row}
   </List>
);

export default VirtualizedTable;

三、高级: canvas table

使用 HTML5 Canvas 来绘制表格内容,選免大量 DOM 操作,提高渲染性能

实现方案

  • 使用 Canvas API: 通过 Canvas API 手动绘制表格的每个单元格。
  • 适用场景:适用于需要更高效浪染和更灵活绘制能力的场景。
 
import React, { useRef, useEffect } from 'react';

const CanvasTable = ({ data }) => {
 const canvasRef = useRef(null);

 useEffect(() => {
   const canvas = canvasRef.current;
   const ctx = canvas.getContext('2d');
   const rowHeight = 20;
   const columnWidths = [50, 150, 50];

   canvas.height = data.length * rowHeight;
   canvas.width = columnWidths.reduce((a, b) => a + b, 0);

   data.forEach((row, rowIndex) => {
     const y = rowIndex * rowHeight;

     ctx.fillText(row.id, 0, y + rowHeight / 2);
     ctx.fillText(row.name, columnWidths[0], y + rowHeight / 2);
     ctx.fillText(row.age, columnWidths[0] + columnWidths[1], y + rowHeight / 2);
     ctx.strokeRect(0, y, canvas.width, rowHeight);
   });
 }, [data]);

 return <canvas ref={canvasRef} />;
};

const data = Array.from({ length: 10000 }, (_, id) => ({
 id,
 name: `Name ${id}`,
 age: Math.floor(Math.random() * 100),
}));

export default function App() {
 return <CanvasTable data={data} />;
}

四、专家: canvas + title

通过将表格划分为多个 tile(瓷砖)区域,只染当前视口及其周围的 tile,提高渲染性能和内存使用效率。

实现方案

  • Tile 分区:将表格划分为多个 tie 区域,每个 tie 只包含一定数量的单元格。
  • 懒加载和预加载:仅在需要时加载和渲染特定的 tile,同时预加载用户视口附近的 tile。
 
import React, { useRef, useEffect, useState } from 'react';

const tileSize = 100; // 每个 tile 的宽度和高度

const CanvasTileTable = ({ data }) => {
 const canvasRef = useRef(null);
 const [visibleTiles, setVisibleTiles] = useState([]);

 useEffect(() => {
   const canvas = canvasRef.current;
   const ctx = canvas.getContext('2d');
   const rowHeight = 20;
   const columnWidths = [50, 150, 50];

   canvas.height = window.innerHeight;
   canvas.width = columnWidths.reduce((a, b) => a + b, 0);

   const updateVisibleTiles = () => {
     const scrollTop = window.scrollY;
     const visibleStart = Math.floor(scrollTop / (tileSize + rowHeight));
     const visibleEnd = visibleStart + Math.ceil(canvas.height / (tileSize + rowHeight));
     const tiles = [];

     for (let i = visibleStart; i <= visibleEnd; i++) {
       tiles.push(i);
     }

     setVisibleTiles(tiles);
   };

   window.addEventListener('scroll', updateVisibleTiles);
   updateVisibleTiles();

   return () => {
     window.removeEventListener('scroll', updateVisibleTiles);
   };
 }, []);

 useEffect(() => {
   const canvas = canvasRef.current;
   const ctx = canvas.getContext('2d');
   const rowHeight = 20;
   const columnWidths = [50, 150, 50];

   ctx.clearRect(0, 0, canvas.width, canvas.height);

   visibleTiles.forEach((tileIndex) => {
     const startRow = tileIndex * tileSize;
     const endRow = Math.min(data.length, startRow + tileSize);

     for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) {
       const row = data[rowIndex];
       const y = (rowIndex % tileSize) * rowHeight;

       ctx.fillText(row.id, 0, y + rowHeight / 2);
       ctx.fillText(row.name, columnWidths[0], y + rowHeight / 2);
       ctx.fillText(row.age, columnWidths[0] + columnWidths[1], y + rowHeight / 2);
       ctx.strokeRect(0, y, canvas.width, rowHeight);
     }
   });
 }, [visibleTiles, data]);

 return <canvas ref={canvasRef} />;
};

const data = Array.from({ length: 100000 }, (_, id) => ({
 id,
 name: `Name ${id}`,
 age: Math.floor(Math.random() * 100),
}));

export default function App() {
 return <CanvasTileTable data={data} />;
}

五、高级专家: skia + WebAssembly

使用 skia 图形库结合 WebAssembly,实现高性能、跨平台的表格渲染。

实现方案

  • skia 图形库:skia 是一个跨平台的 2D 图形库,可以高效地绘制复杂图形。
  • WebAssembly:通过 webAssembly 将 skia 集成到 Web 应用中,提升图形渲染性能.

示例代码

由于 skia 和 WebAssembly 的集成涉及较为复杂的编译和绑定过程,以下提供的是一种基本思路:

1.准备 Skia 和 WebAssembly 环境:需要编译 skia 库为 WebAssembly 模块,可以使用 emscripten 工具链。

2.编译 Skia:

  • 下载并编译 Skia 库,牛成 WebAssembly 模块,
  • 详细步骤可以参考 skia 和 Emscripten 官方文档。

3.在 React 中使用 Skia:

  • 引入编译好的 Skia WebAssembly 模块,
  • 使用 skia 的 API 绘制表格内容。
 
import React, { useRef, useEffect } from "react";
import SkiaCanvas from "@shopify/react-native-skia-web"; // 假设使用 Shopify Skia 的 JS 包(需要先安装)

const Table = ({ data }) => {
  const canvasRef = useRef(null);

  useEffect(() => {
    // 加载 Skia 模块
    (async () => {
      const skia = await import("canvaskit-wasm"); // 假设 Skia WASM 文件
      const CanvasKit = await skia.default();

      const canvasElement = canvasRef.current;
      const surface = CanvasKit.MakeCanvasSurface(canvasElement);

      if (!surface) {
        console.error("Failed to create Skia surface.");
        return;
      }

      const canvas = surface.getCanvas();

      const paint = new CanvasKit.Paint();
      paint.setAntiAlias(true);
      paint.setColor(CanvasKit.Color(0, 0, 0, 1)); // 黑色文本

      const font = new CanvasKit.Font(
        new CanvasKit.Typeface("Roboto"),
        14 // 字体大小
      );

      // 清除画布
      canvas.clear(CanvasKit.WHITE);

      // 行高
      const rowHeight = 30;

      // 绘制表头
      const columnWidths = [50, 200, 100]; // 每列的宽度
      const headerTitles = ["ID", "Name", "Age"];
      headerTitles.forEach((title, index) => {
        canvas.drawText(
          title,
          columnWidths.slice(0, index).reduce((a, b) => a + b, 10), // X 坐标
          rowHeight / 2,
          paint,
          font
        );
      });

      // 绘制表格数据
      data.forEach((row, rowIndex) => {
        const y = rowHeight * (rowIndex + 1);
        canvas.drawText(`${row.id}`, 10, y, paint, font);
        canvas.drawText(`${row.name}`, 60, y, paint, font);
        canvas.drawText(`${row.age}`, 260, y, paint, font);
      });

      surface.flush();
    })();
  }, [data]);

  return (
    <canvas
      ref={canvasRef}
      width="400"
      height={data.length * 30 + 30} // 根据行数动态调整高度
      style={{ border: "1px solid #ccc" }}
    />
  );
};

const App = () => {
  const data = Array.from({ length: 100 }, (_, id) => ({
    id,
    name: `Name ${id}`,
    age: Math.floor(Math.random() * 100),
  }));

  return (
    <div>
      <h1>Skia Table Renderer</h1>
      <Table data={data} />
    </div>
  );
};

export default App;

另外 skia 这个技术可能很多同学都比较陌生,这个库是 C++ 编写的图形处理库,目前由 Google 公司维护。 其实,浏览器 canvas 底层就是 skia: https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/core/html/HTMLCanvasElement.cpp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值