资讯专栏INFORMATION COLUMN

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

KnewOne / 2902人阅读

摘要:所以需要理解运行的原理,才能方便优化。如果启动时能绑定到某个核上也能提升速度单核上进行进程切换耗费的时间更少。优化执行流程接下来我们再多带带优化对应的页面。参考文章性能优化与执行速度提升利用优化

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

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

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

优化Chromium启动项

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

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

    如果将Dom解析和渲染放到同一进程,肯定能提升时间(进程上下文切换的时间)。对应的配置是 ​single-process​

    部分功能disable掉,比如GPU、Sandbox、插件等,减少内存的使用和相关计算。

    如果启动Chromium时能绑定到某个CPU核上也能提升速度(单核上进行进程切换耗费的时间更少)。可惜没有找到对应的配置,官方文档写的是Chromium启动时会自动绑定CPU大核(ARM架构的CPU通常有大小核之分),依此推测Chromium启动时是会绑核的。(此处我并未验证)

最后配置如下:

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执行流程

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

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

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

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

代码如下:

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;itrue,
                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);
    })();        
}
利用cluster优化Puppeteer

通常情况下我们会使用 ​.map()​ 搭配 ​Promise.all()​ 的方式并行处理异步,但是在使用​Puppeteer​批量截图时发现​Promise.all​会打开多个浏览器,导致机器性能急剧下降。

​Promise.all()​ 并行处理

利用 ​Reduce​ 是多个​Promise​顺序执行

await tasks.reduce((sequence, url, idx) => {
  return sequence.then(() => {
    // doAnalyze 是个异步函数
    return doAnalyze(url, idx);
  });
}, Promise.resolve())

场景:有40个URL,需要获取每个博客的首页截图

如果是​Promise.all()​,程序启动会同时打开20+的chromium浏览器,导致机器卡死。

使用​reduce​缓解了压力,但没充分利用多核性能

参入​Cluster​

// cluster_index.js 入口文件
const cluster = require("cluster");

(async () => {
  let run;
  if (cluster.isMaster) {
    run = require("./cluster_master");
  } else {
    run = require("./cluster_worker");
  }
  try {
    await run();
  } catch (e) {
    // 追踪函数的调用轨迹
    console.trace(e);
  }
})();
// cluster_master.js master进程分配任务

const cluster = require("cluster");
const numCPUs = require("os").cpus().length;

// 处理的任务列表
let arr = [
  "https://github.com/guoguoya",
  "http://www.52cik.com",
  "http://zhalice.com",
  "https://www.yzqroom.cn",
  "http://zxh.name",
  "https://fogdong.github.io/",
  "http://github.com/elsieyin",
  "https://summer.tlb058.com",
  "https://skymon4.cn",
  "http://www.jiweiqing.cn",
  "http://effect.im",
  "http://dingkewz.com",
  "http://xcdh.me",
  "http://d2g.io",
  "http://codingdemon.com",
  "http://blog.leanote.com/dujuncheng",
  "http://niexiaotao.com",
  "http://zhengchengwen.com",
  "http://blog.tophefei.com",
  "https://zh-rocco.github.io",
  "http://wangyn.net",
  "http://dscdtc.ml",
  "http://jweboy.github.io",
  "http://www.wenghaoping.com",
  "http://zhoujingchao.github.io",
  "http://kyriejoshua.github.io/jo.github.io/",
  "http://www.withyoufriends.com",
  "http://if2er.com",
  "https://github.com/zhou-yg",
  "http://github/suoutsky",
  "http://richardsleet.github.io",
  "http://www.89io.com",
  "https://guoshencheng.com",
  "http://www.landluck.com.cn",
  "http://www.89io.com",
  "http://myoungxue.top",
  "https://github.com/Wangszzju",
  "http://www.hacke2.cn",
  "https://github.com/enochjs",
  "https://i.jakeyu.top",
  "http://muyunyun.cn",
];

module.exports = async () => {
  // 每个 CPU 分配 N 个任务
  const n = Math.floor(arr.length / numCPUs);
  // 未分配的余数
  const remainder = arr.length % numCPUs;

  for (let i = 1; i <= numCPUs; i += 1) {
    const tasks = arr.splice(0, n + (i > remainder ");"exit", (worker) => {
    console.log(`worker #${worker.id} PID:${worker.process.pid} died`);
  });
  cluster.on("error", (err) => {
    console.log(`worker #${worker.id} PID ERROR: `, err);
  });
};
// cluster_worker.js worker进程 完成任务

const cluster = require("cluster");
const puppeteer = require("puppeteer");

// 禁止直接启动
if (cluster.isMaster) {
  console.log("----", cluster.worker.id)
  process.exit(0);
}

module.exports = async () => {
  const env = process.env.tasks;
  let tasks = [];
  if (/^[.*]$/.test(env)) {
    tasks = JSON.parse(env);
  }
  if (tasks.length === 0) {
    console.log("");, tasks)
    // 非法启动, 释放进程资源
    process.exit(0);
  }
  console.log(`worker #${cluster.worker.id} PID:${process.pid} Start`);
  await tasks.reduce((sequence, url, idx) => {
    return sequence.then(() => {
      return doAnalyze(url, idx);
    });
  }, Promise.resolve())

  console.log(cluster.worker.id + " 顺利完成");
  process.exit(0);
};

async function doAnalyze(url, i) {
  try {
    const browser = await (puppeteer.launch({
      // 若是手动下载的chromium需要指定chromium地址, 默认引用地址为 /项目目录/node_modules/puppeteer/.local-chromium/
      // executablePath: "/Users/huqiyang/Documents/project/z/chromium/Chromium.app/Contents/MacOS/Chromium",
      //设置超时时间
      timeout: 30000,
      //如果是访问https页面 此属性会忽略https错误
      ignoreHTTPSErrors: true,
      // 打开开发者工具, 当此值为true时, headless总为false
      devtools: false,
      // 关闭headless模式, 会打开浏览器
      headless: false
    }));
    const page = await browser.newPage();
    await page.setViewport({width: 1920, height: 1080});
    await page.goto(url);
    await page.waitFor(4000);
    console.log(cluster.worker.id, url, i, "截图中...");
    await page.screenshot({
      path: `./img_cluster/${cluster.worker.id}-${i}.png`,
      // path: "3.png",
      type: "png",
      // quality: 100, 只对jpg有效
      // fullPage: true,
      // 指定区域截图,clip和fullPage两者只能设置一个
      // clip: {
      //   x: 0,
      //   y: 0,
      //   width: 1920,
      //   height: 600
      // }
    });
    browser.close();
  } catch (error) {
    console.log(cluster.worker.id, url, i)
    console.log(error)
  }
};
多个page轮询与多个browser轮询

为了性能,现有解决方案是初始化若干个browser,请求打过来时,直接在browserList中取一个browser实例使用。 作为对比,可以参考初始化一个browser,预先打开若干个page,请求打过来时,直接在pageList中取一个page实例使用。

参考文章:

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7302.html

相关文章

  • 前端每周清单第 29 期:Web 现状分析优化策略、Vue 单元测试、Headless Chrom

    摘要:前端每周清单第期现状分析与优化策略单元测试爬虫作者王下邀月熊编辑徐川前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清单第 29 期:Web 现状分析与优化策略...

    HackerShell 评论0 收藏0
  • 从零开始开发一个Node交互式命令行应用

    摘要:导言对于大多数前端开发者而言,谈到命令行工具,大家肯定都用过。但是谈到开发命令行工具,估计就没几人有了解了。如何优化这个图片爬虫工具目前还有点啊,我们的目标是要开发一个交互式的命令行应用,肯定不能止于此。 导言:对于大多数前端开发者而言,谈到命令行工具,大家肯定都用过。但是谈到开发命令行工具,估计就没几人有了解了。本文旨在用最短的时间内,帮您开发一个实用(斜眼笑)的图片爬虫命令行应用。...

    Harriet666 评论0 收藏0
  • 前端每周清单第 48 期:Slack Webpack 构建优化,CSS 命名规范用户追踪,Vue.

    摘要:发布是由团队开源的,操作接口库,已成为事实上的浏览器操作标准。本周正式发布,为我们带来了,,支持自定义头部与脚部,支持增强,兼容原生协议等特性变化。新特性介绍日前发布了大版本更新,引入了一系列的新特性与提升,本文即是对这些变化进行深入解读。 showImg(https://segmentfault.com/img/remote/1460000012940044); 前端每周清单专注前端...

    sean 评论0 收藏0
  • 前端每周清单年度总结盘点

    摘要:前端每周清单年度总结与盘点在过去的八个月中,我几乎只做了两件事,工作与整理前端每周清单。本文末尾我会附上清单线索来源与目前共期清单的地址,感谢每一位阅读鼓励过的朋友,希望你们能够继续支持未来的每周清单。 showImg(https://segmentfault.com/img/remote/1460000010890043); 前端每周清单年度总结与盘点 在过去的八个月中,我几乎只做了...

    jackwang 评论0 收藏0
  • 前端核心工具:yarn、npm、cnpm三者如何优雅的在一起使用 ?

    摘要:由于文件中版本号的特点,下面三个版本号在安装的时候代表不同的含义。安装版本统一为了防止拉取到不同的版本,有一个锁定文件记录了被确切安装上的模块的版本号。 showImg(https://segmentfault.com/img/bVbs8Rg?w=1920&h=1080); 一位用不好包管理器的前端,是一个入门级前端,一个用不好webpack的前端,是一个初级前端 三个包管理器是可以一...

    plus2047 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<