合并大文件 PDF 总卡死?三个优化方案让你告别浏览器崩溃

1 阅读3分钟

线上地址:sotool.top/merge | GitHub:github.com/sunshey/pdf…

上周有个用户反馈:"合并三个 50 MB 的 PDF,浏览器直接崩了。"

我检查了一下,确实。合并 PDF 听起来是个简单操作——把页面从一个文件复制到另一个文件就行。但大文件合并时,如果加载方式不对,浏览器内存会瞬间爆掉。

这篇文章分享一下我们做合并功能时踩到的坑和优化方案。


坑 1:一次性加载所有文件,内存直接爆炸

症状: 用户上传 3 个 50 MB 的 PDF,点击合并后浏览器卡死,最后提示"内存不足"。

原因: 最初的实现用 Promise.all 并行加载所有文件,每个文件都变成一个 PDFDocument 对象挂在内存里。3 个 50 MB 的 PDF 加载后,加上合并后的新文档,峰值内存轻松突破 200 MB。

我的解法:

改成串行处理。一次只加载一个文件,复制完页面后立即释放:

import { PDFDocument } from 'pdf-lib'async function mergeLargePdfs(files) {
  const merged = await PDFDocument.create()
​
  for (const file of files) {
    const bytes = await file.arrayBuffer()
    const sourcePdf = await PDFDocument.load(bytes)
    const pages = await merged.copyPages(sourcePdf, sourcePdf.getPageIndices())
    pages.forEach(page => merged.addPage(page))
    // sourcePdf 现在可以被垃圾回收了
  }
​
  return await merged.save()
}

峰值内存从 200+ MB 降到 50 MB(最大单个文件的大小)。浏览器不再崩溃。

教训: 处理多个大文件时,不要同时把所有文档都挂在内存里,处理一个释放一个。


坑 2:大文件合并时进度条不动

症状: 用户点了合并,页面假死 10 秒,然后突然完成。用户以为工具坏了。

原因: 合并操作在主线程执行,UI 完全无法更新。

我的解法:

requestIdleCallback 让出主线程,或者把处理过程拆成小批次:

async function mergeWithProgress(files, batchSize = 5) {
  const merged = await PDFDocument.create()
​
  for (let i = 0; i < files.length; i += batchSize) {
    const batch = files.slice(i, i + batchSize)
    await Promise.all(batch.map(async (file) => {
      const bytes = await file.arrayBuffer()
      const sourcePdf = await PDFDocument.load(bytes)
      const pages = await merged.copyPages(sourcePdf, sourcePdf.getPageIndices())
      pages.forEach(page => merged.addPage(page))
    }))
​
    // 更新进度条
    updateProgress(i + batchSize, files.length)
​
    // 让出主线程
    await new Promise(resolve => requestIdleCallback(resolve))
  }
​
  return await merged.save()
}

教训: 任何可能阻塞的操作,都要考虑分批 + 进度反馈。用户体验比追求最快完成速度更重要。


坑 3:文件大小限制不明确

症状: 用户上传了一个 200 MB 的文件,合并了一半才报错"文件过大",前面的等待白费了。

我的解法:

在上传前就校验文件大小,超过 100 MB 直接提示:

const MAX_SIZE = 100 * 1024 * 1024 // 100 MBif (file.size > MAX_SIZE) {
  showError('File too large. Maximum 100 MB.')
  return
}

同时在页面上明确写出限制,不让用户猜。

教训: 提前告知用户限制,比处理到一半报错体验好得多。


最终效果

现在 sotool.top/merge 支持:

  1. 大文件合并 — 每个文件最高 100 MB,串行处理不崩溃
  2. 拖拽排序 — 合并前调整文件顺序
  3. 浏览器端处理 — 文件不上传服务器
  4. 实时进度反馈 — 让用户知道处理到哪一步了

体验地址:sotool.top/merge

代码开源在 GitHub,核心逻辑在 src/views/Merge.vue


你合并过大文件 PDF 吗?遇到过什么坑?