Puppeteer性能優化與執行速度提高

Puppeteer自身不會消耗太多資源,耗費資源的大戶是Chromium Headless。因此須要理解Chromium運行的原理,才能方便優化。html

Chromium消耗最多的資源是CPU,一是渲染須要大量計算,二是Dom的解析與渲染在不一樣的進程,進程間切換會給CPU形成壓力(進程多了以後特別明顯)。其次消耗最多的是內存,Chromium是以多進程的方式運行,一個頁面會生成一個進程,一個進程佔用30M左右的內存,大體估算1000個請求佔用30G內存,在併發高的時候內存瓶頸最早顯現。node

優化最終會落在內存和CPU上(全部軟件的優化最終都要落到這裏),一般來講由於併發形成的瓶頸須要優化內存,計算速度慢的問題要優化CPU。使用Puppeteer的用戶多半會更關心計算速度,因此下面咱們談談如何優化Puppeteer的計算速度。git

優化Chromium啓動項

經過查看Chromium啓動時都有哪些參數能夠配置,能找到大部分線索,由於Chromium這種頂級的開源產品,文檔與接口都是很是清晰的,確定能夠找到相關配置項來定製啓動方式。Chromium 啓動參數列表github

咱們須要找到下面幾種配置來提高速度:web

  1. 若是將Dom解析和渲染放到同一進程,確定能提高時間(進程上下文切換的時間)。對應的配置是 ​single-process​
  2. 部分功能disable掉,好比GPU、Sandbox、插件等,減小內存的使用和相關計算。
  3. 若是啓動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頁與瀏覽器。流程大體以下:bash

請求到達->啓動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;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);
    })();        
}
複製代碼

利用cluster優化Puppeteer

一般狀況下咱們會使用 ​.map()​ 搭配 ​Promise.all()​ 的方式並行處理異步,可是在使用​Puppeteer​批量截圖時發現​Promise.all​會打開多個瀏覽器,致使機器性能急劇降低。

​Promise.all()​ 並行處理

image

利用 ​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 ? 0 : 1));
    // 將任務編號傳遞到 Cluster 內啓動
    cluster.fork({ tasks: JSON.stringify(tasks) });
  }
  cluster.on('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

相關文章
相關標籤/搜索