canvas 优化细节白板方案,大部分同学第一反应,那肯定是 canvas啊,没错,但是,可以很直接地告诉大家,canvas方案在大家平常小数据量的可视化场景,没太大问题。
不过如果是大量数据的渲染,canvas 瓶颈也会凸显,为了进一步优化白板性能,还需要进行深入底层优化表格开发,可能是大家平常开发过程中最常见的场景,表格的优化我们可以给出以下历程:
这是最基础的实现方式,直接使用 HTML 的table元素来渲染表格。
实现方案
Basic Table
ID
Name
Age
使用虚拟化技术来渲染表格,只有视口中的行和列才会被染。
实现方案
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 }) => (
{data[index].id} - {data[index].name} - {data[index].age}
);
const VirtualizedTable = () => (
{Row}
);
export default VirtualizedTable;
使用 HTML5 Canvas 来绘制表格内容,選免大量 DOM 操作,提高渲染性能
实现方案
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 ;
};
const data = Array.from({ length: 10000 }, (_, id) => ({
id,
name: `Name ${id}`,
age: Math.floor(Math.random() * 100),
}));
export default function App() {
return ;
}
通过将表格划分为多个 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 ;
};
const data = Array.from({ length: 100000 }, (_, id) => ({
id,
name: `Name ${id}`,
age: Math.floor(Math.random() * 100),
}));
export default function App() {
return ;
}
使用 skia 图形库结合 WebAssembly,实现高性能、跨平台的表格渲染。
实现方案
示例代码
由于 skia 和 WebAssembly 的集成涉及较为复杂的编译和绑定过程,以下提供的是一种基本思路:
1.准备 Skia 和 WebAssembly 环境:需要编译 skia 库为 WebAssembly 模块,可以使用 emscripten 工具链。
2.编译 Skia:
3.在 React 中使用 Skia:
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 (
);
};
const App = () => {
const data = Array.from({ length: 100 }, (_, id) => ({
id,
name: `Name ${id}`,
age: Math.floor(Math.random() * 100),
}));
return (
Skia Table Renderer
);
};
export default App;
另外 skia 这个技术可能很多同学都比较陌生,这个库是 C++ 编写的图形处理库,目前由 Google 公司维护。 其实,浏览器 canvas 底层就是 skia: https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/core/html/HTMLCanvasElement.cpp