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:
- Lighthouse CI
- This is a wrapper around the core Lighthouse library, meant for use with a CI environment. It exposes an altered CLI tailored to CI, report uploading (and/or serving), an integrated report viewer, and more.
- web.dev has a good introductory guide
- You can also find a pre-built GitHub Action for it
- Lighthouse also powers some existing Google tools for measuring web performance, such as:
- PageSpeed Insights
- web.dev/measure
- Chrome DevTools Audits Panel (this is probably how many, if not most developers are first introduced to Lighthouse)
- Lighthouse is also integrated into dozens of 3rd party services and projects.
- Some popular examples are WebPageTest and Treo
- In addition to using in a NodeJS project, the NPM package can also be used directly via the CLI
- Example:
npx lighthouse https://example.com
- Example:
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:
- A subsection of the main README
- Covers CLI syntax and options and usage examples
- The configuration docs are also helpful, since the CLI can use a configuration file
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 areperf
,experimental
anddesktop
. Each of those actually just maps to the corresponding config file, with-config.js
appended, such asperf-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 beundefined
orlighthouse: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
totrue
when runningtsc
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.