The blind spots in load-only performance testing

Lighthouse has long been the go-to lab tool for performance and best-practice audits, but its traditional focus has been a single moment: the initial page load with an empty cache. That leaves a lot of the page's actual life unexamined. Real users arrive with warm caches, pages with registered service workers behave differently on repeat visits, and a page can shift or jank long after the load event fires.

The Core Web Vitals are defined across all page loads, and CLS in particular is measured from the moment the page is opened until it's closed. A lab test that only looks at the cold load simply can't capture those scenarios. The new Lighthouse user flow API addresses this by letting you script interactions with Puppeteer and run Lighthouse at any point during the page's lifespan. You can audit the initial navigation, take a performance snapshot mid-interaction, or measure a span of time that includes both loads and user input.

The API is still in preview but available now. The examples below require Node 14 or later. In an empty directory, install the dependencies with:

npm install lighthouse puppeteer

The first user flow mode, navigation, is essentially a formal name for what Lighthouse has always done: audit a cold page load. You can still run that standard test, but the flow framework also makes it easy to pair it with additional runs.

To capture a navigation, open a browser with Puppeteer, start a user flow, and navigate to the URL:

import fs from 'fs';
import puppeteer from 'puppeteer';
import { startFlow } from 'lighthouse';

const browser = await puppeteer.launch();
const page = await browser.newPage();

const flow = await startFlow(page, { name: 'Squoosh navigation' });
await flow.navigate('https://squoosh.app/');

await browser.close();

The resulting report presents a summary with a single step. Clicking into it reveals the standard Lighthouse report for that navigation.

The default behavior clears cache and local storage before the run. But a returning visitor is a different story. You can run a second navigation that skips that cleanup to measure the warm-load experience:

await flow.navigate('https://web.dev/performance/');

Whether a warm load helps or hurts depends on the site, but a site where users browse multiple pages per visit will see very different numbers between the cold pass and the cached pass. Running both side by side gives a more honest look at field conditions.

Snapshot mode: auditing a page's current state

The snapshot mode audits the page exactly as it stands, without a reload. That lets you test states that are unreachable in a standard cold load: an open drop-down, a partially filled form, or an expanded settings panel.

As a test case, suppose Squoosh adds an Advanced Settings panel that is only visible once an image is loaded and the menu has been expanded. You can drive Puppeteer through those steps and take a Lighthouse snapshot along the way:

const browser = await puppeteer.launch();
const page = await browser.newPage();

const flow = await startFlow(page, { name: 'Squoosh snapshots' });

await flow.navigate('https://squoosh.app/');

// Wait for image to load
await page.waitForSelector('.img-container img');

const demoImage = await page.$('.img-container img');
await demoImage.click();

await flow.snapshot({ name: 'Squoosh image loaded' });

await page.click('.options-section .button');
await page.click('.show-advanced:');

await flow.snapshot({ name: 'Squoosh expanded options' });

await browser.close();

The flow report will show each step's summary. The snapshots may surface accessibility flags that warrant a manual check even if overall scores look fine.

Timespan mode: measuring interaction over time

The biggest fidelity gap between lab and field data has been user input. Field metrics from CrUX record what actually happens during a visit; a standard Lighthouse run only knows what happens when the page loads with no interaction. The timespan mode runs audits over a defined period, possibly including a navigation, which allows CLS and other metrics to capture the effects of scrolling and clicks.

Consider a test site that injects ads into an article as you scroll, without having reserved space for them. A normal Lighthouse navigation will report a CLS of 0, since nothing shifts during the load. But once the user scrolls and the ads are inserted, the content below gets pushed around, and the real CLS climbs.

You can capture both phases in one script:

const browser = await puppeteer.launch();
const page = await browser.newPage();

const flow = await startFlow(page, { name: 'CLS during scroll' });

await flow.navigate('https://pie-charmed-treatment.glitch.me');

// Scrolling the page AFTER the timespan starts
await flow.startTimespan({ name: 'Navigate and scroll' });
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await flow.endTimespan();

await browser.close();

The navigation-only step shows a CLS of 0. The "Navigate and scroll" timespan, however, shows the layout shifts piling up as the lazy-loaded content is inserted. Only Total Blocking Time and Cumulative Layout Shift are currently available for timespan analysis, but that's enough to expose a problem that was previously invisible to Lighthouse.

Feedback

The user flow APIs are still evolving. Questions can be directed to the Lighthouse discussion forums, and bugs or suggestions belong in the issue tracker.