使用Puppeteer批量生成PDF时出现超时及应用卡顿问题的解决方案咨询
Puppeteer批量生成PDF时出现超时及应用卡顿问题的解决方案咨询
看起来你在批量生成PDF时遇到了实打实的麻烦——数据量一上来(10k-20k条)就卡得动不了,还弹出WS端点超时的错误,这确实够头疼的!咱们先拆解下问题出在哪,再一步步优化你的代码。
先说说你当前代码的核心问题
- 无节制的浏览器实例创建:你每次循环都调用
launch()新建一个Puppeteer浏览器,10k条数据就会瞬间启动10k个浏览器进程,系统CPU、内存直接爆掉,超时就是因为浏览器根本启动不过来。 - forEach+async/await的错误用法:forEach不支持异步等待,你的代码会瞬间把所有异步任务扔出去,并发量直接拉满,资源彻底耗尽。
- 低效的数据库操作:每条数据都单独插一次数据库,频繁的连接和查询也会拖慢整个流程。
针对性优化方案
下面是调整后的代码和关键优化点,咱们一步步来:
1. 复用Puppeteer浏览器实例,不要每次都新建
先修改你的generatepdf.js,让它接受外部传入的浏览器实例,只在循环里新建页面(用完就关页面,别关浏览器):
const pdfrouter = async (data, browser) => { try { const templateHtml = fs.readFileSync( join(process.cwd(), "./index.html"), "utf8" ); // 可以把模板编译移到函数外面,缓存起来,不用每次都编译 const template = Handlebars.compile(templateHtml); const html = template(data); const options = { width: '1230px', headerTemplate: "<p></p>", footerTemplate: "<p></p>", displayHeaderFooter: false, margin: { top: "10px", bottom: "30px" }, printBackground: true, }; // 复用已有的浏览器,新建页面 const page = await browser.newPage(); console.log(`开始处理员工ID: ${data.employeeid}`); // 用setContent代替data URI,更高效且避免长度限制 await page.setContent(html, { waitUntil: 'networkidle0' }); const pdfBuffer = await page.pdf(options); await page.close(); // 只关闭页面,保留浏览器 console.log(`员工ID ${data.employeeid}的PDF生成完成`); return { employeeid: data.employeeid.toString(), base64str: pdfBuffer.toString('base64'), testid: data.testid } } catch (err) { console.error(`生成员工ID ${data.employeeid}的PDF出错:`, err); throw err; // 抛出错误,方便后续统一处理 } }
2. 控制并发数,避免资源耗尽
用p-limit库(需要先安装:npm i p-limit)来限制同时运行的PDF生成任务数,根据你的服务器配置调整,比如先设为10,再慢慢试出最佳值。同时修改createPDF函数,批量处理数据库插入:
import { testres1 } from './getdata.js' import { pdfrouter } from "./generatepdf.js"; import { pool } from "./dbconn.js"; import pLimit from 'p-limit'; import format from 'pg-format'; // 用于批量插入,需安装:npm i pg-format // 控制并发数,根据服务器性能调整,比如10 const limit = pLimit(10); async function createPDF() { const batchInsertQuery = 'INSERT INTO base64staging(filename, base64str, testid) VALUES %L'; try { const result = await testres1(pool); if (!result) { console.log('没有需要处理的数据'); return []; } const dataList = JSON.parse(result); console.log(`共需处理 ${dataList.length} 条记录`); // 只启动一次浏览器,全程复用 const browser = await launch({ args: [ '--no-sandbox', '--disable-dev-shm-usage' // 解决Linux下/dev/shm内存不足的问题,常见超时原因 ], headless: "new" }); try { // 用p-limit包装每个PDF生成任务,控制并发 const pdfPromises = dataList.map(data => limit(async () => pdfrouter(data, browser)) ); // 等待所有任务完成(不管成功失败) const pdfResults = await Promise.allSettled(pdfPromises); console.log(`PDF生成完成,共处理 ${pdfResults.length} 条记录`); // 筛选成功的结果,准备批量插入 const successfulEntries = pdfResults .filter(res => res.status === 'fulfilled') .map(res => res.value); console.log(`准备插入 ${successfulEntries.length} 条成功生成的记录`); // 批量插入数据库,减少查询次数 const insertQuery = format(batchInsertQuery, successfulEntries.map(entry => [ entry.employeeid, entry.base64str, entry.testid ])); await pool.query(insertQuery); return successfulEntries; } finally { // 不管成功失败,最后都关闭浏览器 await browser.close(); console.log('浏览器已关闭'); } } catch (err) { console.error('createPDF流程出错:', err); throw err; } } const testres2 = await createPDF();
3. 额外的优化建议
- 缓存Handlebars模板:把
const template = Handlebars.compile(templateHtml)移到pdfrouter函数外面,只编译一次,提升效率。 - 分批次处理超大数据:如果数据量超过5万,可以分成多个批次(比如每批1000条),处理完一批再处理下一批,避免内存占用过高。
- 监控系统资源:用
top或htop查看CPU、内存使用情况,调整并发数,找到服务器能承受的最大值。 - 优化模板加载:如果模板内容固定,可以预加载到内存,不要每次都读文件。
为什么这些优化能解决你的问题?
- 复用浏览器实例:把浏览器启动的开销从10k次降到1次,彻底避免了浏览器启动超时的问题。
- 控制并发:不让系统同时处理过多任务,CPU和内存不会被瞬间占满,应用也就不会卡顿了。
- 批量插入:减少数据库的连接和查询次数,提升整体流程的速度。
备注:内容来源于stack exchange,提问作者Roger Holland
相关产品推荐
相关产品推荐

