Measuring user flow performance with Lighthouse and WebdriverIO

Measuring user flow performance with Lighthouse and WebdriverIO

Lighthouse has a new user-flow API that allows lab testing at any point within a page's lifespan. There is support for generating a lighthouse report from a puppeteer script, but I wanted to explore using WebdriverIO to do the same!

Why would you want to do this though?

Some of you will know that WebdriverIO has some inbuilt support for performance testing, which leverages chrome devtools as well, but I find one of the large benefits of Lighthouse is the report that gets generated, and not just the scores themselves.

Additionally, we can leverage pre-existing WebdriverIO code to perform snapshots or timespans, other important aspects of front end performance beyond the initial page load.

All the examples here reference web.dev/lighthouse-user-flows/#timespans, where you can see the original Puppeteer code and resulting lighthouse reports; this blog focuses on how to use WebdriverIO with lighthouse flow.

Firstly though, what does a basic puppeteer script look like, when combined with the new user-flow API? From the example in web.dev/lighthouse-user-flows for a basic page load performance analysis we have:

import fs from 'fs';
import open from 'open';
import puppeteer from 'puppeteer';
import { startFlow } from 'lighthouse/lighthouse-core/fraggle-rock/api.js';

async function captureReport() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  const flow = await startFlow(page, {name: 'Single Navigation'});
  await flow.navigate('https://web.dev/performance-scoring/');

  await browser.close();

  const report = flow.generateReport();
  fs.writeFileSync('flow.report.html', report);
  open('flow.report.html', { wait: false });
}

captureReport();

To do the same in WebdriverIO, we just need to retrieve Puppeteer and access the first page:

const puppeteerBrowser = await browser.getPuppeteer();
const page = (await puppeteerBrowser.pages())[0];

It's possible to run WebdriverIO in standalone mode, but as most people are familiar with looking at it used in a test:

import fs from 'fs';
import open from 'open';
import { startFlow } from 'lighthouse/lighthouse-core/fraggle-rock/api.js';

describe('Lighthouse Performance ', () => {
  it('Should be able to generate lighthouse report with Webdriverio', async () => {
     const puppeteerBrowser = await browser.getPuppeteer();
     const page = (await puppeteerBrowser.pages())[0];

     const flow = await startFlow(page, {name: 'Single Navigation'});
     await flow.navigate('https://web.dev/performance-scoring/');

     await browser.close();

     const report = flow.generateReport();
     fs.writeFileSync('flow.report.html', report);
     open('flow.report.html', {wait: false});
  }
}

This though doesn't really offer any benefit over using Puppeteer directly. For that, as mentioned, we want to be performing snapshot or timespan performance assessments, getting the benefit of (arguably) a nicer syntax, or more ideally, leveraging pre-existing WebdriverIO code that will perform the navigation and interactions required.

For example, instead of:

async function captureReport() {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();

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

  await page.goto('https://squoosh.app/', { waitUntil: 'networkidle0'} );

  // Wait for first demo-image button, then open it.
  const demoImageSelector = 'ul[class*="demos"] button';
  await page.waitForSelector(demoImageSelector);
  await flow.snapshot({ stepName: 'Page loaded' });
  await page.click(demoImageSelector);

  // Wait for advanced settings button in UI, then open them.
  const advancedSettingsSelector = 'form label[class*="option-reveal"]';
  await page.waitForSelector(advancedSettingsSelector);
  await flow.snapshot({ stepName: 'Demo loaded' });
  await page.click(advancedSettingsSelector);

  await flow.snapshot({ stepName: 'Advanced settings opened' });

  browser.close();

  const report = flow.generateReport();
  fs.writeFileSync('flow.report.html', report);
  open('flow.report.html', {wait: false});
}

captureReport();

we can instead write the below, using WebdriverIO for navigation and interaction:

import fs from 'fs';
import open from 'open';
import { startFlow } from 'lighthouse/lighthouse-core/fraggle-rock/api.js';

describe('Lighthouse Performance ', () => {
  it('Should be able to generate lighthouse report with Webdriverio', async () => {
     const puppeteerBrowser = await browser.getPuppeteer();
     const page = (await puppeteerBrowser.pages())[0];

     const flow = await startFlow(page, {name: 'Single Navigation'});
     await browser.url('https://squoosh.app/');

     // Wait for first demo-image button, then open it.
     const demoImageSelector = await $('ul[class*="demos"] button');
     await demoImageSelector.waitForDisplayed();
     await flow.snapshot({ stepName: 'Page loaded' });
     await demoImageSelector.click();

     // Wait for advanced settings button in UI, then open them.
     const advancedSettingsSelector = $('label[class*="option-reveal"]');
     await advancedSettingsSelector.waitForDisplayed();

     await flow.snapshot({ stepName: 'Demo loaded' });
     await advancedSettingsSelector.click();

     await flow.snapshot({ stepName: 'Advanced settings opened' });

     const report = flow.generateReport();
     fs.writeFileSync('flow.report.html', report);
     open('flow.report.html', { wait: false });
  }
}

This is obviously a trivial amount of navigation; the following is an example of performing a timespan performance assessment, taken from the company where I work, Glofox, where we use the applications APIs to setup data as well as some high level flows that then use WebdriverIO behind the scenes:

describe('Dashboard Performance', () => {
  it('Lighthouse flow for booking class for member', async () => {
    const { className } = fakeData();
    const puppeteerBrowser = await browser.getPuppeteer();
    const page = (await puppeteerBrowser.pages())[0];

    const flow = await startFlow(page);

    await flow.startTimespan({ stepName: 'Login' });
    // WebdriverIO code - performs invitial browser.url navigations, filling in data across two pages
    await Dashboard.login();
    await flow.endTimespan();

    await flow.snapshot({ stepName: 'Logged in' });

    // create a 'member' via API
    const member = new Member();
    const { id: memberId } = await DashboardAPICalls.Members.create({ member });

    // create a class for member to book via API
    await DashboardAPICalls.Classes.createClass({ className });

    await flow.startTimespan({ stepName: 'Book Class' });
    // book class via WebdriverIO - multiple pages and clicks involved
    await Dashboard.bookClass({ member, className });
    await flow.endTimespan();
  }
}

With this example, hopefully, you can see how we're leveraging our existing WebriverIO code (and API abstractions to set up data!) to perform navigations and interactions, then making the lighthouse flow analysis.

Final Notes

The new Lighthouse user-flow API provides extra possibilities for analyzing user front-end performance - as shown you can leverage WebdriverIO existing code to do any required navigation and form filling. One additional consideration is how to run these types of tests as part of a pipeline - but that's for another day.

Appendix

If you want to run the test on desktop, you can pass in a desktop configuration to the startFlow method:

const config = require('lighthouse/lighthouse-core/config/desktop-config.js');

const puppeteerBrowser = await browser.getPuppeteer();
const page = (await puppeteerBrowser.pages())[0];
const flow = await startFlow(page, { config });

Did you find this article valuable?

Support Hugh McCamphill by becoming a sponsor. Any amount is appreciated!