Laravel图片压缩下载速度优化方案咨询
批量图片压缩下载速度优化方案(Laravel)
问题背景
基于Laravel实现的批量图片压缩下载功能,浏览器显示"Content Download"阶段耗时过长,现有实现代码如下:
API路由(api.php)
Route::post('/download-images', [DownloadController::class, 'downloadImages'])->name('api.download.images');
Controller代码
public function downloadImages(Request $request) { $zipFileName = $this->service->downloadImages($request); return response()->download($zipFileName, null, ['Content-Type: application/octet-stream','Content-Length: '. filesize($zipFileName)])->deleteFileAfterSend(true); }
Service代码
public function downloadImages(Request $request) { $imageUrls = $request->get('images'); $type = $request->get('type') ?? 'images'; $zip = new ZipArchive(); $zipFileName = $type . '.zip'; $zip = new ZipArchive(); if ($zip->open($zipFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { foreach ($imageUrls as $imageUrl) { $imageContent = file_get_contents($imageUrl); $filename = basename($imageUrl); $zip->addFromString($filename, $imageContent); } $zip->close(); return $zipFileName; } return $zipFileName; }
前端AJAX调用
function downloadImages(eleClass) { $("div.spanner").addClass("show"); $("div.overlay").addClass("show"); const imageLinks = []; $('ul.'+ eleClass +' li img').each(function() { var imageLink = $(this).attr('src'); imageLinks.push(imageLink); }); if (imageLinks.length == 0) { $("div.spanner").removeClass("show"); $("div.overlay").removeClass("show"); return; } $.ajax({ url: '/api/download-images', method: 'POST', data: { images: imageLinks }, xhrFields: { responseType: 'blob' // Set the response type to 'blob' }, success: function (data, status, xhr) { // Handle success, e.g., show a success message console.log('Images downloaded successfully.'); // Create a temporary anchor element var downloadLink = document.createElement('a'); downloadLink.href = window.URL.createObjectURL(data); // Create a Blob URL for the response downloadLink.download = eleClass + '.zip'; // Set the desired file name // Programmatically trigger the download downloadLink.click(); $("div.spanner").removeClass("show"); $("div.overlay").removeClass("show"); }, error: function (xhr, status, error) { // Handle error, e.g., display an error message console.error('Error downloading images:', error); $("div.spanner").removeClass("show"); $("div.overlay").removeClass("show"); } }); }
性能瓶颈分析
- 同步串行下载图片:
file_get_contents同步逐个下载图片,总耗时等于所有图片下载时间之和 - 本地磁盘IO开销:先将所有图片下载到本地并生成Zip文件,再输出给浏览器,等待时间长
- 前端Blob处理冗余:AJAX将响应转为Blob后再触发下载,大文件会占用大量浏览器内存,拖慢下载流程
优化方案
方案一:并行下载+流式Zip输出(核心优化)
使用Guzzle并行请求图片,同时用ZipStream直接流式输出Zip包,无需生成本地文件,彻底消除磁盘IO等待,同时并行下载大幅缩短图片获取时间。
步骤1:安装依赖
composer require maennchen/zipstream-php guzzlehttp/guzzle
步骤2:修改Controller代码
use ZipStream\ZipStream; use GuzzleHttp\Client; use GuzzleHttp\Promise; public function downloadImages(Request $request) { $imageUrls = $request->get('images'); $type = $request->get('type') ?? 'images'; // 初始化Guzzle客户端,支持并行请求 $client = new Client(['timeout' => 10]); // 创建异步请求队列 $promises = []; foreach ($imageUrls as $url) { // 用临时文件存储下载的图片,避免占用内存 $promises[$url] = $client->getAsync($url, ['sink' => tmpfile()]); } // 等待所有请求完成(并行执行,总耗时接近最慢的那张图片下载时间) $responses = Promise\Utils::unwrap($promises); // 流式输出Zip包,边打包边发送给浏览器 return response()->stream(function () use ($responses, $type) { $zip = new ZipStream("$type.zip"); foreach ($responses as $url => $response) { $filename = basename($url); // 从临时文件读取内容添加到Zip rewind($response->getBody()->getMetadata('uri')); $zip->addFileFromStream($filename, $response->getBody()->getMetadata('uri')); } $zip->finish(); }, 200, [ 'Content-Type' => 'application/zip', 'Content-Disposition' => "attachment; filename=\"$type.zip\"", ]); }
方案二:前端改用表单提交(消除Blob开销)
放弃AJAX+Blob的方式,直接用动态表单提交,让浏览器原生处理下载流程,避免内存占用和Blob转换耗时。
修改前端代码
function downloadImages(eleClass) { $("div.spanner").addClass("show"); $("div.overlay").addClass("show"); const imageLinks = []; $('ul.'+ eleClass +' li img').each(function() { var imageLink = $(this).attr('src'); imageLinks.push(imageLink); }); if (imageLinks.length == 0) { $("div.spanner").removeClass("show"); $("div.overlay").removeClass("show"); return; } // 创建隐藏表单提交请求 const form = document.createElement('form'); form.method = 'POST'; form.action = '/api/download-images'; form.style.display = 'none'; // 添加CSRF令牌(Laravel必填) const csrfInput = document.createElement('input'); csrfInput.type = 'hidden'; csrfInput.name = '_token'; csrfInput.value = $('meta[name="csrf-token"]').attr('content'); form.appendChild(csrfInput); // 批量添加图片URL参数 imageLinks.forEach((url, index) => { const input = document.createElement('input'); input.type = 'hidden'; input.name = `images[${index}]`; input.value = url; form.appendChild(input); }); // 添加type参数 const typeInput = document.createElement('input'); typeInput.type = 'hidden'; typeInput.name = 'type'; typeInput.value = eleClass; form.appendChild(typeInput); document.body.appendChild(form); form.submit(); document.body.removeChild(form); // 隐藏加载动画(可根据实际下载时间调整,或结合后端通知优化) setTimeout(() => { $("div.spanner").removeClass("show"); $("div.overlay").removeClass("show"); }, 3000); }
方案三:图片预压缩+CDN加速(辅助优化)
如果图片存储在自有服务器:
- 提前用工具(如Intervention Image)对图片进行压缩:
// 示例:下载图片后压缩再打包(仅适用于自有图片) use Intervention\Image\Facades\Image; // 在原有Service代码中修改 $imageContent = file_get_contents($imageUrl); $compressedImg = Image::make($imageContent)->encode('jpg', 70); // 压缩至70%质量 $zip->addFromString($filename, $compressedImg->__toString());
- 将图片托管到CDN,利用CDN的边缘节点加速图片下载,减少后端获取图片的耗时。
总结
优先组合使用方案一+方案二,可大幅缩短"Content Download"阶段耗时:
- 并行下载图片将总下载时间从串行求和变为接近单张最长耗时
- 流式输出Zip无需等待本地文件生成,浏览器可立即开始接收数据
- 表单提交消除前端Blob处理的内存开销和转换耗时
内容的提问来源于stack exchange,提问作者Dinh Luong
相关产品推荐
相关产品推荐

