You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

  1. 用户请求打包时,先检查S3中是否已有对应组合的ZIP文件
  2. 若存在,直接返回预签名URL;若不存在,触发Lambda任务打包并存到S3,再返回URL
  3. Lambda中采用方案二的流式打包逻辑,避免内存溢出

前端额外优化

  • 用AbortController给请求添加取消功能,方便用户中断大文件下载
  • 增加请求重试逻辑,针对网络波动导致的中断自动重试
  • 替换XHR为Fetch API的ReadableStream,更灵活处理流式响应

内容的提问来源于stack exchange,提问作者Frontend

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 04:07:47