跳到主要内容

大数据量渲染如何优化?(虚拟列表/Web Worker/SharedArrayBuffer)

考察点​

  • 前端性能基本功在桌面端的应用
  • "数据不进渲染进程"的架构思维(Electron 特有)

参考答案​

1. 优化层次总览​

层次一:数据根本不进渲染进程(架构层)
层次二:进来了但只渲染可见部分(渲染层)
层次三:计算不阻塞主线程(并发层)

2. 层次一:数据不进渲染进程(Electron 特有优势)​

Web 应用数据必须从网络来;Electron 可以把数据"留在主进程":

// ❌ 100 万条日志全部 IPC 给渲染进程(序列化卡死两端)
ipcMain.handle('logs:get-all', () => allLogs);

// ✅ 主进程分页供给,渲染进程只要当前页
ipcMain.handle('logs:get-page', (e, { offset, limit, filter }) => {
return logStore.query({ offset, limit, filter }); // 只传一页 50 条
});
  • 过滤、搜索、聚合都在主进程做(可配合 SQLite/索引)
  • 渲染进程永远只持有"当前视图所需"的数据量

3. 层次二:虚拟列表(渲染层)​

10 万条数据只渲染可视区的几十条:

// react-virtuoso / vue-virtual-scroller 原理示意
function VirtualList({ items, itemHeight, height }) {
const [scrollTop, setScrollTop] = useState(0);

// 只计算可视区内的条目
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(height / itemHeight) + 1,
items.length
);

return (
<div style={{ height, overflow: 'auto' }} onScroll={(e) => setScrollTop(e.target.scrollTop)}>
{/* 占位撑出滚动高度 */}
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{items.slice(startIndex, endIndex).map((item, i) => (
<div
key={startIndex + i}
style={{ position: 'absolute', top: (startIndex + i) * itemHeight }}
>
{renderItem(item)}
</div>
))}
</div>
</div>
);
}

要点:

  • 不定高场景用 virtuoso 这类成熟库(自己处理动态测量成本高)
  • 配合分页供给:滚动到底部再 invoke 拉下一页(虚拟列表 + 主进程分页是黄金组合)
  • DOM 节点复用减少 GC 压力

4. 层次三:Web Worker 卸载计算​

// 主线程:把解析/过滤/转换丢给 Worker
const worker = new Worker(new URL('./parser.worker.ts', import.meta.url));

worker.postMessage({ type: 'parse', payload: rawData });
worker.onmessage = (e) => {
setTableData(e.data.result); // 结果回来再渲染
};

适合:

  • 大文件解析(日志、CSV、JSON)
  • 全文搜索建索引
  • 数据格式转换、压缩

Worker 与主线程通信也是结构化克隆——大数据用 Transferable(ArrayBuffer transfer) 零拷贝。

5. SharedArrayBuffer(终极方案)​

多线程共享同一块内存,免序列化:

前置条件:跨域隔离(Cross-Origin-Isolated)
需要响应头:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Electron 自定义协议场景下要在 protocol.handle 的响应里带上这两个头:

protocol.handle('myapp', async (request) => {
const response = await net.fetch(fileUrl);
const headers = new Headers(response.headers);
// 开启跨域隔离以解锁 SharedArrayBuffer
headers.set('Cross-Origin-Opener-Policy', 'same-origin');
headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
return new Response(response.body, { status: response.status, headers });
});

代价:COEP 会拦截所有不带 corp 标记的跨域资源,接入成本高——只在确实需要(音视频处理、实时分析)时引入。

6. 渲染策略补充​

  • Canvas/WebGL 替代 DOM:10 万+ 点的图表、表格用 canvas 渲染(如 luckysheet/ag-grid 的 canvas 模式),DOM 数量是渲染性能的第一杀手
  • 时间切片:大量 DOM 更新拆到多帧(scheduler 库 / React 并发特性)
  • GPU 加速:transform/opacity 动画走合成层,避免 layout

7. 面试话术​

三层思路:架构层利用 Electron 优势让数据留在主进程、渲染进程只取视图所需(分页 + 主进程过滤);渲染层虚拟列表只挂可视区 DOM,十万级数据无压力;并发层 Web Worker + Transferable 卸载解析计算,极端场景 SharedArrayBuffer(需要自定义协议下配置 COOP/COEP 跨域隔离)。DOM 数量是第一杀手,超大数据量表格/图表最终走向 canvas 渲染。

常见追问​

  • 虚拟列表行高不固定怎么办? 成熟库(react-virtuoso)支持动态测量;自研则预估高度 + 实测校正,滚动位置要补偿。
  • Worker 里能用 Electron API 吗? 不能(无 Node/Electron 上下文),Worker 是纯 Web 环境,数据进出靠 postMessage。