How to speed up puppeteer?

A script should not lose to a human who has just manual controls like F5 button.

It happens because sometimes the rules that puppeteer follows are much stricter than what we consider as a "fully loaded webpage". Even if you as a human can decide whether your desired element is in the DOM already (because you see the element is there) or it is not there (because you don't see it). E.g.: you will see that your button is not there even if the background image is still loading in the background, or the webfonts are still not loaded and you have the fallback fonts, but puppeteer waits for specific events in the background to get the permission either to go to the catch block (timeout) or to grab the desired element (waitForSelector succeeds). It can really depends on the site you are visiting, but you are able to speed up the process of recognition of your desired element.

I give some examples and ideas how you can achieve this.


Ways to speed up recognition of the desired element

1.) If you don't need every network connections for your task you could speed up page loading by replacing waitUntil: 'networkidle2' to waitUntil: 'domcontentloaded' as this event happens usually earlier and will be fired when #ourButton will be already present in the DOM.

The possible options of page.goto/page.reload:

  • load - consider navigation to be finished when the load event is fired.
  • domcontentloaded - consider navigation to be finished when the DOMContentLoaded event is fired.
  • networkidle0 - consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.
  • networkidle2 - consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.

You are winning over the script because of networkidle2 is too strict. You may need this option (e.g. you are visiting a single-page application or later you will need data from the 3rd party network connection e.g. cookies) but in case it is not mandatory you will experience better performance with domcontentloaded.

2.) Instead of constantly navigating to the same url you could use page.reload method in a loop, e.g.:

await page.goto(url, { waitUntil: 'domcontentloaded' })
let selectorExists = await page.$('#ourButton')

while (selectorExists === null) {
  await page.reload({ waitUntil: 'domcontentloaded' })
  console.log('reload')
  selectorExists = await page.$('#ourButton')
}
await page.click('#ourButton')
// code goes on...

Its main benefit is that you are able to shorten and simplify your pageRefresher function. But I experienced also better performance (however I did no benchmarking but I felt it much faster than re-opening a page).

3.) If you don't need every resource type for your task you could also speed up page loading by disabling images or css with the following script:

await page.setRequestInterception(true)
page.on('request', (request) => {
  if (request.resourceType() === 'image') request.abort()
  else request.continue()
})

[source]

List of resourceType-s.


Try just not awaiting the goto:

page.goto(url) // no await because it doesn't have to resolve fully
await page.waitForSelector('#ourButton') // await this because we need it to be there

Some people like Promise.race for this but this way is simpler


Using the page.$eval() method you can do it as short as this:

await page.goto(url);
page.$eval('button-selector', button => button.click());

By doing so, you combine the actions of searching the desired button and clicking on it into a single line. You will have to await on the page.goto() instruction as you will need the page to be fully loaded before using page.$eval()

1st arg is the selector you need to use to get your HTMLElement in your case a button.

This HTMLElement will be retrieved by running document.querySelector() with the provided selector whitin page context before passing it as argument for the function defined in the following argument.

2nd arg is the function to be executed inside page context wich take the HTMLElement that match the previous selector as argument

The page.$eval() instruction will throw an error if no element is found that match the provided selector.

You can address this in two ways:

  • prevent the error from triggering at all by testing if your HTMLElement exists before using the page.$eval() method.
await page.goto(url);
if (await page.$('button-selector') != null) // await because page.$() returns a promise
    page.$eval('button-selector', button => button.click());

an alternative using only page.$() would be :

await page.goto(url);
if ((button = await page.$('button-selector')) != null)    
        button.click();

Be sure to encapsulate the left part of the condition inside ( ) otherwise button value will be true or false.

  • catch the error when it occurs:
    you could use this to determine when to reload the page
await page.goto(url);
page.$eval('button-selector', button => button.click())
    .catch((err) => {
        // log the error here or do some other stuff
    });

After some tests it looks like we can't use a try ... catch block to capture the error on the page.$eval() method so the above example is the only way to do so.

For more informations you could check the puppeteer API page for page.$eval()

And if you want to go further in accelerating puppeteer I've found those tutorials really helpfull:

  • How to speed up Puppeteer scraping with parallelization
  • Optimizing and Deploying Puppeteer Web Scraper
  • 8 Tips for Faster Puppeteer Screenshots

Edit:
From your code i see you use the page.setViewPort() method to set a viewport size of 1920x1080 px on your page. While it may provides a better viewing when showing the navigator it'll have some impact on performance. It is best practice to use minimal settings when running in headless mode.