Unlighthouse blogpost + version in docker
This commit is contained in:
parent
753a39e13c
commit
66c9220468
7 changed files with 269 additions and 22 deletions
|
@ -73,6 +73,8 @@ jobs:
|
|||
file: Dockerfile.reports
|
||||
push: true
|
||||
tags: forgejo.neshweb.net/firq/firq-dev-website-unlighthouse:latest
|
||||
build-args: |
|
||||
version=${{ inputs.containertag }}
|
||||
|
||||
auto-deploy-dockge:
|
||||
needs: [ build-site ]
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
FROM forgejo.neshweb.net/ci-docker-images/website-serve:2 AS runtime
|
||||
|
||||
ARG version
|
||||
ENV version=0.0.1
|
||||
|
||||
ADD reports /public
|
||||
|
||||
EXPOSE 8081
|
||||
CMD serve --listen 8081 --no-clipboard /public
|
||||
CMD echo $version && serve --listen 8081 --no-clipboard /public
|
||||
|
|
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@firq/fgosite",
|
||||
"version": "0.2.0-pre.92",
|
||||
"version": "0.2.0-pre.93",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@firq/fgosite",
|
||||
"version": "0.2.0-pre.92",
|
||||
"version": "0.2.0-pre.93",
|
||||
"dependencies": {
|
||||
"@astro-community/astro-embed-youtube": "^0.5.6",
|
||||
"@astrojs/check": "^0.9.4",
|
||||
|
@ -15,10 +15,10 @@
|
|||
"@fontsource-variable/work-sans": "^5.2.5",
|
||||
"astro": "^5.6.1",
|
||||
"astro-meta-tags": "^0.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"iconoir": "^7.10.1",
|
||||
"postcss-preset-env": "^10.1.5",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unlighthouse": "^0.16.3"
|
||||
|
@ -4682,9 +4682,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -4699,12 +4699,13 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.23.3",
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"browserslist": "^4.24.4",
|
||||
"caniuse-lite": "^1.0.30001702",
|
||||
"fraction.js": "^4.3.7",
|
||||
"normalize-range": "^0.1.2",
|
||||
"picocolors": "^1.0.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -12744,9 +12745,10 @@
|
|||
"integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@firq/fgosite",
|
||||
"type": "module",
|
||||
"version": "0.2.0-pre.92",
|
||||
"version": "0.2.0-pre.93",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
|
@ -18,10 +18,10 @@
|
|||
"@fontsource-variable/work-sans": "^5.2.5",
|
||||
"astro": "^5.6.1",
|
||||
"astro-meta-tags": "^0.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"iconoir": "^7.10.1",
|
||||
"postcss-preset-env": "^10.1.5",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unlighthouse": "^0.16.3"
|
||||
|
|
|
@ -5,9 +5,9 @@ description: 'A handful of observations from the cernunnos fight'
|
|||
author: 'Requiem & Firq'
|
||||
tags: ['fgo', 'lostbelt 6', 'cernunnos']
|
||||
---
|
||||
import thumbnail_firq from "../../assets/thumbnails/WrHudtdfivA.jpg"
|
||||
import thumbnail_requiem from "../../assets/thumbnails/O1f-go7uJQM.jpg"
|
||||
import YoutubeEmbed from "../../components/youtubeEmbed.astro"
|
||||
import thumbnail_firq from "@assets/thumbnails/WrHudtdfivA.jpg"
|
||||
import thumbnail_requiem from "@assets/thumbnails/O1f-go7uJQM.jpg"
|
||||
import YoutubeEmbed from "@components/youtubeEmbed.astro"
|
||||
|
||||
## Foreword
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ description: 'Blog post talking about instant death in FGO and how you can take
|
|||
author: 'Firq'
|
||||
tags: ['fgo', 'games']
|
||||
---
|
||||
import thumbnail from "../../assets/thumbnails/UwbNp_dB_VU.jpg"
|
||||
import YoutubeEmbed from "../../components/youtubeEmbed.astro"
|
||||
import thumbnail from "@assets/thumbnails/UwbNp_dB_VU.jpg"
|
||||
import YoutubeEmbed from "@components/youtubeEmbed.astro"
|
||||
|
||||
> **Disclaimer**<br/>
|
||||
> While writing this, Requiem and I faced a bit of a challenge concerning death rate calculations. Case in point is the passive "Item Construction"
|
||||
|
|
240
src/data/blog/unlighthouse.md
Normal file
240
src/data/blog/unlighthouse.md
Normal file
|
@ -0,0 +1,240 @@
|
|||
---
|
||||
title: 'Unlighthouse - Easy Website Testing'
|
||||
pubDate: 2025-04-12
|
||||
description: 'How I am automatically testing my site deployments using unlighthouse'
|
||||
author: 'Firq'
|
||||
tags: [ 'unlighthouse', 'coding' ]
|
||||
---
|
||||
|
||||
Since one of the first iterations of this site, I was using Google Lighthouse to benchmark my results - and it really paid off at times. Missing ARIA roles, bad color contrast, the fact that the default Youtube embed is actually not that great - all of this was exposed by Lighthouse over time.
|
||||
|
||||
How this whole thing evolved and how I am currently using Unlighthouse to make all of this run even smoother will be covered in this article.
|
||||
|
||||
## Unlighthouse
|
||||
|
||||
First of all, I want to give a big shout-out to <a href="https://harlanzw.com/" target="_blank" rel="noopener noreferrer" >Harlan Wilton (harlan-zw)</a> for making `unlighthouse` - it makes analyzing larges sites so much easier. The package can be installed from `npm` and enables you to easily run Lighthouse for each of your subpages - even from a CI environment. This is a huge improvement given that the best alternative is the `Lighthouse` docker container that is provided by GitLab.
|
||||
|
||||
For ease of use, I also build my own version of that GitLab docker container, but with unlighthouse instead. This allows me to easily use it in other CI jobs, with pinned dependencies and resistance against random changes. However, it also requires me to regularly check for updated chromium versions, as this is usually what causes issues when updating the container.
|
||||
|
||||
The container is <a href="https://forgejo.neshweb.net/CI-Docker-Images/unlighthouse" target="_blank" rel="noopener noreferrer" >available from here</a> and can be pulled with the following url:
|
||||
|
||||
```shell
|
||||
docker pull forgejo.neshweb.net/ci-docker-images/unlighthouse:latest
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Dockerfile for the unlighthouse container</summary>
|
||||
|
||||
```dockerfile
|
||||
FROM node:22.14-bookworm
|
||||
|
||||
LABEL authorname="firq"
|
||||
LABEL description="unlighthouse container for ci-based lighthouse testing"
|
||||
WORKDIR /unlighthouse
|
||||
|
||||
ENV CHROMIUM_VERSION="135.0.7049.84-1~deb12u1"
|
||||
ENV UNLIGHTHOUSE_VERSION="0.16.3"
|
||||
ENV NODE_ENV='production'
|
||||
|
||||
# Update path so executable can be run globally
|
||||
ENV PATH="/unlighthouse/node_modules/.bin:${PATH}"
|
||||
|
||||
RUN apt-get update && apt-get -y install --no-install-recommends chromium=${CHROMIUM_VERSION} procps && rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install @unlighthouse/cli@${UNLIGHTHOUSE_VERSION} puppeteer
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Automation with Forgejo
|
||||
|
||||
However, running `unlighthouse` manually after each page build is too much of a hassle - so it was time to automate it. For that reason, I created a corresponding actions job that would run the checks each time a preview version of the site was being built.
|
||||
|
||||
In the beginning, I would run the test against the actual deployed instance, but with time it became increasingly obvious that this would be inefficient, mainly because I would run this using two different tags (`x.x.x-pre.x` and `x.x.x-ulh.x`) to trigger the corresponding CI runs.
|
||||
|
||||
Regarding this, I decided to instead run everything using a service container. This means that the `unlighthouse` job runs in the main job task, while the site is running as a service container in parallel. This allows access to the container using an alias (`website`) instead of a URL or IP address. (Note: The yaml is a bit reduced, meaning lines that only provide visual context in the UI are removed).
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
unlighthouse:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: forgejo.neshweb.net/ci-docker-images/unlighthouse:0.16.3
|
||||
services:
|
||||
website:
|
||||
image: forgejo.neshweb.net/firq/firq-dev-website:${{ inputs.containertag }}
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- run: |
|
||||
while [ "$(curl -o /dev/null -s -w '%{http_code}' http://website:8081)" -ne 200 ];
|
||||
do echo "Waiting...";
|
||||
sleep 5;
|
||||
done;
|
||||
- run: unlighthouse-ci --site "http://website:8081"
|
||||
- run: find ./unlighthouse-reports -type f | xargs sed -i "s|http://website:8081|https://preview.firq.dev|g";
|
||||
```
|
||||
|
||||
So what is happening here? The first few lines are just there for configuring the runner and starting up the service-container. The container tag is passed from the upstream pipeline, making this setup a lot more flexible. Afterwards, two things happen: The code in the git repo gets checked out, and the job gets put on hold until the service-container is available. This is necessary, as sometimes the service container takes a few seconds to start up.
|
||||
|
||||
Afterwards, `unlighthouse` runs against the given site inside the service-container, using the local config for any other settings. After the run concludes, the artifacts inside the `unlighthouse-reports` folder get search-and-replaced once to change the url from the service-container to the preview site. This is mainly for visual consistency, but also has the benefit of making the "Check with PageSpeedInsights" button work.
|
||||
|
||||
To enable this to work more fluently, I decided to run the `unlighthouse` CI as a downstream pipeline, which was triggered by one of the CI jobs after the `build-and-push` step concludes. This also allows me to easily pass the new container name to the downstream pipeline for usage.
|
||||
|
||||
```yaml
|
||||
run-unlighthouse:
|
||||
needs: [ build-site ]
|
||||
if: success()
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Launch workflow
|
||||
run: |
|
||||
payload="{\"ref\": \"${GITHUB_REF_NAME}\", \"inputs\": { \"containertag\": \"${GITHUB_REF_NAME}\" }}"
|
||||
curl -X "POST" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-d "${payload}" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/workflows/unlighthouse.yml/dispatches" -v
|
||||
```
|
||||
|
||||
And with that, the general CI setup concludes. I won't go into too much detail about how the deployment of the reports works, as it is similar to the main site. If you are interested, see <a href="https://forgejo.neshweb.net/Firq/firq-dev-website/src/branch/dev" target="_blank" rel="noopener noreferrer" >the repository linked here</a> for further details.
|
||||
|
||||
## Fine-tuning the config
|
||||
|
||||
After setting everything up, it took some good time until I managed to fine-tune the configuration that unlighthouse uses. Don't get me wrong, running with the defaults would probably also work, but from my experience the defaults are set so strict that a perfect score is unobtainable - even if for example <a href="https://pagespeed.web.dev/" target="_blank" rel="noopener noreferrer" >PageSpeedInsights</a> doesn't see any issues.
|
||||
|
||||
My strategy for the config was to copy the settings that PageSpeedInsights uses, as this seemed like a good baseline for testing. This means that the Lighthouse settings look like this:
|
||||
|
||||
```typescript
|
||||
lighthouseOptions: {
|
||||
throttlingMethod: 'devtools',
|
||||
throttling: {
|
||||
cpuSlowdownMultiplier: 4,
|
||||
requestLatencyMs: 150,
|
||||
downloadThroughputKbps: 1638.4,
|
||||
uploadThroughputKbps: 1638.4,
|
||||
},
|
||||
screenEmulation: { width: 412, height: 823, deviceScaleFactor: 1.75 },
|
||||
skipAudits: [ 'is-on-https', 'redirects-http', 'uses-http2' ],
|
||||
}
|
||||
```
|
||||
|
||||
As mentioned previously, as I am using a docker network, I can't easily use HTTPS and HTTP/2. Given that, I decided to disable the corresponding audits, as I know for a fact that this always works for the real deployment.
|
||||
|
||||
The other configuration steps are for Puppeteer, which handles the simulation of the site in a CI environment. This is really straightforward:
|
||||
|
||||
```typescript
|
||||
puppeteerOptions: {
|
||||
args: [ '--no-sandbox', '--disable-setuid-sandbox' ]
|
||||
},
|
||||
puppeteerClusterOptions: {
|
||||
maxConcurrency: 1
|
||||
},
|
||||
```
|
||||
|
||||
`maxConcurrency` ensures that there is only one inspection task running at the time, as having multiple run in parallel results in some weird situations where the report fails to correctly generate.
|
||||
|
||||
The rest of the configuration is for `unlighthouse` itself, regarding both the scanner and the ci environment, as well as the configuration of outputs and sites to check
|
||||
|
||||
```typescript
|
||||
ci: {
|
||||
budget: 50,
|
||||
buildStatic: true,
|
||||
},
|
||||
scanner: {
|
||||
sitemap: true,
|
||||
dynamicSampling: false,
|
||||
samples: 3,
|
||||
},
|
||||
outputPath: 'unlighthouse-reports',
|
||||
cache: true,
|
||||
urls
|
||||
```
|
||||
|
||||
However, there is one interesting entry there: `urls`. This is a dynamic list of URLs generated from the sitemap, which happens earlier in the config (what a blessing `export default async` config-files are). This is necessary, as scraping the site from the sitemap directly is not working in my environment (the crawler finds `https://preview.firq.dev`, while I need `http://website:8081` for it to run). After some digging, I raised that usecase with Harlan, who proceeded to enable me to do exactly that in no time (thank you again for doing this - <a href="https://github.com/harlan-zw/unlighthouse/issues/248" target="_blank" rel="noopener noreferrer" >see issue 248 here</a>)
|
||||
|
||||
This is where the magic snippet comes in, which 1. fetches the sitemap, 2. replaces the URLs and 3. fetches each of the URLs once to warm up the `serve` webserver to ensure that the server-caching correctly works (improves the performance by a lot).
|
||||
|
||||
```typescript
|
||||
// 1. fetch sitemap
|
||||
const sitemap = await (await fetch('http://website:8081/sitemap-0.xml')).text();
|
||||
|
||||
// 2. replace URLs
|
||||
const urls = sitemap.match(/<loc>(.*?)<\/loc>/g)!.map(
|
||||
(loc) => loc.replace(/<\/?loc>/g, '').replace(/https:\/\/firq.dev/g, 'http://website:8081')
|
||||
);
|
||||
|
||||
// 3. warm up serve
|
||||
for (const url of urls) { await fetch(url) };
|
||||
```
|
||||
|
||||
Afterwards, `urls` can be used in the config to provide `unlighthouse` with a list of urls to check - already replaced and modified to work with the docker network.
|
||||
|
||||
<details>
|
||||
<summary>View the whole config here</summary>
|
||||
|
||||
(It's also really nice that unlighthouse provides type hints for the config, makes figuring out where goes what a lot easier).
|
||||
|
||||
```typescript
|
||||
import type { UserConfig } from 'unlighthouse'
|
||||
|
||||
export default async (): Promise<UserConfig> => {
|
||||
/* fetch sitemap from debug container */
|
||||
const sitemap = await (await fetch('http://website:8081/sitemap-0.xml')).text();
|
||||
|
||||
/* format URLs to work with debug container */
|
||||
const urls = sitemap.match(/<loc>(.*?)<\/loc>/g)!.map(
|
||||
(loc) => loc.replace(/<\/?loc>/g, '').replace(/https:\/\/firq.dev/g, 'http://website:8081')
|
||||
);
|
||||
|
||||
/* ensure serve is already "warm", preventing startup lag that reduces performance */
|
||||
for (const url of urls) { await fetch(url) };
|
||||
|
||||
/* actual config */
|
||||
return {
|
||||
lighthouseOptions: {
|
||||
throttlingMethod: 'devtools',
|
||||
throttling: {
|
||||
cpuSlowdownMultiplier: 4,
|
||||
requestLatencyMs: 150,
|
||||
downloadThroughputKbps: 1638.4,
|
||||
uploadThroughputKbps: 1638.4,
|
||||
},
|
||||
screenEmulation: {
|
||||
width: 412,
|
||||
height: 823,
|
||||
deviceScaleFactor: 1.75,
|
||||
},
|
||||
skipAudits: [ 'is-on-https', 'redirects-http', 'uses-http2' ],
|
||||
},
|
||||
puppeteerOptions: {
|
||||
args: [ '--no-sandbox', '--disable-setuid-sandbox' ],
|
||||
},
|
||||
puppeteerClusterOptions: {
|
||||
maxConcurrency: 1
|
||||
},
|
||||
ci: {
|
||||
budget: 50,
|
||||
buildStatic: true,
|
||||
},
|
||||
scanner: {
|
||||
sitemap: true,
|
||||
dynamicSampling: false,
|
||||
samples: 3,
|
||||
},
|
||||
outputPath: 'unlighthouse-reports',
|
||||
cache: true,
|
||||
urls
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## What's next?
|
||||
|
||||
After setting this whole thing up over the course of multiple months, with a variety of issues and shortcomings along the way, I hope that this is now ready for good.
|
||||
I will, however, write a Forgejo Action for me to reuse in the future, as this would enable me to easily test other sites with the same concept.
|
||||
|
||||
If you want to check out the whole thing in action, check out <a href="https://forgejo.neshweb.net/Firq/firq-dev-website/src/branch/dev" target="_blank" rel="noopener noreferrer" >the website repository here </a>. In addition, you can find the reports that get generated <a href="unlighthouse.firq.dev" target="_blank" rel="noopener noreferrer" >here at unlighthouse.firq.dev</a>
|
||||
|
||||
Anyway, I wish I gave you an interesting insight in how Unlighthouse ensures good site quality - and how YOU can also profit from website testing in your CI.
|
Loading…
Add table
Reference in a new issue