线上好好的回家睡一觉就崩了?我用 Playwright 把 SPA 的内存泄漏变成了 CI 的红红叉
线上好好的,回家睡一觉就崩了——这件事每个前端工程师都遇到过,但你永远不知道是哪个页面在偷偷吃内存。
问题在于,大多数内存泄漏不会在开发环境露头。局部测试 5 分钟、10 分钟,根本看不出端倪。只有用户真正长时间使用、页面反复路由切换,内存才会慢慢累积,等到某个临界点,页面彻底卡死或直接崩溃。
这件事今天有解了——Playwright 的 Soak Test 把内存泄漏检测变成了 CI 管道的自动红叉。
什么是 Soak Test
Soak Test(浸泡测试)不是新概念,传统后端服务早就用上了:让服务持续运行一段时间,观察内存、连接数、响应时间有没有爬升。
把它搬到 SPA 上,逻辑是一样的:用 Playwright 写一个循环脚本,让它反复进入同一个页面,监控 DOM 节点数和事件监听器计数有没有增长。
增长就说明有泄漏,不增长就是安全的——这个判断标准比任何堆快照分析都简单。
核心实现:三件事
第一,循环路由。 用 Playwright 的 page.goto() 反复进入目标页面,配合 waitForLoadState('networkidle') 确保页面完整渲染。
// Playwright Soak Test 核心脚本
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
let prevCount = await page.evaluate(() =>
document.querySelectorAll('*').length
);
for (let i = 0; i < 100; i++) {
await page.goto('https://your-app/dashboard');
await page.waitForLoadState('networkidle');
const currentCount = await page.evaluate(() =>
document.querySelectorAll('*').length
);
if (currentCount > prevCount * 1.05) {
console.error(`循环 ${i + 1} 次:DOM 节点从 ${prevCount} 增长到 ${currentCount},疑似泄漏`);
process.exit(1);
}
prevCount = currentCount;
}
await browser.close();
console.log('100 次循环,DOM 节点稳定,测试通过');
})();
第二,事件监听器计数。 DOM 节点数只是表面,事件监听器泄漏才是 SPA 的重灾区。用 Chrome DevTools Protocol 的 Runtime.queryObjects() 可以拿到页面内所有对象,从而间接推断监听器泄漏。
// 检测 EventTarget 对象增长(Chrome only)
const listenerCount = await page.evaluate(async () => {
const protocol = await page.createCDPSession(page);
const { objects } = await protocol.send('Runtime.queryObjects', {
prototypeObjectName: 'EventTarget'
});
return objects.length;
});
第三,集成进 CI。 GitHub Actions 跑这个脚本,失败就 Block 合并——和单元测试、类型检查一样的待遇。
name: Soak Test
on: [push, pull_request]
jobs:
soak-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install playwright && npx playwright install chromium
- run: node soak-test.js
为什么这个思路值
传统的内存泄漏排查靠 Chrome DevTools 手动快照、两份快照对比——耗时、依赖人工、没法自动化。
Soak Test 换了一个角度:不问”哪里漏了”,只问”有没有在漏”。只要 CI 变红,你就知道这个页面有问题,然后再来查是哪个组件、哪个生命周期钩子。排查范围直接缩到最小。
再加上它跑在 CI 里,每次 PR 都过一遍——等于给每个路由页面都装了一个内存健康度的体温计,不用等用户给你报错。
落地方骤
如果你想把这个方案落地,分三步走:
第一步,先挑一个高频页面(通常是 Dashboard 或者表单页)写一个 50 次循环的 Soak Test,GitHub Actions 跑通,感受一下这个检测有多灵敏。
第二步,把循环次数逐步提到 200~500 次,覆盖更多路由场景。同时把 DOM 节点数和 EventTarget 对象数作为两个独立指标一起监控。
第三步,把 Soak Test 纳入 CI Gate——Fail 就 Block 合并。内存泄漏不是功能 Bug,它不会被功能测试覆盖,只能靠这种方式主动抓。
这件事真正改变的是工程师的心态:以前是等用户告警、靠堆快照排查;现在是每次提交都在问”这个页面会漏内存吗”,CI 直接给你答案。
评论区
登录后可评论。