-
-
Save teppeis/01d225eb2c3adb3e7f1de0e8353c509b to your computer and use it in GitHub Desktop.
CSS/JS code coverage during lifecycle of page load
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @author ebidel@ (Eric Bidelman) | |
* License Apache-2.0 | |
* | |
* Shows how to use Puppeeteer's code coverage API to measure CSS/JS coverage across | |
* different points of time during loading. Great for determining if a lazy loading strategy | |
* is paying off or working correctly. | |
* | |
* Install: | |
* npm i puppeteer chalk cli-table | |
* Run: | |
* URL=https://example.com node coverage.js | |
*/ | |
const puppeteer = require('puppeteer'); | |
const chalk = require('chalk'); | |
const Table = require('cli-table'); | |
const URL = process.env.URL || 'https://www.chromestatus.com/features'; | |
const EVENTS = [ | |
'domcontentloaded', | |
'load', | |
'networkidle0', | |
]; | |
function formatBytesToKB(bytes) { | |
if (bytes > 1024) { | |
const formattedNum = new Intl.NumberFormat('en-US', {maximumFractionDigits: 1}).format(bytes / 1024); | |
return `${formattedNum}KB`; | |
} | |
return `${bytes} bytes`; | |
} | |
class UsageFormatter { | |
constructor(stats) { | |
this.stats = stats; | |
} | |
static eventLabel(event) { | |
return chalk.magenta(event); | |
} | |
summary(used = this.stats.usedBytes, total = this.stats.totalBytes) { | |
const percent = Math.round((used / total) * 100); | |
return `${formatBytesToKB(used)}/${formatBytesToKB(total)} (${percent}%)`; | |
} | |
shortSummary(used, total = this.stats.totalBytes) { | |
const percent = Math.round((used / total) * 100); | |
return used ? `${formatBytesToKB(used)} (${percent}%)` : 0; | |
} | |
/** | |
* Constructors a bar chart for the % usage of each value. | |
* @param {!{jsUsed: number, cssUsed: number, totalBytes: number}=} stats Usage stats. | |
* @return {string} | |
*/ | |
barGraph(stats = this.stats) { | |
const maxBarWidth = 30; | |
const jsSegment = ' '.repeat((stats.jsUsed / stats.totalBytes) * maxBarWidth); | |
const cssSegment = ' '.repeat((stats.cssUsed / stats.totalBytes) * maxBarWidth); | |
const unusedSegment = ' '.repeat(maxBarWidth - jsSegment.length - cssSegment.length); | |
return chalk.bgRedBright(jsSegment) + chalk.bgBlueBright(cssSegment) + | |
chalk.bgBlackBright(unusedSegment); | |
} | |
} | |
const stats = new Map(); | |
/** | |
* @param {!Object} coverage | |
* @param {string} type Either "css" or "js" to indicate which type of coverage. | |
* @param {string} eventType The page event when the coverage was captured. | |
*/ | |
function addUsage(coverage, type, eventType) { | |
for (const entry of coverage) { | |
if (!stats.has(entry.url)) { | |
stats.set(entry.url, []); | |
} | |
const urlStats = stats.get(entry.url); | |
let eventStats = urlStats.find(item => item.eventType === eventType); | |
if (!eventStats) { | |
eventStats = { | |
cssUsed: 0, | |
jsUsed: 0, | |
get usedBytes() { return this.cssUsed + this.jsUsed; }, | |
totalBytes: 0, | |
get percentUsed() { | |
return this.totalBytes ? Math.round(this.usedBytes / this.totalBytes * 100) : 0; | |
}, | |
eventType, | |
url: entry.url, | |
}; | |
urlStats.push(eventStats); | |
} | |
eventStats.totalBytes += entry.text.length; | |
for (const range of entry.ranges) { | |
eventStats[`${type}Used`] += range.end - range.start - 1; | |
} | |
} | |
} | |
async function collectCoverage() { | |
const browser = await puppeteer.launch({headless: true}); | |
// Do separate load for each event. See | |
// https://github.com/GoogleChrome/puppeteer/issues/1887 | |
const collectPromises = EVENTS.map(async event => { | |
console.log(`Collecting coverage @ ${UsageFormatter.eventLabel(event)}...`); | |
const page = await browser.newPage(); | |
await Promise.all([ | |
page.coverage.startJSCoverage(), | |
page.coverage.startCSSCoverage() | |
]); | |
await page.goto(URL, {waitUntil: event}); | |
const [jsCoverage, cssCoverage] = await Promise.all([ | |
page.coverage.stopJSCoverage(), | |
page.coverage.stopCSSCoverage() | |
]); | |
addUsage(cssCoverage, 'css', event); | |
addUsage(jsCoverage, 'js', event); | |
await page.close(); | |
}); | |
await Promise.all(collectPromises); | |
return browser.close(); | |
} | |
(async() => { | |
await collectCoverage(); | |
for (const [url, vals] of stats) { | |
console.log('\n' + chalk.cyan(url)); | |
const table = new Table({ | |
head: [ | |
'Event', | |
`${chalk.bgRedBright(' JS ')} ${chalk.bgBlueBright(' CSS ')} % used`, | |
'JS used', | |
'CSS used', | |
'Total bytes used' | |
], | |
style: {head: ['white'], border: ['grey']} | |
}); | |
EVENTS.forEach(event => { | |
const usageForEvent = vals.filter(val => val.eventType === event); | |
if (usageForEvent.length) { | |
for (const stats of usageForEvent) { | |
const formatter = new UsageFormatter(stats); | |
table.push([ | |
UsageFormatter.eventLabel(stats.eventType), | |
formatter.barGraph(), | |
formatter.shortSummary(stats.jsUsed), // !== 0 ? `${formatBytesToKB(stats.jsUsed)}KB` : 0, | |
formatter.shortSummary(stats.cssUsed), | |
formatter.summary() | |
]); | |
} | |
} else { | |
table.push([UsageFormatter.eventLabel(event), 'no usage found', '-', '-', '-']); | |
} | |
}); | |
console.log(table.toString()); | |
} | |
// Print total usage for each event. | |
// console.log('\n'); | |
EVENTS.forEach(event => { | |
let totalBytes = 0; | |
let totalUsedBytes = 0; | |
const metrics = Array.from(stats.values()); | |
const statsForEvent = metrics.map(eventStatsForUrl => { | |
const statsForEvent = eventStatsForUrl.filter(stat => stat.eventType === event)[0]; | |
// TODO: need to sum max totalBytes. Currently ignores stats if event didn't | |
// have an entry. IOW, all total numerators should be max totalBytes seen for that event. | |
if (statsForEvent) { | |
totalBytes += statsForEvent.totalBytes; | |
totalUsedBytes += statsForEvent.usedBytes; | |
} | |
}); | |
const percentUsed = Math.round(totalUsedBytes / totalBytes * 100); | |
console.log(`Total used @ ${chalk.magenta(event)}: ${formatBytesToKB(totalUsedBytes)}/${formatBytesToKB(totalBytes)} (${percentUsed}%)`); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment