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

Node.js中合并大PDF文件避免内存溢出的解决方案

解决大PDF文件合并时的Node.js内存溢出问题

核心问题分析

你遇到的内存溢出并非V8堆内存不足(heapUsed无明显变化),而是**外部内存(External Memory)和数组缓冲区(ArrayBuffers)**累积导致的——这部分内存由Node.js底层C++模块(比如PDF处理库)占用,不在V8的自动GC回收范围内,逐次合并时旧的缓冲区/资源引用未释放,导致内存翻倍增长。

针对性解决方案

1. 放弃内存缓存PDF,改用磁盘临时文件存储

不要将所有生成的PDF存入内存数组,而是让Puppeteer直接将PDF写入磁盘临时文件,合并时直接读取磁盘文件而非内存Buffer:

const puppeteer = require('puppeteer');
const fs = require('fs').promises;
const path = require('path');

// 生成单个PDF并写入临时文件
async function generatePdfToTemp(pageContent, tempPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(pageContent);
  // 直接写入磁盘,不返回Buffer
  await page.pdf({ path: tempPath, format: 'A4', printBackground: true });
  await browser.close();
}

// 示例:生成30个临时PDF文件
const tempPdfPaths = [];
for (let i = 0; i < 30; i++) {
  const tempPath = path.join('./temp', `pdf-${i}.pdf`);
  await generatePdfToTemp(/* 你的页面内容 */, tempPath);
  tempPdfPaths.push(tempPath);
}

2. 复用单个PDFMerger实例,避免频繁创建

不要每次迭代新建PDFMerger实例,用同一个实例逐个添加磁盘文件,最后一次性输出:

const PDFMerger = require('pdf-merger-js');

async function mergeAllPdfs(inputPaths, outputPath) {
  const merger = new PDFMerger();
  // 逐个添加磁盘文件
  for (const pdfPath of inputPaths) {
    await merger.add(pdfPath);
  }
  // 一次性合并输出到最终文件
  await merger.save(outputPath);
  // 清理临时文件
  await Promise.all(inputPaths.map(p => fs.unlink(p)));
}

// 调用合并
await mergeAllPdfs(tempPdfPaths, './final.pdf');

3. 分批次合并,降低单次内存占用

如果文件数量/体积过大,可先将PDF分成小批次合并为中间临时文件,再合并这些中间文件:

async function mergeBatch(batchPaths, batchOutputPath) {
  const merger = new PDFMerger();
  for (const path of batchPaths) {
    await merger.add(path);
  }
  await merger.save(batchOutputPath);
  // 合并后立即删除当前批次的源文件,释放磁盘空间+避免内存残留
  await Promise.all(batchPaths.map(p => fs.unlink(p)));
}

async function mergeLargePdfs(allInputPaths, finalOutputPath, batchSize = 5) {
  const batchOutputs = [];
  // 分批次合并
  for (let i = 0; i < allInputPaths.length; i += batchSize) {
    const batch = allInputPaths.slice(i, i + batchSize);
    const batchOutput = path.join('./temp', `batch-${Math.floor(i/batchSize)}.pdf`);
    await mergeBatch(batch, batchOutput);
    batchOutputs.push(batchOutput);
  }
  // 合并所有批次输出
  await mergeAllPdfs(batchOutputs, finalOutputPath);
}

// 调用分批次合并
await mergeLargePdfs(tempPdfPaths, './final.pdf', 5);

4. 强制释放外部内存(可选)

如果仍存在内存残留,可在关键节点手动触发GC(需启动Node.js时添加--expose-gc参数),并主动切断Buffer引用:

// 在每次合并批次后触发GC
await mergeBatch(batch, batchOutput);
global.gc(); // 仅在启动时加--expose-gc可用

5. 调整Node.js内存限制(临时缓解)

启动Node.js时增大旧空间内存限制,比如:

node --max-old-space-size=8192 your-script.js

注:这只是临时方案,无法从根本解决外部内存累积问题,仅适用于应急场景。

为什么之前的方法无效?

你之前逐次创建新PDFMerger实例并合并缓冲区的方式,会导致旧的Buffer实例和底层PDF处理资源未被释放——每次合并都会复制一份Buffer数据,而外部内存(C++层的PDF解析资源)不会被V8自动回收,最终导致内存翻倍直至溢出。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 17:55:57