Excel 百万级数据导出性能优化

Excel 百万级数据导出性能优化

工单系统的报表导出功能,一开始用的 xlsx 库直接在前端生成 Excel 文件。数据量小的时候没问题,但当运营需要导出半年的工单数据(100 万+ 行)时,浏览器直接崩了。

问题分析

100 万行数据 × 每行 15 列 = 1500 万个单元格。xlsx 库会把整个工作簿构建为一个内存中的 JS 对象,峰值内存占用可达 2-3GB,直接触发浏览器标签页崩溃。

核心问题:一次性把所有数据加载到内存

方案演进

方案一:后端流式导出(推荐)

最彻底的方案是把导出逻辑放到后端。Java 用 Apache POI 的 SXSSFWorkbook(流式模式),Python 用 openpyxl 的 write_only 模式,内存占用恒定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 后端 Java 流式导出
SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 缓冲 100 行
Sheet sheet = workbook.createSheet("工单数据");

int rowNum = 0;
for (WorkOrder order : workOrderService.findAll()) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(order.getId());
row.createCell(1).setCellValue(order.getTitle());
row.createCell(2).setCellValue(order.getStatus());
// ... 写入其他字段
if (rowNum % 100 == 0) {
// 每 100 行刷新一次,写入临时文件
workbook.write(tempOutputStream);
}
}

workbook.write(outputStream);
workbook.dispose(); // 清理临时文件

SXSSFWorkbook 的核心原理是只在内存中保留最近 100 行,之前的行已经写到磁盘临时文件了。内存占用从 O(n) 降到 O(100)。

方案二:前端分片导出

如果必须在前端导出(比如数据来自前端已有的表格),可以用分片策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// composables/useExcelExport.ts
import { ref } from 'vue'
import * as XLSX from 'xlsx'

export function useExcelExport() {
const progress = ref(0)
const isExporting = ref(false)

async function exportLargeData(data: any[], filename: string) {
isExporting.value = true
progress.value = 0

const CHUNK_SIZE = 10000
const chunks = Math.ceil(data.length / CHUNK_SIZE)
const wb = XLSX.utils.book_new()

// 用一个空工作表开始
const ws = XLSX.utils.aoa_to_sheet([])

for (let i = 0; i < chunks; i++) {
const start = i * CHUNK_SIZE
const end = Math.min(start + CHUNK_SIZE, data.length)
const chunk = data.slice(start, end)

// 分批写入
XLSX.utils.sheet_add_json(ws, chunk, {
origin: -1,
skipHeader: i > 0, // 只有第一批写表头
})

progress.value = Math.round(((i + 1) / chunks) * 100)

// 让出主线程,避免卡顿
await new Promise(resolve => setTimeout(resolve, 0))
}

XLSX.utils.book_append_sheet(wb, ws, '数据')
XLSX.writeFile(wb, filename)
isExporting.value = false
}

return { progress, isExporting, exportLargeData }
}

方案三:Web Worker + 流式写入

把文件生成放到 Web Worker 里,主线程保持响应:

1
2
3
4
5
6
7
8
9
10
// worker/excel-worker.ts
self.onmessage = function(e) {
const { data, columns } = e.data
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.json_to_sheet(data, { header: columns })

// 生成 ArrayBuffer 而不是直接下载
const buffer = XLSX.write(wb, { type: 'array', bookType: 'xlsx' })
self.postMessage({ buffer }, [buffer])
}

数据库层面优化

导出慢的另一个原因是查询慢。100 万条数据的 SELECT * 本身就很耗时。

1
2
3
4
5
6
7
8
9
10
11
-- 优化前:全量查询
SELECT * FROM work_orders WHERE created_at >= '2026-01-01'

-- 优化后:流式游标
DECLARE cur CURSOR FOR
SELECT id, title, status, created_at
FROM work_orders
WHERE created_at >= '2026-01-01'
ORDER BY created_at

-- 后端分批 fetch,每批 1000 条

关键优化点:

  1. 只查需要的字段 — 不要 SELECT *
  2. 用游标而非 LIMIT/OFFSET — 百万级数据 OFFSET 会越来越慢
  3. 加合适的索引 — 导出条件字段必须有索引

前端进度条

导出时间长,进度条是必须的:

1
2
3
4
5
6
<template>
<div v-if="isExporting" class="export-progress">
<div class="progress-bar" :style="{ width: progress + '%' }" />
<span>{{ progress }}% — 正在生成文件...</span>
</div>
</template>

方案对比

方案 内存占用 速度 复杂度 适用场景
xlsx 一次性生成 O(n) 极高 < 1万行
前端分片导出 O(1万) 1-10万行
Web Worker O(1万) 主线程空闲 需要保持 UI 响应
后端流式导出 O(100) > 10万行

最终方案

我们的选择是后端流式导出 + 前端进度条。超过 5 万行就走后端,前端只负责发起请求和显示进度。用户体验好,服务器内存也扛得住。

如果后端实在改不了,前端分片 + Web Worker 是保底方案,但要注意分片大小和主线程让出的节奏。


Excel 百万级数据导出性能优化
https://000902.icu/2026/06/23/excel-export/
作者
Xiazhou
发布于
2026年6月24日
许可协议