Puppeteer运行触发EventEmitter内存泄漏 11个exit监听器绑定process问题
问题产生原因
- 核心触发原因:代码一次性并行启动了超过10个Puppeteer浏览器实例。Puppeteer每次执行
puppeteer.launch()时,都会自动给process对象注册exit、SIGINT、SIGTERM、SIGHUP四类事件的监听器,用于Node进程异常退出时自动清理残留的浏览器进程,避免孤儿进程占用资源。Node.js的EventEmitter默认单事件最多允许注册10个监听器,你启动了11个浏览器实例就触发了阈值警告。 - 代码写法缺陷放大了问题:
await products?.map(async ilan => {})写法无效,map返回的是Promise数组,直接await不会等待所有异步任务执行完成,导致所有浏览器实例几乎同时启动,没有先后顺序- 每个遍历逻辑中套了多余的立即执行async函数,进一步脱离了外层的异步管控,即使任务抛出错误也可能导致
await browser.close()没有执行,已经注册的监听器无法被移除,出现永久内存泄漏 - 没有做并发数控制,产品数量越多,启动的浏览器实例越多,监听器泄漏的问题会越严重
解决方法
最优方案:复用浏览器实例(推荐)
不要每次处理产品都新建浏览器,全局只启动1个浏览器实例,每个产品任务仅新建Page标签页即可,从根源上避免重复注册process监听器,同时大幅降低CPU、内存资源消耗。
修复异步逻辑+控制并发
如果业务场景确实需要多浏览器实例,要做好异步等待和并发限制:
- 把无效的map+await写法换成
Promise.all或者for...of串行执行,保证前一个任务完成、浏览器正常关闭后再启动下一个 - 把
browser.close()放到finally块中,不管任务成功还是报错都能正常关闭浏览器、卸载监听器 - 控制同时运行的浏览器实例数不超过10个,避免触发默认阈值
临时规避方案(不推荐)
如果只是临时调试需要启动大量实例,可以手动调高Node事件监听器的阈值,在代码最顶部加入:
require('events').EventEmitter.defaultMaxListeners = 50 // 按实际需要调整数值
该方案仅屏蔽警告,没有解决根本的资源泄漏问题,不建议生产环境使用。
优化后的代码示例
try { // 全局仅启动一个浏览器实例 const browser = await puppeteer.launch({ // devtools: true // defaultViewport: null, // headless: true, // executablePath: '/usr/bin/chromium-browser', // args: ['--no-sandbox'], }); // 用for...of串行执行任务,如需控制并发可以使用p-limit等工具库 for (const ilan of products || []) { const page = await browser.newPage(); try { const email = ""; const password = ""; const title = ilan.title; const location = ilan.Country[random].country; const categories = ilan.category; const link = ilan.link; const description = ilan.description; const manufacturer = ilan.manufacturer; const model = ilan.model; const year = ilan.year; console.log('title', title) await page.goto('你的目标提交地址'); async function input(focus, deger) { await page.focus(focus) await page.keyboard.type(deger) } await input('#user_email', email) await input('#user_password', password) // 提交登录 await page.click('input[type="submit"]') await page.waitForTimeout(10000) // 填充表单 await input('input[id="api_listing_title"]', title) await input('input[id="api_listing_location"]', location) await input('input[type="search"]', categories) await page.click('li[role="option"]') await input('input[id="youtube_link"]', link) await input('textarea[name="api_listing[description]"]', description) await input('input[name="api_listing[manufacturer]"]', manufacturer) await input('input[name="api_listing[model]"]', model) await input('input[name="api_listing[year]"]', year) await page.select('#api_listing_condition', 'new') // 上传图片 const inputUploadHandle = await page.$('input[type=file]'); // 修复图片上传的异步等待逻辑 for (const image of ilan.images || []) { await inputUploadHandle.uploadFile(image.path) } await page.waitForTimeout(10000) await page.click('input[name="commit"]') await page.waitForTimeout(15000) } catch (error) { console.log("🌵💜🐢 error", error) res.json({ message: 'error' }) } finally { // 无论任务成功失败都关闭当前页面,释放资源 await page.close(); } } // 所有任务执行完成后关闭浏览器 await browser.close(); } catch (err) { console.error('全局执行错误', err) }
内容的提问来源于stack exchange,提问作者Yunus Emre Acar
相关产品推荐
相关产品推荐

