Skip to content

Instantly share code, notes, and snippets.

@xiaochengh
Last active August 24, 2020 23:49
Show Gist options
  • Save xiaochengh/257b9395cebd40903ed841e348d5c703 to your computer and use it in GitHub Desktop.
Save xiaochengh/257b9395cebd40903ed841e348d5c703 to your computer and use it in GitHub Desktop.
Analyze CLS caused by web fonts
WPR_PATH=/path-to-user-home/go/src/github.com/catapult-project/catapult/web_page_replay_go/src/

Methodology

Load page with and without the --disable-remote-fonts flag. Their CLS difference should be attributed to web fonts.

Contributors

@martinschierle @xiaochengh

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);});
<html>
<head>
<style>
#mainTable tr:nth-child(odd){
background-color: lightblue;
}
thead {
font-weight: bold;
background-color: lightgrey;
}
.screenshot {
margin:20px;
width:150px;
}
.heatmap {
width:400px;
}
td {
word-break:break-all;
width: 16%;
}
</style>
</head>
<body onload="renderHello()">
<h1>Analysis results for Font-related layout shifts</h1>
<table>
<thead>
<tr>
<td width="10%">URL</td>
<td>CLS With Web-Fonts</td>
<td>CLS Without Webfonts</td>
<td>CLS Diff</td>
<td>Screenshot With Web-Fonts</td>
<td>Screenshot Without Web-Fonts</td>
</tr>
</thead>
<tbody id="mainTable">
{{#items}}
<tr>
<td>{{url}}</td>
<td>{{withFontCLS}}</td>
<td>{{noFontCLS}}</td>
<td>{{diff}}</td>
<td><img src="{{withFontScreenshot}}" width="200px"></td>
<td><img src="{{noFontScreenshot}}" width="200px"></td>
</tr>
{{/items}}
</tbody>
</table>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment