必须用response()->stream()而非download()处理大文件,因其边读边发、内存恒定几KB;需ob_end_clean()、fread+flush、禁用Nginx缓冲,并对S3等远程文件优先用预签名URL重定向或流包装器拉取。

大文件下载在 Laravel 中不能靠“一键 download”解决。直接用 response()->download() 或 Storage::download() 处理百兆级以上文件,极易触发内存溢出、超时中断或响应卡死——根本原因是这些方法会把整个文件加载进 PHP 内存再输出。真正可靠的方案是绕过全量读取,改用流式分块传输,让数据边读边发,内存占用恒定在几 KB。
什么时候必须用 stream() 而不是 download()
当文件体积超过 20MB、存储在远程驱动(如 S3/FTP)、或需支持断点续传/自定义响应头时,download() 就不再适用。它底层调用 readfile() 并尝试探测 MIME 类型和计算 Content-Length,在 NFS、对象存储挂载路径下反而更慢甚至失败。而 response()->stream() 把控制权交还给你:打开文件句柄、按需读取、立即 flush,不缓存、不预加载。
- 本地大日志、导出报表、视频片段等静态文件
- 从 S3 等远程存储拉取但又不想落地到本地磁盘
- 需要动态设置
Content-Range实现断点续传 - 需兼容 Nginx / Apache 的代理缓冲策略
stream() 的关键操作四步法
仅写一个回调函数远远不够。流式响应生效依赖前后端协同,缺一不可:
-
手动关闭输出缓冲:开头必须调用
ob_end_clean(),否则中间件残留的缓冲会导致 “headers already sent” 错误 -
分块读取 + 即时刷新:用
fopen($path, 'rb')打开只读流,每次fread($handle, 8192)后紧跟echo + flush() + ob_flush() -
禁用 Web 服务器缓冲:Nginx 需配置
fastcgi_buffering off和proxy_buffering off;Apache 则要确保mod_deflate不介入二进制流 -
设置防缓存与直通头:响应头中必须包含
X-Accel-Buffering: no(Nginx)、Cache-Control: no-cache、Content-Transfer-Encoding: binary
远程文件(S3/FTP)不落地下载的两种安全路径
不让文件经过 Laravel 应用中转,是最省资源的做法:
- 公有资源 → 302 重定向到预签名 URL:适用于 S3 公有桶或临时授权链接,浏览器直接向对象存储发起请求,完全卸载应用压力
-
私有资源 → 流包装器按需拉取:使用
stream_wrapper_register()注册自定义协议(如s3://),在stream()回调中用 SDK 分块 fetch,避免一次性下载整份内容到临时目录 - 切忌对 S3 文件调用
Storage::disk('s3')->download():它会先 pull 到sys_get_temp_dir()再readfile(),既慢又占磁盘空间
多文件打包下载:用 MediaStream 替代 ZIP 生成
用户要下载 10 个 PDF,传统做法是用 ZipArchive 先生成完整压缩包再返回 —— 这样要等全部文件读完、压缩完才能开始传输。Laravel MediaLibrary 提供的 MediaStream 改变了这个逻辑:
- 实时流式压缩:ZIP 结构边构建边发送,无需等待所有文件就绪
- 混合来源支持:可同时加入本地存储、S3、甚至 HTTP 远程 URL 的文件
- 内存恒定:无论打包多少个文件,PHP 进程内存增长几乎为零
- 示例:
return MediaStream::create('reports.zip')->addMedia($mediaCollection);



















