使用 Puppeteer 编写内存泄漏自动化检测脚本

Puppeteer 是一个 Node 库,提供高级 API 来控制 Chrome 或 Chromium,非常适合用于自动化检测前端内存泄漏问题。下面我将详细介绍如何使用 Puppeteer 编写内存泄漏检测脚本。

基础环境设置

首先安装 Puppeteer:

bash 复制代码
npm install puppeteer

基本内存检测脚本

javascript 复制代码
const puppeteer = require('puppeteer');

async function detectMemoryLeaks(url) {
  const browser = await puppeteer.launch({
    headless: false, // 设置为 true 在生产环境
    devtools: true   // 打开开发者工具
  });
  
  const page = await browser.newPage();
  
  // 启用性能监控
  await page.goto(url, { waitUntil: 'networkidle2' });
  
  // 获取初始内存使用情况
  const initialMemory = await page.metrics();
  console.log('初始内存:', initialMemory.JSHeapUsedSize / 1024 / 1024, 'MB');
  
  // 执行可能引起内存泄漏的操作
  for (let i = 0; i < 10; i++) {
    await page.click('#some-button'); // 替换为你的交互操作
    await page.waitForTimeout(1000);
    
    // 获取当前内存使用
    const currentMemory = await page.metrics();
    const usedMB = currentMemory.JSHeapUsedSize / 1024 / 1024;
    console.log(`操作 ${i+1} 后内存:`, usedMB, 'MB');
    
    // 检查内存增长是否异常
    if (usedMB > initialMemory.JSHeapUsedSize / 1024 / 1024 * 2) {
      console.warn('⚠️ 检测到可能的内存泄漏!');
    }
  }
  
  await browser.close();
}

// 测试你的页面
detectMemoryLeaks('http://localhost:3000');

高级内存分析脚本

javascript 复制代码
const puppeteer = require('puppeteer');
const fs = require('fs');

async function advancedMemoryAnalysis(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  // 1. 获取初始堆快照
  await page.goto(url);
  const client = await page.target().createCDPSession();
  await client.send('HeapProfiler.enable');
  
  // 保存初始堆快照
  const initialSnapshot = await client.send('HeapProfiler.takeHeapSnapshot', {
    reportProgress: false
  });
  fs.writeFileSync('initial.heapsnapshot', JSON.stringify(initialSnapshot));
  
  // 2. 执行测试操作
  for (let i = 0; i < 5; i++) {
    await page.click('#leaky-button');
    await page.waitForTimeout(1000);
  }
  
  // 3. 获取操作后堆快照
  const finalSnapshot = await client.send('HeapProfiler.takeHeapSnapshot', {
    reportProgress: false
  });
  fs.writeFileSync('final.heapsnapshot', JSON.stringify(finalSnapshot));
  
  // 4. 比较内存变化
  const initialMetrics = await page.metrics();
  const finalMetrics = await page.metrics();
  
  console.log('内存变化分析:');
  console.log('JS堆初始大小:', initialMetrics.JSHeapUsedSize / 1024 / 1024, 'MB');
  console.log('JS堆最终大小:', finalMetrics.JSHeapUsedSize / 1024 / 1024, 'MB');
  console.log('DOM节点数变化:', finalMetrics.Nodes - initialMetrics.Nodes);
  
  await browser.close();
  
  console.log('分析完成,可以使用Chrome开发者工具加载 .heapsnapshot 文件进行详细比较');
}

advancedMemoryAnalysis('http://localhost:3000');

定时内存监控脚本

javascript 复制代码
const puppeteer = require('puppeteer');
const { setInterval, clearInterval } = require('timers');

async function monitorMemory(url, duration = 60000) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  
  const memoryData = [];
  const startTime = Date.now();
  
  const interval = setInterval(async () => {
    const metrics = await page.metrics();
    const timeElapsed = (Date.now() - startTime) / 1000;
    const memoryUsage = metrics.JSHeapUsedSize / 1024 / 1024;
    
    memoryData.push({
      time: timeElapsed,
      memory: memoryUsage
    });
    
    console.log(`[${timeElapsed.toFixed(1)}s] 内存使用: ${memoryUsage.toFixed(2)} MB`);
    
    if (Date.now() - startTime >= duration) {
      clearInterval(interval);
      await browser.close();
      
      // 生成简单报告
      const initial = memoryData[0].memory;
      const peak = Math.max(...memoryData.map(d => d.memory));
      const growth = ((peak - initial) / initial * 100).toFixed(2);
      
      console.log('\n内存监控报告:');
      console.log(`初始内存: ${initial.toFixed(2)} MB`);
      console.log(`峰值内存: ${peak.toFixed(2)} MB`);
      console.log(`内存增长率: ${growth}%`);
      
      if (growth > 50) {
        console.warn('⚠️ 检测到显著内存增长,可能存在内存泄漏');
      }
    }
  }, 5000);
}

monitorMemory('http://localhost:3000', 300000); // 监控5分钟

实战技巧

  1. 模拟用户操作

    javascript 复制代码
    // 模拟一系列用户操作
    async function simulateUserFlow(page) {
      await page.type('#username', 'testuser');
      await page.type('#password', 'password123');
      await page.click('#login-btn');
      await page.waitForNavigation();
      await page.click('#open-modal');
      await page.waitForSelector('.modal');
      await page.click('.close-modal');
    }
  2. 检测DOM节点泄漏

    javascript 复制代码
    async function checkDOMLeaks(page) {
      const initialCount = (await page.evaluate(() => document.querySelectorAll('*').length));
      
      // 执行操作
      for (let i = 0; i < 10; i++) {
        await page.click('#add-element');
        await page.click('#remove-element');
      }
      
      const finalCount = (await page.evaluate(() => document.querySelectorAll('*').length));
      
      if (finalCount > initialCount * 1.2) {
        console.warn('DOM节点数异常增长:', initialCount, '->', finalCount);
      }
    }
  3. 检测事件监听器泄漏

    javascript 复制代码
    async function checkEventListenerLeaks(page) {
      const initialListeners = await page.evaluate(() => {
        return Array.from(document.querySelectorAll('*')).reduce((count, el) => {
          return count + (el._events ? Object.keys(el._events).length : 0);
        }, 0);
      });
      
      // 执行操作
      await page.click('#add-listeners');
      await page.click('#remove-listeners');
      
      const finalListeners = await page.evaluate(() => {
        return Array.from(document.querySelectorAll('*')).reduce((count, el) => {
          return count + (el._events ? Object.keys(el._events).length : 0);
        }, 0);
      });
      
      if (finalListeners > initialListeners) {
        console.warn('事件监听器泄漏:', initialListeners, '->', finalListeners);
      }
    }

集成到CI/CD流程

javascript 复制代码
const puppeteer = require('puppeteer');
const assert = require('assert');

describe('内存泄漏测试', () => {
  let browser;
  let page;
  
  before(async () => {
    browser = await puppeteer.launch();
    page = await browser.newPage();
    await page.goto('http://localhost:3000');
  });
  
  after(async () => {
    await browser.close();
  });
  
  it('不应有显著内存增长', async () => {
    const initial = await page.metrics();
    
    // 执行测试操作
    for (let i = 0; i < 10; i++) {
      await page.click('#leaky-component');
      await page.waitForTimeout(500);
    }
    
    const final = await page.metrics();
    const growth = (final.JSHeapUsedSize - initial.JSHeapUsedSize) / initial.JSHeapUsedSize;
    
    assert.ok(growth < 0.5, `内存增长超过50% (${(growth*100).toFixed(2)}%)`);
  });
  
  it('DOM节点数应保持稳定', async () => {
    const initial = await page.evaluate(() => document.querySelectorAll('*').length);
    
    // 执行测试操作
    await page.click('#render-list');
    await page.click('#clear-list');
    
    const final = await page.evaluate(() => document.querySelectorAll('*').length);
    const diff = final - initial;
    
    assert.ok(diff <= 5, `DOM节点数差异过大 (${diff}个)`);
  });
});

分析建议

  1. 结合Chrome开发者工具

    • 将生成的堆快照(.heapsnapshot)加载到Chrome开发者工具的Memory面板中
    • 比较不同时间点的快照,找出内存增长的对象
  2. 重点关注

    • 分离的DOM树(Detached DOM tree)
    • 持续增长的数组或对象
    • 未被释放的闭包
  3. 常见模式检查

    • 循环引用
    • 未清理的定时器
    • 全局变量积累
    • 未取消的事件监听器

通过以上Puppeteer脚本,你可以自动化地检测前端应用中的内存泄漏问题,并将其集成到开发流程中,提前发现和解决内存问题。

相关推荐
GIS之路9 分钟前
GeoTools 结合 OpenLayers 实现属性查询(二)
前端·信息可视化
烛阴17 分钟前
一文搞懂 Python 闭包:让你的代码瞬间“高级”起来!
前端·python
AA-代码批发V哥21 分钟前
HTML之表单结构全解析
前端·html
聪聪的学习笔记32 分钟前
【1】确认安装 Node.js 和 npm版本号
前端·npm·node.js
小磊哥er44 分钟前
【前端工程化】你知道前端编码规范包含哪些内容吗
前端
菌菇汤1 小时前
uni-app实现单选,多选也能搜索,勾选,选择,回显
前端·javascript·vue.js·微信小程序·uni-app·app
Ramos丶1 小时前
【ABAP】 从无到有 新建一个Webdynpro程序
java·前端·javascript
qq_411671981 小时前
vue3 的模板引用ref和$parent
前端·javascript·vue.js
清幽竹客3 小时前
vue-37(模拟依赖项进行隔离测试)
前端·vue.js
vvilkim3 小时前
Nuxt.js 页面与布局系统深度解析:构建高效 Vue 应用的关键
前端·javascript·vue.js