PHP 8.2 下载慢需逐层排查:先用开发者工具看TTFB与Content Download,再用curl验证;禁用file_get_contents全量加载,改用stream_copy_to_stream或readfile+准确Content-Length;关闭Nginx/Apache缓冲、清空PHP输出缓冲、去除BOM和隐式输出;强制Cache-Control不缓存,加随机参数或无痕模式排除客户端劫持。

PHP 8.2 网站文件下载速度慢,不是单纯调大超时就能解决的问题,它可能卡在 PHP 层内存堆积、Web 服务器缓冲拦截、网络链路复用旧响应,甚至客户端侧被 CDN 或浏览器缓存静默劫持——必须逐层剥离验证。
第一步:确认是“真慢”还是“假卡”
打开浏览器开发者工具 → Network 标签页 → 触发下载请求 → 查看该请求的 Timing 选项卡:【重点看 Stalled 和 Waiting (TTFB) 时间是否超过 1 秒】。若 TTFB 高但 Content Download 极短,说明卡在服务端准备阶段;若 Content Download 持续缓慢且带宽利用率低,问题大概率出在网络或客户端侧。
用 curl 直接绕过浏览器验证:curl -o /dev/null -s -w 'time_total: %{time_total}s\nspeed_download: %{speed_download}b/s\n' http://yoursite/download.php。如果 speed_download 明显低于服务器带宽,说明 PHP 或 Web 服务器层有瓶颈。
第二步:检查 PHP 是否在内存里硬扛整个文件
方法一:禁用 file_get_contents() 全局搜索
立即学习“PHP免费学习笔记(深入)”;
在项目中全局搜索 file_get_contents( 和 readfile(,尤其注意是否用于下载逻辑。如果发现类似 $data = file_get_contents($path); echo $data; 的写法,【这会导致 GB 级文件全载入内存再吐出,必然慢且易 OOM】。
方法二:强制改用流式转发
将下载逻辑替换为 fopen + stream_copy_to_stream:先用 stream_context_create 设置 timeout=300 和 user_agent,再 fopen 打开文件句柄,最后 stream_copy_to_stream($fp, STDOUT) 直接透传到输出流。此方式不经过 PHP 用户态内存,适合任意大小文件。
方法三:用 readfile() 但必须配 filesize()
readfile() 本身是 C 层 sendfile 封装,快且省内存,但前提是 header('Content-Length: ' . filesize($path)) 必须准确。如果 filesize() 返回 false(路径权限错、符号链接断裂、或文件 >2GB 且系统为 32 位),浏览器会收不到长度头,部分 iOS 客户端直接拒绝下载或无限转圈。
第三步:揪出 Web 服务器偷偷缓存或攒包
① Nginx 用户:检查 location 块中是否含 proxy_buffering on; 或 fastcgi_buffering on; ——这两项会让 Nginx 把整个响应体缓存完再发给浏览器,导致“进度条不动”。应设为 off,并确认 chunked_transfer_encoding on; 已启用。
② Apache 用户:检查是否启用了 mod_deflate。该模块会对所有响应自动 gzip,但二进制文件压缩无效反而增加 CPU 开销,且可能破坏 Content-Length。临时加 SetEnv no-gzip 1 测试是否提速。
③ 通用验证法:curl -I http://yoursite/download.php,看响应头里是否有 Transfer-Encoding: chunked。没有?说明 Web 服务器或 PHP 输出缓冲把流截断了。
第四步:清空 PHP 输出缓冲并封死 header 前任何输出
在下载脚本最开头加入:if (ob_get_level()) ob_end_clean();。PHP 8.2 对输出控制极其敏感,哪怕 php.ini 里 output_buffering=4096,也会让 readfile() 数据先进缓冲区再刷出,造成延迟和内存压力。
检查脚本开头是否有 UTF-8 BOM、空行、echo、print、var_dump,甚至日志扩展(如 xdebug.output_buffering=On)都可能在 header() 前触发隐式输出。一旦发生,header() 失效,Cache-Control 等关键头丢失,浏览器或 CDN 就可能复用旧响应。
用 vim -b 或 VS Code(编码显示设为 “UTF-8 with BOM”)检查文件头,确保无不可见字符。
第五步:验证客户端侧是否被缓存劫持
方法1:加 Cache-Control 强制不缓存
在发送 header 前一次性写全:header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 不要拆成多行 header() 调用,PHP 8.2 中后一次会完全覆盖前一次。
方法2:换域名或加随机参数测试
临时把下载 URL 改为 http://yoursite/download.php?v=123456,或换一个全新二级域名(如 dl.yoursite.com),观察速度是否恢复。若恢复,基本可断定是浏览器或中间代理(如公司防火墙、CDN)在复用旧响应。
方法3:关掉所有浏览器扩展重试
广告拦截、下载管理类插件可能劫持 attachment 响应并自行处理,导致行为异常。用 Chrome 无痕窗口(禁用所有扩展)直连测试,排除干扰。



















