Skip to content

Instantly share code, notes, and snippets.

@ebidel
Last active October 18, 2025 09:48
Show Gist options
  • Save ebidel/2e9f78f8bd3f025653aba711a4689694 to your computer and use it in GitHub Desktop.
Save ebidel/2e9f78f8bd3f025653aba711a4689694 to your computer and use it in GitHub Desktop.

Revisions

  1. ebidel revised this gist Feb 28, 2018. 1 changed file with 1 addition and 205 deletions.
    206 changes: 1 addition & 205 deletions coverage.js
    Original file line number Diff line number Diff line change
    @@ -1,205 +1 @@
    /**
    * @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}%)`);
    });

    })();
    Moved to https://github.com/ebidel/puppeteer-examples
  2. ebidel revised this gist Feb 26, 2018. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions screenshot.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1 @@
    <img width="741" alt="screen shot 2018-02-26 at 10 20 41 am" src="https://user-images.githubusercontent.com/238208/36687690-2de2bbaa-1adf-11e8-912b-e21cda0160ce.png">

    ![Analytics](https://ga-beacon.appspot.com/UA-46812528-2/2e9f78f8bd3f025653aba711a4689694)
  3. ebidel revised this gist Feb 26, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion screenshot.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    <img width="741" alt="screen shot 2018-02-26 at 10 20 41 am" src="https://user-images.githubusercontent.com/238208/36687690-2de2bbaa-1adf-11e8-912b-e21cda0160ce.png">

    ![Analytics](https://ga-beacon.appspot.com/UA-46812528-2/2e9f78f8bd3f025653aba711a4689694?pixel)
    ![Analytics](https://ga-beacon.appspot.com/UA-46812528-2/2e9f78f8bd3f025653aba711a4689694)
  4. ebidel revised this gist Feb 26, 2018. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions screenshot.md
    Original file line number Diff line number Diff line change
    @@ -1 +1,3 @@
    <img width="741" alt="screen shot 2018-02-26 at 10 20 41 am" src="https://user-images.githubusercontent.com/238208/36687690-2de2bbaa-1adf-11e8-912b-e21cda0160ce.png">

    ![Analytics](https://ga-beacon.appspot.com/UA-46812528-2/2e9f78f8bd3f025653aba711a4689694?pixel)
  5. ebidel created this gist Feb 26, 2018.
    205 changes: 205 additions & 0 deletions coverage.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,205 @@
    /**
    * @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}%)`);
    });

    })();
    1 change: 1 addition & 0 deletions screenshot.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    <img width="741" alt="screen shot 2018-02-26 at 10 20 41 am" src="https://user-images.githubusercontent.com/238208/36687690-2de2bbaa-1adf-11e8-912b-e21cda0160ce.png">