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:
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:
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:
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:
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 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 screenshotsPixelmatch
for detecting differences.
If you are looking to learn Node.js or get better at it, check the Complete Node.js Developer course.