1. Playwright自动化脚本内存溢出问题深度解析
最近在团队内部推进Playwright自动化测试时,遇到了一个棘手的内存溢出问题。当我们的测试套件运行到第37个用例时,Node.js进程就会崩溃,错误日志显示"JavaScript heap out of memory"。这个问题困扰了我们两周时间,经过系统排查和多种方案验证,最终找到了稳定可靠的解决方案。下面就把这个完整的问题定位和解决过程分享给大家。
Playwright作为现代浏览器自动化工具,其基于Chromium的架构在带来强大功能的同时,也带来了更高的内存消耗。特别是在长时间运行的测试套件中,内存管理不当很容易导致溢出。我们的项目使用Playwright 1.42版本,测试框架是Jest,运行在GitHub Actions的Windows虚拟机上。最初发现问题时,平均每个测试用例会增加约50MB内存占用,运行30多个用例后内存就突破了Node.js默认的1.4GB堆限制。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内存溢出问题的根因分析
2.1 浏览器实例未正确关闭
通过Chrome DevTools的内存快照对比分析,发现最大的内存泄漏源是未关闭的BrowserContext。在最初的测试代码中,我们虽然调用了browser.close(),但没有显式处理context。实际上每个测试文件都创建了自己的context,但在测试失败时没有完善的清理机制。
javascript复制// 问题代码示例
beforeEach(async () => {
browser = await playwright.chromium.launch();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await browser.close(); // 仅关闭browser不够
});
2.2 页面资源未释放
另一个重要发现是测试中加载的页面资源(特别是大型图片和视频)会持续占用内存。即使导航到新页面,之前的资源也不会立即释放。这在数据驱动测试中尤为明显,因为我们会重复使用相同页面加载不同数据集。
2.3 Jest测试隔离问题
Jest默认会缓存模块并保持测试环境运行,这与Playwright的浏览器实例生命周期产生了冲突。我们发现即使正确关闭了browser,内存中的某些对象仍然被Jest的测试沙箱保留。
3. 系统化解决方案
3.1 完整的资源清理方案
修正后的资源管理方案需要包含三个层面的清理:
javascript复制afterEach(async () => {
// 1. 关闭所有页面
for (const page of context.pages()) {
await page.close();
}
// 2. 清除所有cookies和storage
await context.clearCookies();
await context.clearPermissions();
// 3. 关闭context和browser
await context.close();
await browser.close();
});
3.2 内存使用优化配置
在playwright.config.js中添加以下内存优化配置:
javascript复制export default {
workers: 3, // 限制并行worker数量
use: {
headless: true,
viewport: { width: 1280, height: 720 }, // 固定视窗大小
ignoreHTTPSErrors: true,
bypassCSP: true,
// 关键优化参数
javaScriptEnabled: false, // 对不需要JS的页面禁用
offline: false,
httpCredentials: null,
serviceWorkers: 'block',
}
}
3.3 Node.js内存参数调整
对于大型测试套件,需要调整Node.js内存限制。我们通过cross-env在package.json中设置:
json复制"scripts": {
"test": "cross-env NODE_OPTIONS=--max-old-space-size=4096 jest"
}
4. 进阶优化技巧
4.1 使用BrowserContext重用策略
对于登录状态一致的测试组,可以复用BrowserContext而不是为每个测试创建新实例:
javascript复制let sharedContext;
beforeAll(async () => {
sharedContext = await browser.newContext();
});
afterAll(async () => {
await sharedContext.close();
});
test('test1', async () => {
const page = await sharedContext.newPage();
// ...
});
4.2 内存监控方案
在测试中添加实时内存监控:
javascript复制const { performance, memoryUsage } = require('node:perf_hooks');
beforeEach(() => {
global.gc(); // 需要启动时添加--expose-gc参数
console.log(`内存使用: ${JSON.stringify(memoryUsage())}`);
});
4.3 截图和视频优化
禁用不必要的记录功能可以显著减少内存使用:
javascript复制// playwright.config.js
video: 'off',
screenshot: 'only-on-failure',
5. 典型问题排查指南
5.1 内存持续增长的常见原因
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 每个测试后内存下降不明显 | Context未关闭 | 确保afterEach中调用context.close() |
| 堆内存正常但RSS持续增长 | 页面资源泄漏 | 禁用不必要的资源加载(如图片) |
| 随机性内存溢出 | 并行度过高 | 减少workers数量 |
5.2 GitHub Actions上的特殊配置
在CI环境中需要额外配置:
yaml复制jobs:
test:
runs-on: windows-latest
env:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- uses: actions/setup-node@v3
with:
node-version: 18
6. 实战验证与效果对比
优化前后我们在相同环境运行完整的142个测试用例,内存使用对比如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 峰值内存使用 | 3.2GB | 1.1GB |
| 测试执行时间 | 8分12秒 | 6分45秒 |
| 稳定性 | 37%失败率 | 100%通过 |
这套方案已经在我们的持续集成流水线稳定运行3个月,没有再出现内存溢出问题。关键点在于:完整的资源生命周期管理、合理的并行度控制,以及必要的Node.js内存参数调整。
