Puppeteer性能优化与执行速度提升

之前一篇关于无头浏览器的文章可以看这里 无头浏览器性能对比与Puppeteer的优化文档 ,这篇文章提了部分优化思路和使用场景,就是没具体的代码。今天整理了使用技巧与代码分享给大家(为什么我要说大家)。下文中的代码都用在了生产环境中,单台机器能达到100QPS(没优化之前是20QPS)。平均请求响应时间在400ms左右,测试程序为执行一段Echarts代码生成柱状图然后截图返回,400ms比请求某些静态资源的时间都短。

Puppeteer自身不会消耗太多资源,耗费资源的大户是Chromium Headless。所以需要理解Chromium运行的原理,才能方便优化。

Chromium消耗最多的资源是CPU,一是渲染需要大量计算,二是Dom的解析与渲染在不同的进程,进程间切换会给CPU造成压力(进程多了之后特别明显)。其次消耗最多的是内存,Chromium是以多进程的方式运行,一个页面会生成一个进程,一个进程占用30M左右的内存,大致估算1000个请求占用30G内存,在并发高的时候内存瓶颈最先显现。

优化最终会落在内存和CPU上(所有软件的优化最终都要落到这里),通常来说因为并发造成的瓶颈需要优化内存,计算速度慢的问题要优化CPU。使用Puppeteer的用户多半会更关心计算速度,所以下面我们谈谈如何优化Puppeteer的计算速度。

优化Chromium启动项

通过查看Chromium启动时都有哪些参数可以配置,能找到大部分线索,因为Chromium这种顶级的开源产品,文档与接口都是非常清晰的,肯定可以找到相关配置项来定制启动方式。Chromium 启动参数列表

我们需要找到下面几种配置来提升速度:

  1. 如果将Dom解析和渲染放到同一进程,肯定能提升时间(进程上下文切换的时间)。对应的配置是 single-process
  2. 部分功能disable掉,比如GPU、Sandbox、插件等,减少内存的使用和相关计算。
  3. 如果启动Chromium时能绑定到某个CPU核上也能提升速度(单核上进行进程切换耗费的时间更少)。可惜没有找到对应的配置,官方文档写的是Chromium启动时会自动绑定CPU大核(ARM架构的CPU通常有大小核之分),依此推测Chromium启动时是会绑核的。(此处我并未验证)

最后配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const browser = await puppeteer.launch(
{
headless:true,
args: [
‘–disable-gpu’,
‘–disable-dev-shm-usage’,
‘–disable-setuid-sandbox’,
‘–no-first-run’,
‘–no-sandbox’,
‘–no-zygote’,
‘–single-process’
]
});

Chromium 启动参数列表 文档中的配置项都可以尝试看看,我没有对所有选项做测试,但可以肯定存在某些选项能提升Chromium速度。

Chromium的启动项优化后能节省200ms左右的请求时间,收益非常可观。

优化Chromium执行流程

接下来我们再单独优化Chromium对应的页面。我之前的文章中提过,如果每次请求都启动Chromium,再打开tab页,请求结束后再关闭tab页与浏览器。流程大致如下:

请求到达->启动Chromium->打开tab页->运行代码->关闭tab页->关闭Chromium->返回数据

真正运行代码的只是tab页面,理论上启动一个Chromium程序能运行成千上万的tab页,可不可以复用Chromium每次只打开一个tab页然后关闭呢?当然是可以的,Puppeteer提供了puppeteer.connect() 方法,可以连接到当前打开的浏览器。流程如下:

请求到达->连接Chromium->打开tab页->运行代码->关闭tab页->返回数据

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const MAX_WSE = 4;  //启动几个浏览器 
let WSE_LIST = []; //存储browserWSEndpoint列表
init();
app.get('/', function (req, res) {
let tmp = Math.floor(Math.random()* MAX_WSE);
(async () => {
let browserWSEndpoint = WSE_LIST[tmp];
const browser = await puppeteer.connect({browserWSEndpoint});
const page = await browser.newPage();
await page.goto('file://code/screen/index.html');
await page.setViewport({
width: 600,
height: 400
});
await page.screenshot({path: 'example.png'});
await page.close();
res.send('Hello World!');
})();
});

function init(){
(async () => {
for(var i=0;i<MAX_WSE;i++){
const browser = await puppeteer.launch({headless:true,
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-sandbox',
'--no-zygote',
'--single-process'
]});
browserWSEndpoint = await browser.wsEndpoint();
WSE_LIST[i] = browserWSEndpoint;
}
console.log(WSE_LIST);
})();
}

程序启动时(使用Express提供Web接口),初始化一定数量的无头浏览器,并保存 WSEndpoint列表,当收到请求时,通过随机数做简单的负载均衡(利用多核特性)。

使用tab方式渲染后请求速度提升了200ms左右,一个tab进程使用内存降到20M以内,带来的收益也非常可观。不过这里要注意,官方并不建议这样做,因为一个tab页阻塞或者内存泄露会导致整个浏览器阻塞并Crash。万全的解决办法是定期重启程序,可参考php-fpm的做法,当请求1000次或者内存超过限制后重启对应的进程。

合理选择无头浏览器与版本

Puppeteer当前支持Chromium和Firefox,我测试了Firefox,结论是Firefox headless现在还不够成熟,相关的资料也比较少,不建议在生产环境使用。Chromium的版本非常多,百花齐放,这里我建议使用chromium-headless 这是一个独立的版本,能为你的程序带来200ms左右的性能提升(这个最爽,啥都不用做)。安装与使用方式(CentOS):

1
2
$ yum install chromium-headless
$ /usr/lib64/chromium-browser/headless_shell (调用路径)

其他系统可以自行查找该版本,找不到可以选择自己编译。

最后

Puppeteer(其实是Chromium)的优化空间还非常大,需要不停的去实验和测试。希望我提供的思路能帮助正在尝试优化Puppeteer的人。