Compare Website Screenshots With Node.JS And Puppeteer

In this tutorial, we will build a simple application that takes multiple screenshots of a webpage and detect the differences. But first, why would you want to do that?

You might want to test the webpage after you deploy new changes and compare the two versions. Or, perhaps you want to keep track of a page and want to be notified about changes.

Take a webpage screenshot

Let's start implementing the application!

Screenshot with Puppeteer

Puppeteer is a library that implements the Chrome DevTools Protocol to automate everything you can do in the browser. Puppeteer is not the only library that allows you to take screenshots of a webpage. You can also use Playwright, which supports most modern browsers. Puppeteer is oriented more towards headless Chrome and Chromium. Although it also supports Mozilla Firefox.

Open the terminal and install Puppeteer as follows:

npm i puppeteer

Now create a new file, take.js, and write the following piece of code:

// take.js

'use strict';
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
    try {
      const page = await browser.newPage();
      await page.goto('https://catalins.tech/');

      await page.screenshot({ path: 'catalins.tech.png' });
    } catch (e) {
      console.log(e)
    } finally {
      await browser.close();
    }
})();

The above code opens the website catalins.tech and takes a screenshot of the page. Run the script with the following command:

node take.js

You should get this screenshot:

A screenshot of catalins.tech website

Before going further, I want to highlight that the resulting screenshot might not look good on the Apple Retina displays. To fix it, set deviceScaleFactor to 2 for the viewport:

// take.js

'use strict';
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://catalins.tech/');

    await page.setViewport({width: 800, height: 600, deviceScaleFactor: 2});

    await page.screenshot({ path: '[email protected]' });
   } catch (e) {
     console.log(e)
   } finally {
     await browser.close();
   }
})();

Now, if you rerun the script, you should get the following picture:

A screenshot with a higher device scale ratio for the catalins.tech website

If you compare the previous screenshot with this one, you can see that the above one has a higher quality.

Or with screenshot API

In some cases, Puppeteer might not be the best tool for the job. If you need to take many screenshots of different sites, you will need to scale, monitor, and support Puppeteer servers. It can become costly very quickly.

In that case, you can use any screenshot API to take screenshots for you and offload the burden of dealing with Puppeteer infrastructure.

I am the author of ScreenshotOne (a screenshot API). I fixed and handled many corner cases when dealing with Puppeteer and simplified it as a simple API.

You can use the SDK or send regular HTTP requests. I will show an example of taking webpage screenshots with the ScreenshotOne SDK. Let's first install it:

npm install screenshotone-api-sdk --save

Open the take.js file and write the following code:

// take.js 

const fs = require('fs');
const screenshotone = require('screenshotone-api-sdk');
(async () => {
  // create API client 
  // sign up at https://screenshotone.com/ to get your access and secret keys
  const client = new screenshotone.Client("<your access key>", "<your secret key>");

  // set up options
  const options = screenshotone.TakeOptions
    .url("https://catalins.tech")
    .viewportWidth(800)
    .viewportHeight(600)
    .deviceScaleFactor(2);

  const imageBlob = await client.take(options);
  const buffer = Buffer.from(await imageBlob.arrayBuffer());
  fs.writeFileSync("[email protected]", buffer)
  // the screenshot is stored in the [email protected] file
})();

The result, as you might expect, is the same:

A screenshot of the catalins.tech taken with ScreenshotOne.com

Compare screenshots with Pixelmatch

Once we have the screenshots, we can start comparing them. There is a wide variety of image comparison methods due to nuances in the image formats and the level of noise you can tolerate.

I picked up the most straightforward library to compare images — pixelmatch.

But before comparing, we'll modify the webpage by removing the navigation links. Then we'll take another screenshot.

// take.js

'use strict';
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://catalins.tech/');

    // remove the header links
    await page.addStyleTag({content: '.blog-sub-header { visibility: hidden !important; }'});

    await page.screenshot({ path: 'catalins.tech_without_header_links.png' });
    } catch (e) {
      console.log(e)
    } finally {
      await browser.close();
    }
})();

Running the code results in the following image:

A screenshot of the catalins.tech website without the header links

As you can see, the navigation links are gone. The code should detect the difference when we compare the above image with the previous one (with navigation links).

Now, let's install the pixelmatch library and detect the differences:

npm install pixelmatch

Create a new file, diff.js, and write the following code:

// diff.js

'use strict';

const fs = require('fs');
const PNG = require('pngjs').PNG;
const pixelmatch = require('pixelmatch');

const img1 = PNG.sync.read(fs.readFileSync('catalins.tech.png'));
const img2 = PNG.sync.read(fs.readFileSync('catalins.tech_without_header_links.png'));
const { width, height } = img1;
const diff = new PNG({ width, height });

const result = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: 0.1 });

console.log(`Different pixels: ${result}`);

fs.writeFileSync('diff.png', PNG.sync.write(diff));

Then run the script:

node diff.js

The output is:

Different pixels: 1875

And the resulting image with the difference:

The difference of the screenshots

The threshold parameter allows you to tune the sensitivity of the comparison: the less the threshold, the more sensitive the comparison, and vice versa.

To act on the detected difference in the screenshots, you need to use the result with the number of pixels. If there is a difference, you can send an email, push notification or apply any logic you wish.

You can combine all the code in one file as follows:

// take_and_diff.js

'use strict';

const puppeteer = require('puppeteer');
const fs = require('fs');
const PNG = require('pngjs').PNG;
const pixelmatch = require('pixelmatch');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://catalins.tech/');

    await page.screenshot({ path: 'catalins.tech.png' });

    // remove the header links, 
    // but in the real case, you would use an old or a fresh screenshot of the same site to compare
    await page.addStyleTag({ content: '.blog-sub-header { visibility: hidden !important; }' });
    await page.screenshot({ path: 'catalins.tech_without_header_links.png' });

    const img1 = PNG.sync.read(fs.readFileSync('catalins.tech.png'));
    const img2 = PNG.sync.read(fs.readFileSync('catalins.tech_without_header_links.png'));
    const { width, height } = img1;
    const diff = new PNG({ width, height });

    const result = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: 0.1 });
    if (result > 0) {
      console.log(`Different pixels: ${result}`);
      // send an email with the difference 
      // or perform any other kind of logic
      fs.writeFileSync('diff.png', PNG.sync.write(diff));
    } else {
      console.log('The screenshots are the same');
    }
  } catch (e) {
    console.log(e)
  } finally {
    await browser.close();
  }
})();

Summary

The quickest way to take screenshots of a webpage and compare them is to use the:

  • Puppeteer library for screenshots
  • Pixelmatch for detecting differences.

If you are looking to learn Node.js or get better at it, check the Complete Node.js Developer course.

Support this blog 🧡

If you like this content and it helped you, please consider supporting this blog. This helps me create more free content and keep this blog alive.

Become a supporter