Scale Your Marketing

8 Tips for Faster Puppeteer Screenshots

Jon Yongfook · February 2021

Contents

Bannerbear makes use of Puppeteer for image rendering. If you're working with Puppeteer and trying to squeeze more performance out of it, here are some things you can try.

Quick and shameless plug: if you're using Puppeteer to do custom image generation in your app, try using Bannerbear instead! Your design team can set up templates, you get an API for the template, and we take care of the performance / scalability stuff!

Who is this article for?

This advice is mostly applicable if you are performing some kind of internal screenshot process in your app, using web pages under your control.

This advice is less applicable if you are doing general screenshots of public websites, but some of the tips will still be helpful!

When optimizing Puppeteer, remember that there are only so many ways to speed up the startup/shutdown performance of Puppeteer itself. Most likely, the biggest speed gains will come from getting your target pages to render faster.

Some of this can be achieved via Puppeteer settings, but some will come from optimizing the actual pages you are rendering.

Run Puppeteer with Minimal Settings

You can run Puppeteer with minimal settings. This removes all the extras that are probably unnecessary for your screenshot script - although you may need to tweak these settings based on your specific needs.

This will help Puppeteer to consume less memory and start faster.

You can find a helpful list of these Chrome switches with their explanations here.

const puppeteer = require('puppeteer');

const minimal_args = [
  '--autoplay-policy=user-gesture-required',
  '--disable-background-networking',
  '--disable-background-timer-throttling',
  '--disable-backgrounding-occluded-windows',
  '--disable-breakpad',
  '--disable-client-side-phishing-detection',
  '--disable-component-update',
  '--disable-default-apps',
  '--disable-dev-shm-usage',
  '--disable-domain-reliability',
  '--disable-extensions',
  '--disable-features=AudioServiceOutOfProcess',
  '--disable-hang-monitor',
  '--disable-ipc-flooding-protection',
  '--disable-notifications',
  '--disable-offer-store-unmasked-wallet-cards',
  '--disable-popup-blocking',
  '--disable-print-preview',
  '--disable-prompt-on-repost',
  '--disable-renderer-backgrounding',
  '--disable-setuid-sandbox',
  '--disable-speech-api',
  '--disable-sync',
  '--hide-scrollbars',
  '--ignore-gpu-blacklist',
  '--metrics-recording-only',
  '--mute-audio',
  '--no-default-browser-check',
  '--no-first-run',
  '--no-pings',
  '--no-sandbox',
  '--no-zygote',
  '--password-store=basic',
  '--use-gl=swiftshader',
  '--use-mock-keychain',
];

const browser = await puppeteer.launch({
  headless: true,
  args: minimal_args
})

Cache Resources with userDataDir

By default, when starting a new browser session Puppeteer does not reuse CSS / JS / Images assets that were downloaded during a previous session. This means that everything gets loaded from scratch with a new browser session.

If you are hitting webpages with common CSS / JS / Image assets you may want to tell Puppeteer to use a cache for these assets so that they do not get unnecessarily downloaded in subsequent sessions (kind of how your regular browser works).

You can achieve this by setting a path when launching Puppeteer:

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
  userDataDir: './my/path'
})

Disable Unnecessary Resources

Another useful startup setting is blocking resources that you know you will not need.

For example if you are using Puppeteer to just grab HTML, you can tell Puppeteer to not even bother downloading CSS / JS / Images. However, we are using Puppeteer for screenshots so we want to keep those.

What we may want to disable however, is unnecessary 3rd party scripts. AKA ads / tracking etc. All of these add to page load time and are most likely undesirable for screenshot scripts (unless you actually want to screenshot the ads).

By using a list of blocked domains you can tell Puppeteer to not waste time downloading resources from those domains. You can find a list of known tracking domains here - unfortunately it may not be practical to include the whole list, since it is 9.8mb!

const page = await browser.newPage();

const blocked_domains = [
  'googlesyndication.com',
  'adservice.google.com',
];

await page.setRequestInterception(true);
page.on('request', request => {
  const url = request.url()
  if (blocked_domains.some(domain => url.includes(domain))) {
    request.abort();
  } else {
    request.continue();
  }
});

await page.goto('https://www.nytimes.com');

Screenshot ASAP

You can control when Puppeteer takes the screenshot and you should take the screenshot as soon as your page is ready for it.

As a default setting you might be waiting for network activity to stop before taking the screenshot:

await page.goto(my_url, {waitUntil: 'networkidle2'});

But this might not be the fastest event to trigger your screenshot on. Theoretically the page could have been ready for a screenshot 1 or 2 seconds before this event, with no difference in terms of the visual presentation - perhaps some scripts were finishing loading in the background, for example.

If the page you are taking screenshots of is under your control, you can tell Puppeteer exactly when to take the screenshot by using the waitForSelector() method:

await page.waitForSelector('.take_screenshot_now');

Then in your page's javascript code you can trigger this selector exactly at the moment you know the page is visually-ready for the screenshot, and not at a later point when network activity idles.

Reduce the HTTP Payload Size

This one only applies when the pages you are taking screenshots of are under your control.

Your page will render faster the smaller you are able to shrink the HTTP payload. Obvious, but sometimes you can get all caught up thinking about how to optimize Puppeteer and forget about the basics! All the usual rules here apply:

  • Gzip / Minify your assets
  • Optimize images
  • Remove unnecessary CSS / JS

Return Screenshots as JPG

This will depend on the type of screenshots you are doing but if you are doing full page screenshots, the size of a png can quickly get very unwieldy.

Here is a comparison of the raw file size produced by a Puppeteer screenshot of unsplash.com

The JPG version is (obviously) much smaller. On the Puppeteer side of things there is a negligible difference in speed to generate a JPG vs a PNG - but where this will slow things down is in network transfer.

If you're transferring large files from your Puppeteer instance to, say an S3 bucket or other cloud storage, the latency involved in transferring a > 5MB file compared to a 400KB file will add up and slow down your overall process.

Path vs Buffer

Puppeteer gives the option of creating a screenshot either on the file system or as a Buffer.

In general, using a Buffer will be faster than making Puppeteer write to the file system - after which you'll probably be reading the file and doing some other operation with it, creating further slowdown.

Remove All Unnecessary I/O

Most of the time when performing screenshot operations you will be doing some other things too. For example, moving files from one place to another, transforming formats etc.

To illustrate, for example on Bannerbear in a previous version of the screenshot workflow we would:

  1. Take the screenshot with Puppeteer
  2. Pass the binary to Rails (network file transfer)
  3. Rails would upload to S3 (network file transfer)
  4. Operation complete

This was optimized to:

  1. Take the screenshot with Puppeteer
  2. Upload to S3 in the same Node script (network file transfer)
  3. Return the S3 url to Rails

Conclusion

I hope some of these tips were helpful! One thing that has definitely helped us make progress with our speed improvements is to look at the whole lifecycle of the Puppeteer screenshot operation, rather than just singling out the screenshot execution.

It's quite likely that there will be some low-hanging fruit in your lifecycle that you can tweak to get a < 1000ms improvement as a quick win.

Quick and shameless plug: if you're using Puppeteer to do custom image generation in your app, try using Bannerbear instead! Your design team can set up templates, you get an API for the template, and we take care of the performance / scalability stuff!

Tags for this article

Author
Jon Yongfook@yongfook

Jon is the founder of Bannerbear. He has worked as a designer and programmer for 20 years and is fascinated by the role of technology in design automation.

Follow the Journey

Hello I'm Jon, the founder of Bannerbear — every 2 weeks I send a newsletter with updates from the Product, Marketing and Business sides of my startup, subscribe below to receive it!