导出Excel慢的根源是PhpSpreadsheet全量加载导致内存暴涨和超时,应改用流式CSV或box/spout等轻量库实现低内存、高响应的导出。

导出 Excel 慢,大概率不是 TP 本身的问题,而是你用了 PhpOffice\PhpSpreadsheet 全量加载再输出——它会把整张表塞进内存,10 万行就可能吃掉 500MB+ 内存,PHP 直接超时或 OOM。真要提速,得绕开“生成完整对象再写文件”这条路,改用流式 CSV 或原生流式写入。
为什么 PhpSpreadsheet 导出慢还容易崩
它默认构建完整 Spreadsheet 对象,每行都实例化 Row、Cell,还要维护样式、公式、合并单元格等元信息。哪怕你只导纯数据,底层也按 Excel 标准全量建模。
- 10 万行 × 10 列数据,内存占用常超 400MB,远超多数 PHP 配置的
memory_limit - 导出响应时间随行数非线性增长,10 万行可能要 8 秒,20 万行直接 30 秒+ 或超时
-
ob_end_clean()或header()发送前若缓冲区未及时刷新,浏览器卡在 loading 状态 - 并发导出时,多个大对象堆积,Web 服务器(如 Apache/PHP-FPM)进程迅速耗尽内存
用 fputcsv 流式写 CSV 替代 Excel
绝大多数业务导出本质是“看数据”,不是“做报表”。CSV 文件体积小、生成快、兼容性好,且能被 Excel 正确打开(需注意 UTF-8 BOM 和中文乱码)。
- 用
fopen('php://output', 'w')直接写响应体,不落地临时文件 - 写入前加 BOM:
fputs($fp, "\xEF\xBB\xBF"),避免 Excel 打开中文乱码 - 表头和每行数据都走
fputcsv($fp, $row, ',', '"'),自动转义引号和逗号 - 每写 1000 行调一次
fflush($fp),强制刷出缓冲,防止阻塞和超时 - 不要用
array_map('htmlspecialchars', $row)—— CSV 不需要 HTML 转义,反而破坏原始数据
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="export_' . date('Y-m-d') . '.csv"');
$fp = fopen('php://output', 'w');
fputs($fp, "\xEF\xBB\xBF");
fputcsv($fp, ['部门', '金额', '备注']);
foreach ($dataIterator as $row) {
fputcsv($fp, [$row['dept'], $row['amount'], $row['note']]);
if (++$i % 1000 === 0) fflush($fp);
}
fclose($fp);
ThinkPHP 8 中如何安全接入流式导出
TP8 的响应生命周期支持手动接管输出流,但要注意别让中间件或视图层提前触发 ob_start(),否则 php://output 会被拦截。
- 导出接口必须设为无模板:在控制器方法里加
return null;并手动输出,避免 TP 自动渲染 - 禁用所有输出缓冲中间件,比如关闭
think\middleware\CheckRequestCache和think\middleware\AllowCrossDomain(它们可能调用ob_start()) - 若用
Filesystem类,它默认走file_put_contents(),不适用于流式;必须绕过,直接用原生fopen/fputcsv - 数据库查询务必用游标分页或
yield迭代器(如 TP8 的cursor()),而不是select()全部拉到内存 - 导出前加
set_time_limit(0),并确认max_execution_time在 php.ini 中未硬限制
Excel 导出真没法流式?那加个折中方案
如果业务方死磕 .xlsx 后缀(比如要保留颜色、冻结窗格),又不愿接受 CSV,可退一步:用 PhpOffice\PhpSpreadsheet\Writer\Csv 写完再用系统命令转 XLSX,或用更轻量的库如 box/spout(它原生支持流式 XLSX 写入,内存恒定在 ~3MB)。
-
box/spout不依赖 DOMDocument 或 zip 扩展,纯 PHP 实现,WriterEntityFactory::createXLSXWriter()可边写边发 - 它不支持公式、图表、条件格式,但够用——95% 的导出需求只是带样式的表格数据
- 注意:Spout 的
addRow()传入的是Row对象,不是数组;中文字段要确保源数据已是 UTF-8 - 别在 TP 的
view或response流程里嵌套 Spout 输出,仍需手动header+fopen('php://output')
真正卡点不在“会不会写代码”,而在是否愿意放弃“用 Excel 库导 Excel”的思维惯性——流式的核心是“不建模,只落字节”,一旦想通这点,100 万行导出也能压在 3 秒内,且内存稳定在 2MB 左右。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











