线上地址: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 MB
if (file.size > MAX_SIZE) {
showError('File too large. Maximum 100 MB.')
return
}
同时在页面上明确写出限制,不让用户猜。
教训: 提前告知用户限制,比处理到一半报错体验好得多。
最终效果
现在 sotool.top/merge 支持:
- 大文件合并 — 每个文件最高 100 MB,串行处理不崩溃
- 拖拽排序 — 合并前调整文件顺序
- 浏览器端处理 — 文件不上传服务器
- 实时进度反馈 — 让用户知道处理到哪一步了
体验地址:sotool.top/merge
代码开源在 GitHub,核心逻辑在 src/views/Merge.vue。
你合并过大文件 PDF 吗?遇到过什么坑?