ThinkPHP流式响应需手动接管输出流,因默认响应机制强制全量缓存;关键障碍包括PHP输出缓冲、中间件拦截、Nginx proxy_buffering及前端API支持不足。

ThinkPHP 处理流式响应不是靠“开启某个开关”,而是必须绕过默认响应流程、手动接管输出流。直接用 response()->json() 或 return 任何封装好的响应方法,都会触发全量缓存,流式立刻失效。
为什么 response()->stream() 在 TP6+ 里仍可能不流式?
很多人调用 Response::stream() 后发现浏览器还是等全部数据吐完才开始接收——问题不在函数本身,而在它运行的上下文被框架中间件或 PHP 配置拦住了。
-
ob_start()已被中间件(如调试工具栏、日志记录)提前调用,你echo出去的数据先进了 ob 缓冲,根本没到 HTTP 层 -
output_buffering在 php.ini 里设为4096或On,PHP 自身会攒够缓冲才发包 - 没在回调里显式调用
flush()和ob_flush(),或者调用顺序反了(必须先ob_flush()再flush()) - 返回前没清空已有缓冲:
ob_end_clean()比ob_get_clean()更安全,后者可能漏掉嵌套层
大文件下载必须用 fopen('php://output', 'wb') 而非 echo
用 echo fread($file, 8192) 看似简单,但遇到中文文件名、Nginx 压缩、字符编码混杂时容易出错;真正可控的做法是把输出目标明确绑定到 PHP 标准输出流。
- 打开
fopen('php://output', 'wb')后,所有fwrite()都直通 HTTP body,不受 ob 影响 - 写入前必须先发 header:
header('Content-Type: application/octet-stream')、header('Content-Length: ' . filesize($path)),且不能有任何前置输出 - 每写一块后调用
fflush($fp),比flush()更底层、更可靠 - 路径必须是绝对路径,
app()->getRootPath() . 'public/files/big.zip'比./files/big.zip少踩 80% 的路径错误
AI 流式代理必须禁用 Nginx proxy_buffering
即使 PHP 层 flush 成功,Nginx 默认开启 proxy_buffering on,会把你的 SSE 数据攒满 64KB 或等到连接关闭才转发给浏览器——用户看到的就是“卡住几秒后突然刷出全部”。
立即学习“PHP免费学习笔记(深入)”;
- Nginx location 块中加两行:
proxy_buffering off;和proxy_cache off; - 务必设置
header('X-Accel-Buffering: no'),这是告诉 Nginx “别碰我这个响应” - 不要依赖
set_time_limit(0),它只管脚本执行时间,不管连接是否被 Nginx 或浏览器主动断开 - 前端必须用
fetch()+ReadableStream.getReader(),XMLHttpRequest对text/event-stream支持极差
Db::cursor() 才是海量导出的真正流式起点
用 chunkById() 导出 100 万行,内存峰值仍可能飙到 200MB——因为它每次 chunk 都要 load 整批结果进数组。真流式必须从数据库游标开始,一行一取、一行一写。
-
$cursor = Db::table('logs')->cursor();返回的是 PDOStatement,不加载任何数据到 PHP 内存 - 循环中只能用
$cursor->fetch(),不能调->toArray()或->all(),否则前功尽弃 - 关联查询必须拆解:先查主表 ID 列表,再用
whereIn('id', $ids)批量查关联表,最后 PHP 合并,避免 JOIN 把整张表拖进内存 - 写 CSV 时用
fputcsv($fp, $row)+fflush($fp),别拼接字符串,字符串拼接本身就会吃光内存
流式最脆弱的地方不在代码逻辑,而在配置链:PHP output_buffering → 中间件 ob 层 → Web 服务器 proxy_buffering → 浏览器 fetch 流解析。任一环没关严,就退回“假流式”。



















