The Eleventy community is often focused on performance in a way that you don't tend to see with other tools, mostly because Eleventy seems to attract fans of keeping things lean and appreciating the fundamentals of the web (the holy trinity of HTML, CSS, and minimal JS โ in that order), and perhaps partly due to the Eleventy Leaderboards baiting everyone's competitive sides!
When I first started building my personal site I was very focused on getting a good Lighthouse score. It's dipped then recovered a few times over the years, but it has been at the point where it's something I've wanted to show off in my site's footer for a while.
At first, I did this using Zach Leatherman's <speedlify-score>
web component which uses the Eleventy Leaderboards/Speedlify as its source of data.
This worked well, but the leaderboards are only updated every so often, so it took time for improvements to show up. It also felt counter-productive to be using a third-party package with its own client-side JavaScript and CSS to report on performance metrics!
I started looking at something I could use at build time instead, and found that the PageSpeed Insights API can return the results of a Lighthouse test!
Getting data from the PageSpeed Insights API and using it in Eleventy
This isn't a full tutorial and assumes some prior knowledge of working with npm, APIs, and environment variables.
Let's take a look at how I got my Eleventy site to trigger a Lighthouse test each time it is built and to show the results in my site footer.
Get an API key and store it in an environment variable
Head to the PageSpeed Insights API's getting started guide and follow the links to get an API key.
Add the key into an environment variable (locally and on your remote environments - don't forget to do it on Netlify etc or your deployments will fail!) called PAGESPEED_API_KEY
.
Create a JavaScript data file to hold our API call
Create a lighthouse.js
file inside your Eleventy data directory.
Make sure you have installed the dotenv
and @11ty/eleventy-cache-assets
packages (or alternatives, if you prefer).
Add the script below, swapping https://example.com
for your own URL, or an environment variable representing your own URL.
require('dotenv').config();
const Cache = require('@11ty/eleventy-cache-assets');
module.exports = async function () {
const params = new URLSearchParams();
params.append('url', 'https://example.com');
params.append('key', process.env.PAGESPEED_API_KEY);
// We use the fields query string param to ask the Google API to only
// return the data we need - a score and title for each category in the
// Lighthouse test. Without this, the API returns a *lot* of data, which
// isn't the end of the world but is also unnecessary.
params.append('fields', 'lighthouseResult.categories.*.score,lighthouseResult.categories.*.title');
params.append('prettyPrint', false);
// I use the mobile strategy, but `desktop` is a valid value too.
params.append('strategy', 'mobile');
// I've not used the PWA category, but you could if it is relevant to your site.
params.append('category', 'PERFORMANCE');
params.append('category', 'ACCESSIBILITY');
params.append('category', 'BEST-PRACTICES');
params.append('category', 'SEO');
let data = await Cache(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?${params.toString()}`, {
duration: '1d',
type: 'json',
});
data = data.lighthouseResult.categories;
const getGrade = function (score) {
if (score < 0.5) {
return 'bad';
}
if (score < 0.9) {
return 'ok';
}
return 'good';
};
Object.keys(data).map(function (key) {
data[key].score = (data[key].score * 100).toFixed();
data[key].grade = getGrade(data[key].score);
});
return {
categories: data,
};
};
The function will return an object containing another object called categories
, inside of which will be your scores. I nested the scores inside of the categories
object in case we ever want to return anything else from lighthouse.js
, like some Core Web Vitals scores perhaps?
Example output
{
"categories": {
"performance": { "title": "Performance", "score": "100", "grade": "good" },
"accessibility": { "title": "Accessibility", "score": "100", "grade": "good" },
"best-practices": { "title": "Best Practices", "score": "100", "grade": "good" },
"seo": { "title": "SEO", "score": "100", "grade": "good" }
}
}
Using the data in a template
Thanks to the magic of Eleventy data files, you can now loop over the scores via lighthouse.categories
almost anywhere you like.
Most of my site uses the Eleventy Vue plugin, but my footer is still using a Nunjucks file, so here's how I displayed the scores. I'm using Tailwind here for styling, but of course, you can style the output however you like - it's your website!
<div class="flex flex-wrap items-center gap-2">
<p id="homepage-lighthouse-scores">Homepage Lighthouse scores (%):</p>
<ul
class="flex items-center space-x-1"
aria-labelledby="homepage-lighthouse-scores"
>
{% for key, result in lighthouse.categories %}
<li
class="flex items-center justify-center cursor-help rounded-full h-8 w-8 border-2 {{ 'border-green-300' if result.grade === 'good' else 'border-yellow-400' }}"
title="{{ result.title }}"
>
<span class="sr-only">{{ result.title }}: </span>{{ result.score }}
</li>
{% endfor %}
</ul>
</div>
โฆand here's how it looks!:
Room for improvement
Here are some ideas for ways to adapt this to make it your own:
- My site is basically a single giant page, so I only bother to test one URL. Perhaps you'd like to swap the data file for a template function so you can run an individual test for each page on your site by using
this.page.url
? - My colour scheme for my footer is yellow so that's the default colour I've used for my rings. Even if I got a bad Lighthouse score, the rings would still be yellow, not red. If you are getting some low scores you should probably add some extra Nunjucks code to output a red border if
result.grade === 'bad'
, otherwise the yellow colour I am using would make it look like all your grades are 'ok'!
Share this post
Top comments (1)
Thank you. I had no idea we could add SEO, A11ty to the category. I did not use this API because it was not available by default. Instead they trim it and add un-neccessary data. typical google ๐