Skip to content

Instantly share code, notes, and snippets.

@martinschierle
Last active October 24, 2022 20:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martinschierle/f9c7542d5550a7d3f7d05008d339deab to your computer and use it in GitHub Desktop.
Save martinschierle/f9c7542d5550a7d3f7d05008d339deab to your computer and use it in GitHub Desktop.
Puppeteer script to check performance of a page with and without service worker
// Idea and some code stemming from https://michaljanaszek.com/blog/test-website-performance-with-puppeteer#cacheAndServiceWorker
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const {URL} = require('url');
var fs = require('fs');
var sw_scope;
let NETWORK_PRESETS = {
'GPRS': {
'offline': false,
'downloadThroughput': 50 * 1024 / 8,
'uploadThroughput': 20 * 1024 / 8,
'latency': 500
},
'Regular2G': {
'offline': false,
'downloadThroughput': 250 * 1024 / 8,
'uploadThroughput': 50 * 1024 / 8,
'latency': 300
},
'Good2G': {
'offline': false,
'downloadThroughput': 450 * 1024 / 8,
'uploadThroughput': 150 * 1024 / 8,
'latency': 150
},
'Regular3G': {
'offline': false,
'downloadThroughput': 750 * 1024 / 8,
'uploadThroughput': 250 * 1024 / 8,
'latency': 100
},
'Good3G': {
'offline': false,
'downloadThroughput': 1.5 * 1024 * 1024 / 8,
'uploadThroughput': 750 * 1024 / 8,
'latency': 40
},
'Regular4G': {
'offline': false,
'downloadThroughput': 4 * 1024 * 1024 / 8,
'uploadThroughput': 3 * 1024 * 1024 / 8,
'latency': 20
},
'DSL': {
'offline': false,
'downloadThroughput': 2 * 1024 * 1024 / 8,
'uploadThroughput': 1 * 1024 * 1024 / 8,
'latency': 5
},
'WiFi': {
'offline': false,
'downloadThroughput': 30 * 1024 * 1024 / 8,
'uploadThroughput': 15 * 1024 * 1024 / 8,
'latency': 2
}
}
var first = true;
const phone = devices['Nexus 5X'];
var results = { "url:": "",
"withSW": {"firstLoad": {}, "repeatLoad": {}},
"withoutSW": {"firstLoad": {}, "repeatLoad": {}},
};
async function runTest(url, throttleCPU, throttleNetwork, runs) {
sw_scope = url; // as a default, we'll try to update later
results.url = url;
for(var i=0; i < runs; i++) {
console.log("Starting test with sw");
await testPage(url, throttleCPU, throttleNetwork, false, results.withSW, runs);
console.log("Starting test without sw");
await testPage(url, throttleCPU, throttleNetwork, true, results.withoutSW, runs);
}
}
async function testPage(url, throttleCPU, throttleNetwork, disableSW, resultsObject, runs) {
const browser = await puppeteer.launch();
// first load
console.log(" Running first load");
await loadPage(url, browser, throttleCPU, throttleNetwork, disableSW, resultsObject.firstLoad, runs);
// repeat load
console.log(" Running repeat load");
await loadPage(url, browser, throttleCPU, throttleNetwork, disableSW, resultsObject.repeatLoad, runs);
await browser.close();
}
async function loadPage(url, browser, throttleCPU, throttleNetwork, disableSW, resultsMap, runs) {
const page = await browser.newPage();
const client = await page.target().createCDPSession();
// enable some debug stuff
await client.send('Network.enable');
await client.send('ServiceWorker.enable');
if(throttleNetwork) {
console.log(" Throttling Network");
await client.send('Network.emulateNetworkConditions', NETWORK_PRESETS.Good3G);
}
if(throttleCPU) {
console.log(" Throttling CPU");
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
}
await page.emulate(phone);
// disable sw if needec
if(disableSW) {
console.log(" UnregisterSW");
// stopping doesn't help, as it's not yet installed anyway, but let's keep it just in case
await client.send('ServiceWorker.stopAllWorkers');
// unregister doesn't help, as it's not yet installed on first load. On second load it would trigger reinstall, which again will skew number
//await client.send('ServiceWorker.unregister', {scopeURL: sw_scope,});
// blocking would be best, but doesn't seem to work for sws - probably as they are not requested from page context :-/
//await client.send('Network.setBlockedURLs', {urls: ["*sw.js*"]});
// bypass only affects calls from page, wouldn't eliminate overhead from precaching done just by sw, but let's keep it in just in case
await client.send('Network.setBypassServiceWorker', {bypass: true});
// this will make it impossible to even register a sw
await page.evaluateOnNewDocument("navigator.serviceWorker.register = function(){console.log('mep')};");
}
//load target url
console.log(" Loading page....");
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 90000
});
// we'll need to wait a sec, otherwise FMP isn't calculated yet
await page.waitFor(2000);
var registrations = await page.evaluate("navigator.serviceWorker?navigator.serviceWorker.getRegistrations():[]");
console.log(" SWs on page: " + registrations.length);
console.log(" Collecting results...");
// find sw scopeURL
var new_scope = await page.evaluate("navigator.serviceWorker.getRegistrations().then(function(regs) {if(regs&&regs.length>0) return regs[0].scope})");
if(new_scope) {
sw_scope = new_scope;
console.log(" sw scope is: " + sw_scope);
}
// collect performance metrics - you can also comment out and use lighthouse below instead
var fmp = (await page._client.send('Performance.getMetrics')).metrics.find(x => x.name === "FirstMeaningfulPaint").value/1000;
const firstPaint = await page.evaluate("window.performance.getEntriesByName('first-paint')[0].startTime;");
const firstContentfulPaint = await page.evaluate("window.performance.getEntriesByName('first-contentful-paint')[0].startTime;");
const domInteractive = await page.evaluate("window.performance.timing.domInteractive - window.performance.timing.navigationStart");
const loadEventStart = await page.evaluate("window.performance.timing.loadEventStart - window.performance.timing.navigationStart");
if(!resultsMap.firstPaint) resultsMap.firstPaint = 0;
if(!resultsMap.firstContentfulPaint) resultsMap.firstContentfulPaint = 0;
if(!resultsMap.meaningfulPaint) resultsMap.meaningfulPaint = 0;
if(!resultsMap.domInteractive) resultsMap.domInteractive = 0;
if(!resultsMap.loadEventStart) resultsMap.loadEventStart = 0;
resultsMap.firstPaint += firstPaint/runs;
resultsMap.firstContentfulPaint += firstContentfulPaint/runs;
resultsMap.domInteractive += domInteractive/runs;
resultsMap.loadEventStart += loadEventStart/runs;
resultsMap.meaningfulPaint += fmp/runs;
//console.log(firstPaint + " - " + fmp + " - " + domInteractive + " - " + loadEventStart);
}
runTest("https://amp.dev", true, true, 10).then(function() {
//console.log(JSON.stringify(results, null, 2));
console.log("First Load Diff: " + results.url + ", "
+ (results.withSW.firstLoad.firstPaint-results.withoutSW.firstLoad.firstPaint) + ", "
+ (results.withSW.firstLoad.firstContentfulPaint-results.withoutSW.firstLoad.firstContentfulPaint) + ", "
+ (results.withSW.firstLoad.meaningfulPaint-results.withoutSW.firstLoad.meaningfulPaint) + ", "
+ (results.withSW.firstLoad.domInteractive-results.withoutSW.firstLoad.domInteractive) + ", "
+ (results.withSW.firstLoad.loadEventStart-results.withoutSW.firstLoad.loadEventStart)
);
console.log("Repeat Load Diff: " + results.url + ", "
+ (results.withSW.repeatLoad.firstPaint-results.withoutSW.repeatLoad.firstPaint) + ", "
+ (results.withSW.repeatLoad.firstContentfulPaint-results.withoutSW.repeatLoad.firstContentfulPaint) + ", "
+ (results.withSW.repeatLoad.meaningfulPaint-results.withoutSW.repeatLoad.meaningfulPaint) + ", "
+ (results.withSW.repeatLoad.domInteractive-results.withoutSW.repeatLoad.domInteractive) + ", "
+ (results.withSW.repeatLoad.loadEventStart-results.withoutSW.repeatLoad.loadEventStart)
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment