canvas 优化细节白板方案,大部分同学第一反应,那肯定是 canvas啊,没错,但是,可以很直接地告诉大家,canvas方案在大家平常小数据量的可视化场景,没太大问题。
不过如果是大量数据的渲染,canvas 瓶颈也会凸显,为了进一步优化白板性能,还需要进行深入底层优化表格开发,可能是大家平常开发过程中最常见的场景,表格的优化我们可以给出以下历程:
- 用库
- 初级:table dom
- 中级:虚拟表格
- 高级:canvas table
- 专家:canvas + tile 技术
- 高级专家:skia + Webassembly
- 结合给定的五个不同层级的实现方案,以下是详细说明如何在不同层次上实现高效的表格渲染方案:
一、初级: 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