使用 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脚本,你可以自动化地检测前端应用中的内存泄漏问题,并将其集成到开发流程中,提前发现和解决内存问题。

相关推荐
杰克尼20 小时前
vue_day04
前端·javascript·vue.js
明远湖之鱼20 小时前
浅入理解跨端渲染:从零实现 React DSL 跨端渲染机制
前端·react native·react.js
悟忧21 小时前
规避ProseMirror React渲染差异带来的BUG
前端
小皮虾21 小时前
小程序云开发有类似 uniCloud 云对象的方案吗?有的兄弟,有的!
前端·javascript·小程序·云开发
Android疑难杂症21 小时前
鸿蒙Notification Kit通知服务开发快速指南
android·前端·harmonyos
T___T21 小时前
全方位解释 JavaScript 执行机制(从底层到实战)
前端·面试
阳懿21 小时前
meta-llama-3-8B下载失败解决。
前端·javascript·html
Qinana21 小时前
🌊 深入理解 CSS:从选择器到层叠的艺术
前端·css·程序员
IT_陈寒21 小时前
Python 3.12新特性实测:10个让你的代码提速30%的隐藏技巧 🚀
前端·人工智能·后端
闲人编程21 小时前
从零开发一个简单的Web爬虫(使用Requests和BeautifulSoup)
前端·爬虫·beautifulsoup·bs4·web·request·codecapsule