Node.js+Next.js从AWS S3下载大文件ZIP超时问题解决方案
解决S3大文件打包下载超时问题(Node.js后端 + Next.js前端)
问题根源
你当前的方案是后端先把S3文件全量拉取到内存、打包成ZIP后再整体返回给前端,这种方式在处理大文件时会触发两个核心问题:
- 后端内存占用暴增,甚至可能出现内存溢出
- 单次HTTP请求传输时间过长,就算调大超时配置,也会因为网络波动、浏览器默认限制等触发超时
优化方案
方案一:前端本地打包(适合文件数量少、单文件不大的场景)
后端不用打包,直接返回每个文件的S3预签名URL,前端批量下载后用JS库本地打包,避免后端压力和长请求超时。
后端代码(Node.js/Next.js API路由):
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); const s3Client = new S3Client({ region: "你的S3区域" }); async function getPresignedUrls(fileIds) { // 根据你的业务逻辑,把fileIds映射为S3对象的key const objectKeys = fileIds.map(id => `files/${id}`); return Promise.all( objectKeys.map(async key => { const command = new GetObjectCommand({ Bucket: "你的S3桶名", Key: key }); const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); return { url, filename: key.split("/")[1] }; }) ); } export default async function handler(req, res) { const { fileIds } = req.body; const presignedUrls = await getPresignedUrls(fileIds); res.status(200).json(presignedUrls); }
前端代码(Next.js):
先依赖安装:
npm install jszip file-saver
修改下载逻辑:
import JSZip from 'jszip'; import { saveAs } from 'file-saver'; const downloadFiles = async (fileIds) => { setProgress(0); const response = await fetch('/api/get-presigned-urls', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-access-token': createJWTToken() }, body: JSON.stringify({ fileIds }) }); const presignedUrls = await response.json(); const zip = new JSZip(); let loadedCount = 0; await Promise.all( presignedUrls.map(async ({ url, filename }) => { const fileResponse = await fetch(url); const blob = await fileResponse.blob(); zip.file(filename, blob); loadedCount++; setProgress(Math.ceil((loadedCount / presignedUrls.length) * 100)); }) ); const zipBlob = await zip.generateAsync({ type: 'blob' }); saveAs(zipBlob, `FileDrop-${Date.now()}.zip`); };
方案二:后端流式打包+流式返回(适合大文件/多文件场景)
不把整个文件加载到内存,从S3流式读取文件,同时流式写入ZIP,再通过HTTP分块传输返回给前端,彻底解决内存和超时问题。
后端代码(Node.js/Next.js API路由):
先依赖安装:
npm install archiver
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3"); const archiver = require('archiver'); const s3Client = new S3Client({ region: "你的S3区域" }); export default async function handler(req, res) { const { fileIds } = req.body; const objectKeys = fileIds.map(id => `files/${id}`); // 设置响应头,开启分块传输 res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename=FileDrop-${Date.now()}.zip`); res.setHeader('Transfer-Encoding', 'chunked'); const archive = archiver('zip', { zlib: { level: 0 } }); // 关闭压缩提速,需要压缩可调整level值 // 将打包流pipe到响应对象 archive.pipe(res); // 遍历文件,流式添加到ZIP for (const key of objectKeys) { const command = new GetObjectCommand({ Bucket: "你的S3桶名", Key: key }); const { Body } = await s3Client.send(command); archive.append(Body, { name: key.split("/")[1] }); } // 完成打包 archive.finalize(); // 错误处理 archive.on('error', (err) => { res.status(500).end('打包失败'); }); }
前端调整:
保留原有XHR逻辑,针对分块传输调整进度显示(分块传输时e.total可能无法获取):
xhr.onprogress = function (e) { if (e.lengthComputable) { const progress = (e.loaded / e.total) * 100; setProgress(Math.ceil(progress)); } else { // 无法计算总进度时,显示已加载的字节数 setProgress(`已加载 ${(e.loaded / 1024 / 1024).toFixed(2)}MB`); } };
方案三:预打包缓存(适合高频下载的固定文件组合)
如果某些文件组合经常被下载,可以提前用AWS Lambda打包并存到S3,用户请求时直接返回预签名URL:
- 用户请求打包时,先检查S3中是否已有对应组合的ZIP文件
- 若存在,直接返回预签名URL;若不存在,触发Lambda任务打包并存到S3,再返回URL
- Lambda中采用方案二的流式打包逻辑,避免内存溢出
前端额外优化
- 用
AbortController给请求添加取消功能,方便用户中断大文件下载 - 增加请求重试逻辑,针对网络波动导致的中断自动重试
- 替换XHR为Fetch API的
ReadableStream,更灵活处理流式响应
内容的提问来源于stack exchange,提问作者Frontend
相关产品推荐
相关产品推荐

