How to Use and Customize Lighthouse – CLI & NodeJS

Introduction

Google Lighthouse is a free (as in both cost, and open-source) tool for evaluating the performance and capabilities of a website (or web app). Over time its capabilities have been expanded, and the “Lighthouse” product now encompasses multiple categories, including accessibility, 20+ category “groups”, 150+ audits, and hundreds of metrics, timings, and more.

With power, in this case, comes a little bit of complexity. This post covers a bit of the Lighthouse NodeJS package and CLI tool, getting started with using it, and some basics on customizing the output. As well tips as using it in a TypeScript project.

Let’s get started!

Different Ways to Use Lighthouse

Although this post will focus on the particulars of the CLI and a customized integration with the Lighthouse NodeJS package, published on NPM, I wanted to mention that there are many different forms that Lighthouse takes, and systems it is integrated into. Here are some of those:

NPM Package

The “core” of Lighthouse is the GoogleChrome/lighthouse repo, which is also distributed as a published NPM package.

Lighthouse Package – CLI Usage

The Lighthouse package exposes a convenient CLI, which can be used to accomplish many standard Lighthouse related tasks, with or without customization, all from the command line.

The instructions for the CLI live in a few different places, but there are two main starting points:

If you are going to be using the CLI with a NodeJS project, you should install it as a local dependency, otherwise, you can install it globally with npm install -g lighthouse

A minimal example that shows running Lighthouse and then viewing the results is:

lighthouse https://example.com --view

Lighthouse Package – Customized Usage

Working with the Lighthouse CLI, there is a lot that you can do entirely through command line arguments. You can specify which audits to run, with --only-audits, or categories, with --only-categories, or even emulate a different screen type with --screenEmulation. However, not everything can be specified by argument alone, and at a certain point putting dozens of arguments into a CLI command also becomes unwieldy.

If you are looking to configure Lighthouse to your exact needs, going beyond the default arguments, here are your options.

Lighthouse Config File

If you want to customize Lighthouse to your specific requirements, you should be considering a Lighthouse configuration file.

You can use this to customize pretty much every setting for Lighthouse, and it can be reused across both the CLI and custom code that uses the Lighthouse package.

For example, here is a config file for only generating a PWA and performance report, emulating a mobile form factor:

my-lh-config.js

module.exports = {
    extends: 'lighthouse:default',
    settings: {
        onlyCategories: ['performance', 'pwa'],
        formFactor: 'mobile'
    }
}

To use this config with Lighthouse, you can pass the config file path via CLI:

lighthouse https://example.com --config-path ./my-lh-config.js

Or, you could import the file and pass it to the Lighthouse runner in a custom NodeJS script:

import MyConfig from './my-lh-config.js';

const results = await lighthouse('https://example.com', {}, MyConfig);

Lighthouse Config Files – Presets, Examples, and Extending

Rather than building your config file completely from scratch, you might find it helpful to start with an existing template, or even just completely reuse a preset config that ships with Lighthouse. This is not only an option with Lighthouse, but it is encouraged!

Presets: In the Lighthouse project, the CLI parameter of --preset is meant for specifying a config file by its special preset alias. As of right now, those are perf, experimental and desktop. Each of those actually just maps to the corresponding config file, with -config.js appended, such as perf-config.js

The pre-built config files that ship with Lighthouse can be found in the lighthouse-core/config/ directory. Because each config file follows the conventions of a JS module whose default export is a LH config object, you can use them with the CLI:

lighthouse https://example.com --config-path ./node_modules/lighthouse/lighthouse-core/config/desktop-config.js

… Or in custom JS:

import DesktopConfig from 'lighthouse/lighthouse-core/config/desktop-config.js';

const results = await lighthouse('https://example.com', {}, DesktopConfig);

Don’t forget that you can also extend the default configs.

🚨 Note that this is not done through the config.extends property – that is only for dictating if your config values should be applied on top of the default settings. This value should only ever be undefined or lighthouse:default.

To extend an existing config, you can import and then spread the config object into your own. For example:

const results = await lighthouse('https://example.com', {}, {
    ...DesktopConfig,
    categories: {
        ...DesktopConfig.categories,
        'installable-check': {
            title: 'Installable Check',
            auditRefs: [
                {id: 'installable-manifest', weight: 1, group: 'pwa-installable'}
            ]
        }
    }
});

Custom NodeJS Scripting

Working with Lighthouse outside of the CLI, in a custom NodeJS script, can feel a lot different than the CLI, even though a lot of the same settings are shared. For example, in a NodeJS script, you are responsible for spawning the Chrome instance that will be used to host the Lighthouse execution, and passing the port number to the Lighthouse runner / main function.

There is less documentation about the NodeJS scripting side than the CLI side (part of why I’m writing this post), but I can still point to at least two documentation section to review:

Here is an example of using the Lighthouse package in a custom NodeJS script:

// Main lighthouse runner / fn
import lighthouse from 'lighthouse';

// Required for launching chrome instance
import chromeLauncher from 'chrome-launcher';

// So we can save output
import {writeFile} from 'fs/promises';

(async () => {
    // Launch instance of Chrome
    const chrome = await chromeLauncher.launch();

    // Gather results and report from Lighthouse
    const results = await lighthouse('https://example.com', {
        port: chrome.port,
        output: 'html'
    }, {
        extends: 'lighthouse:default',
        settings: {
            onlyCategories: ['performance']
        }
    });

    // Save report to file
    await writeFile('./lighthouse-report.html', results.report);

    // Kill Chrome
    await chrome.kill();
})();

There are examples like this, and more, in my TypeScript example repo (more on this below).

Flags vs Config

When using the lighthouse() function in NodeJS, the first three arguments are url, flags and configJson. I’ll call configJson just config to make it easier. The delineation between flags and config can be a little murky, because they share some common properties (such as onlyCategories), but the general idea is that flags:

  • If used, any values will override configJson, and are the highest level of configuration. Similar to what you would pass via CLI
  • Are less granular than the configuration; used for high-level settings, like report output format
  • Should (IMHO) be used sparingly

The config, on the other hand, is the main way to configure Lighthouse and is where you will most commonly be passing custom settings (other than the port for Chrome and output options).

Slimming Down Results

One common misunderstanding that I had about Lighthouse was the linkage between scores, audits, categories, and artifacts. It’s tempting to think that if you limit the report to just a single category that the size of the data collected should shrink drastically, but that is not necessarily the case. Each category can pull in dozens of audits, and the score is not based on a single data source – rather it is the composite of multiple audits and metrics.

Related explanation here on #11357, and here on #11885.

If you really need a smaller JSON export from the report, the key is to use onlyCategories and onlyAudits to slim down the collected metrics, and then go a step further and use JavaScript’s delete to trim items you don’t need off the result object.

For example, here is a small cleaner function I put together that focuses on collecting the standard browser Lighthouse report scores, removing a lot of data in the JSON that is no longer necessary once the score is generated:

const AUDITS = {
    FCP: 'first-contentful-paint',
    LCP: 'largest-contentful-paint',
    FID: 'max-potential-fid',
    CLS: 'cumulative-layout-shift',
    TTI: 'interactive'
};

const onlyAudits = Object.keys(AUDITS).map((key) => AUDITS[key]);

/**
 * @param {LH.RunnerResult} result
 * @returns {LH.RunnerResult}
 */
function cleanReport(result) {
    for (const key in result.lhr.audits) {
        if (!onlyAudits.includes(key)) {
            delete result.lhr.audits[key];
        }
    }
    delete result.lhr.stackPacks;
    delete result.lhr.i18n;
    delete result.lhr.timing;
    delete result.lhr.categoryGroups;
    delete result.artifacts;
    delete result.report;
    return result;
}

This is for the core result object, but could be modified to operate directly on the result.report string instead.

TypeScript Support

🚨 Warning: Due to some incomplete types, you might need to set skipLibCheck to true when running tsc with some of these approaches.

Lighthouse Config Types

Although supporting types for the overall module is complicated (more on this in a bit), adding types for just the custom Lighthouse config object takes just a few lines of code, depending on your approach.

First, you need to make the ambient types for the config object available. You can do so by using a triple slash directive:

/// <reference path="./node_modules/lighthouse/types/config.d.ts" />

Or, if you have a tsconfig.json file and prefer this approach, adding that file to the include array. You can add all of the Lighthouse ambient types with:’

{
    "include": ["./node_modules/lighthouse/types/**/*.d.ts"]
}

Once you have those types loaded, you can refer to the LH.Config.Json interface when using LH config objects. For example, in a TypeScript file:

const config: LH.Config.Json = {
    extends: 'lighthouse:default',
    // ...
}

module.exports = config;

If you want to annotate the config type in a regular JavaScript file, you can do that with the support of an IDE like VSCode, which supports advanced type-checking via JSDoc:


// @ts-check
/// <reference path="./node_modules/lighthouse/types/config.d.ts" />

/** @type {LH.Config.Json} */
const config = {
    extends: 'lighthouse:default',
    // ...
}

module.exports = config;

Lighthouse Module Types

Lighthouse is a huge repository, spanning thousands of files, and unfortunately the TypeScript situation is currently not exactly ideal. Although some types are explicitly exported, large parts of the codebase only use internal type annotations (JSDoc / Closure Compiler), and do not publish their types as part of the NPM package. The net result of this is that, in your own code, imported methods from lighthouse might appear half-typed (with lots of any inferred types), or not typed at all.

Luckily, between the types that are exported and what TS can infer, you can pretty much fill in the gaps if you are coding a TypeScript project that uses the Lighthouse package. You can take a look at how some existing TypeScript projects did this, such as Paul Irish’s pwmetrics, or, I’ve also put together a small sample repo to demonstrate: lighthouse-ts-examples. Of particular interest is probably the ambients.d.ts file, which is what provides the types for the main lighthouse() function.

Leave a Reply

Your email address will not be published. Required fields are marked *