Running Lighthouse CI on All Modified Next.js Pages using GitHub Actions
On a Next.js website with tons of pages, you can automate Lighthouse CI testing for every deployment without having to run it on every page.
What is Lighthouse?
Lighthouse is an open-source auditing tool for web pages developed by Google.
Web developers use it to optimize some of the metrics that Google may use
to rank your site in search results. Countless case studies show that improving your core web vitals can lead to a lower bounce rate,
an increase in page views, and even higher sales.
It’s integrated into Chromium browsers as a part of the devtools, but it can also be run automatically in a headless environment with
Lighthouse CI.
Running Lighthouse Automatically
Lighthouse CI is incredibly simple to set up locally, and it can be easily run on GitHub Actions using existing packages.
Step 1: Install the npm package
Step 2: Configure Lighthouse CI
See the configuration reference for more details.
The config file should go in the root or source directory of your project.
Once it’s configured, build your site and run lhci collect to start Lighthouse CI.
You should see an output similar to this:
Step 4: Set up GitHub Actions
First, you need to authorize the LHCI GitHub App
and copy the token provided on the authorization confirmation page.
The workflow will use this token to add Lighthouse results to the “Checks” list on a pull request.
Next, follow this guide
from the GitHub docs to add the token as an encrypted secret in your GitHub repository:
Then, create a new file in your GitHub repository in the .github/workflows folder for the LHCI GitHub Actions workflow.
That’s it! You should now see Lighthouse CI results on your new commits and pull requests:
Running only on changed files
Currently, this isn’t very useful for sites with many pages;
only the pages you manually add are checked, and every added page is checked every time you run Lighthouse CI.
To solve this problem, I made use of a third-party GitHub Action and a custom Node.js script.
The first thing I did was add the changed files action to my workflow:
Then, I created a new JavaScript file in the root of the project
and added some code to separate the input by my specified separator (in this case, commas).
This is where it gets complicated. If a page was modified directly, we can easily
detect that it was changed and add it to the list of pages to run LHCI on.
However, if a deeply-nested component was changed, its parent page has to be marked as
modified.
My solution is somewhat naive, but it gets the job done in a majority of scenarios.
Now, we need a method to find which pages depend on a file.
This is a recursive process: first, find all references to the changed file in other files; then, repeat that process for all the files that referenced the changed file.
This is the main procedure, and it covers the first part of the process.
Now, we need the glue to tie it all together:
Handling dynamic route segments
The simplest solutions are often the best ones.
To handle this special case, I just replace dynamic route segment placeholders with some “example data”
which would represent the average page.
Rewriting the Lighthouse CI configuration
The last piece of the puzzle is rewriting the LHCI configuration with our list of changed pages:
Putting it all together
Here’s the full code for my solution.
GitHub Actions Workflow
Node.js Script (pages/ directory)
Modifying the script for the app/ directory
Instead of checking that each page starts with pages/, we need to check if the filename starts with app/and ends with page.tsx.
We also need to filter the dependent pages to make sure they end with page.tsx, because in the app/ directory, components can be colocated with pages.
We need to make similar changes in the code that filters our final list:
Make sure you add the app directory to the list of folders to search for references:
The list of exceptions is no longer necessary, as anything that ends with page.tsx in the app directory is a standard page.
For example: the 404 page is now not-found.tsx, and _app.tsx is now layout.tsx. Neither of them end with page.tsx, so we don’t have to include a special case for either of them.
Limitations
Modifications to components referenced in _app (pages directory) or the root or
nested layouts (app directory) do not trigger testing of every affected page.
Originally, this was a bug, but it does end up preventing a lot of unnecessary
CI time, so I am not interested in developing a fix at this time.
Some ways of depending on other files may not be recognized. Regular import and
dynamic imports do work provided that the “import” keyword is on the same line as
the referenced file name.
If you have a complex project setup or you want your workflow to be more thorough,
you can use a package like vercel/nft (Node File Trace)
to find every dependent file for a given source file.
Caching
You can add a few lines to your GitHub workflow to save dependencies and build output between runs: