|
const puppeteer = require('puppeteer'); |
|
const { createCanvas, loadImage } = require('canvas') |
|
var fs = require('fs'); |
|
const fsExtra = require('fs-extra') |
|
const mergeImg = require('merge-img'); |
|
const mustache = require('mustache') |
|
const child_process = require('child_process'); |
|
|
|
let MAX_URLS = 50; |
|
let REPEAT = 3; |
|
|
|
let TEMPLATE = fs.readFileSync('template.html', 'utf8'); |
|
|
|
require('dotenv').config(); |
|
|
|
// Path to Web Page Replay src/ directory. A 'wpr' executable is required. |
|
let WPR_PATH = process.env.WPR_PATH; |
|
|
|
const Good3G = { |
|
'offline': false, |
|
'downloadThroughput': 1.5 * 1024 * 1024 / 8, |
|
'uploadThroughput': 750 * 1024 / 8, |
|
'latency': 40 |
|
}; |
|
|
|
const phone = puppeteer.devices['Nexus 5X']; |
|
|
|
|
|
function injectJs() { |
|
|
|
window.cls = 0; |
|
let po = new PerformanceObserver((list) => { |
|
for (const entry of list.getEntries()) { |
|
window.cls += entry.value; |
|
} |
|
}); |
|
|
|
po.observe({type: 'layout-shift', buffered: true}); |
|
} |
|
|
|
async function getBrowser(options) { |
|
options = options || {}; |
|
let args = ['--no-sandbox']; |
|
if(!options.allowWebFonts) { |
|
args.push("--disable-remote-fonts"); |
|
} |
|
if (options.useWpr) { |
|
args.push( |
|
'--host-resolver-rules="MAP *:80 127.0.0.1:8080,MAP *:443 127.0.0.1:8081,EXCLUDE localhost"', |
|
'--ignore-certificate-errors-spki-list=PhrPvGIaAMmd29hj8BCZOq096yj7uMpRNHpn5PDxI6I=' |
|
); |
|
} |
|
console.log(`Brwoser args = ${args}`); |
|
const browser = await puppeteer.launch({ |
|
args: args, |
|
//headless: false, |
|
//executablePath: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', |
|
timeout: 10000 |
|
}); |
|
return browser; |
|
} |
|
|
|
|
|
async function getNewPage(browser) { |
|
const page = await browser.newPage(); |
|
await page.emulate(phone); |
|
await page.setCacheEnabled(false); // no cache, so that we can reuse the same page several times |
|
return page; |
|
} |
|
|
|
|
|
async function getCLS(page, url) { |
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000}); |
|
await page.waitFor(1000); // let's give it a bit more time, to be sure everything's loaded |
|
|
|
console.log("Injecting JS..."); |
|
await Promise.race([ |
|
page.evaluate(injectJs), |
|
page.waitFor(5000) |
|
]); |
|
page.waitFor(2000); |
|
|
|
console.log("Gathering data..."); |
|
let cls = await Promise.race([ |
|
page.evaluate(function() {return window.cls}), |
|
page.waitFor(5000) |
|
]); |
|
return cls; |
|
} |
|
|
|
async function doOne(url, options) { |
|
console.log(`Processing ${options.allowWebFonts ? 'with' : 'without'} web fonts: ${url}`); |
|
let browser = await getBrowser(options); |
|
let page = await getNewPage(browser); |
|
let cls = 0; |
|
let repeat = options.useWpr === 'record' ? 1 : REPEAT; |
|
for (let i = 0; i < repeat; ++i) |
|
cls += await getCLS(page, url); |
|
cls /= REPEAT; |
|
await page.screenshot({path: 'output/' + options.screenshotPath}); |
|
await browser.close(); |
|
return cls; |
|
} |
|
|
|
function getWPRRun(mode) { |
|
let wpr = `${WPR_PATH}/wpr`; |
|
let wprArgs = [mode, '--http_port=8080', '--https_port=8081', 'archive.wprgo']; |
|
|
|
let process = child_process.spawn(wpr, wprArgs, {cwd: WPR_PATH}); |
|
process.stdout.on('data', () => {}); |
|
process.stderr.on('data', data => {}); |
|
|
|
let finish = new Promise(resolve => process.on('exit', resolve)); |
|
|
|
return {process: process, finish: finish}; |
|
} |
|
|
|
async function doBatchWithOptions(urls, max, options) { |
|
let wpr; |
|
if (options.useWpr) |
|
wpr = getWPRRun(options.useWpr); |
|
|
|
let results = {}; |
|
for (let url of urls.slice(0, max)) { |
|
try { |
|
let screenshotPath = `images/${options.allowWebFonts ? 'withfonts' : 'nofonts'}/${new URL(url).hostname}.png`; |
|
let cls = await doOne(url, Object.assign({screenshotPath: screenshotPath}, options)); |
|
|
|
results[url] = {url: url, cls: cls, screenshot: screenshotPath}; |
|
} catch (error) { |
|
console.log(error); |
|
//process.exit(0); |
|
} |
|
} |
|
|
|
if (wpr) { |
|
wpr.process.kill('SIGINT'); |
|
await wpr.finish; |
|
} |
|
|
|
return results; |
|
} |
|
|
|
async function doBatch(urls, max) { |
|
// reset output file and images dir |
|
fsExtra.emptyDirSync("output"); |
|
fs.mkdirSync("output/images"); |
|
fs.mkdirSync("output/images/withfonts"); |
|
fs.mkdirSync("output/images/nofonts"); |
|
|
|
await doBatchWithOptions(urls, max, {allowWebFonts: true, useWpr: 'record'}); |
|
|
|
let withFontResults = await doBatchWithOptions(urls, max, {allowWebFonts: true, useWpr: 'replay'}); |
|
let noFontResults = await doBatchWithOptions(urls, max, {allowWebFonts: false, useWpr: 'replay'}); |
|
|
|
let results = []; |
|
for (let url of urls) { |
|
let withfont = withFontResults[url]; |
|
let nofont = noFontResults[url]; |
|
if (!withfont || !nofont) |
|
continue; |
|
|
|
let diff = nofont.cls - withfont.cls; |
|
results.push({url: url, withFontCLS: withfont.cls, noFontCLS: nofont.cls, diff: diff, withFontScreenshot: withfont.screenshot, noFontScreenshot: nofont.screenshot}); |
|
} |
|
|
|
// write out result html |
|
results.sort((a, b) => (a.diff < b.diff) ? -1 : 1) |
|
var rendered = mustache.render(TEMPLATE, {items: results}); |
|
fs.writeFileSync('output/index.html', rendered) |
|
} |
|
|
|
let urls = fs.readFileSync('input.csv').toString().split("\n").filter(url => url.length); |
|
doBatch(urls, 200).then(res => {console.log("Done!");process.exit(0);}); |