Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created November 11, 2024 14:04
Show Gist options
  • Save mizchi/a137d2870f51ae2b1c94c48944ba615f to your computer and use it in GitHub Desktop.
Save mizchi/a137d2870f51ae2b1c94c48944ba615f to your computer and use it in GitHub Desktop.
/* # Lighthouse performance testing with n times by deno
$ deno run -A src/lhs.ts https://www.npmjs.com/package/puppeteer --times 3
{
avg: 0.45999999999999996,
times: 3,
scores: {
"first-contentful-paint": 0.03,
"first-meaningful-paint": 0.12,
"speed-index": 0.45,
"total-blocking-time": 0.53,
"max-potential-fid": 0.02,
interactive: 0.15,
"mainthread-work-breakdown": 0.98,
"bootup-time": 0.96,
"uses-rel-preconnect": 0.89,
"uses-long-cache-ttl": 0.5,
"render-blocking-resources": 0.25,
"unused-css-rules": 0.78,
"unused-javascript": 0.29,
"legacy-javascript": 0.9,
"largest-contentful-paint": 0.01,
"unminified-css": 0.9
}
}
*/
import chromeFinder from "npm:[email protected]";
import puppeteer, { Page } from "npm:[email protected]";
import Lighthouse, { Config as LhsConfig, Audit } from "npm:[email protected]";
const lhsMobileConfig: LhsConfig = {
extends: "lighthouse:default",
settings: {
onlyCategories: ["performance"],
formFactor: "mobile",
screenEmulation: {
mobile: true,
width: 412,
height: 823,
deviceScaleFactor: 2,
disabled: false,
},
throttling: {
cpuSlowdownMultiplier: 4,
throughputKbps: 1.6 * 1024,
rttMs: 150,
},
},
};
interface AuditResult extends Audit {
id: string;
score: number | null;
}
async function getLhPerf(
page: Page,
options: {
url: string;
}
): Promise<{ at: number; score: number | null; audits: Array<AuditResult> }> {
const now = Date.now();
const runnerResult = await Lighthouse(
options.url,
{
port: Number(new URL(page.browser().wsEndpoint()).port),
logLevel: "error",
output: "json",
},
lhsMobileConfig
);
if (!runnerResult) throw new Error("Lighthouse failed to run");
const perf = runnerResult.lhr.categories.performance;
const score = perf.score;
const currentAudits = Object.values(runnerResult.lhr.audits).filter(
(v) => v.score && v.score < 1
);
return {
at: now,
score,
audits: currentAudits,
};
}
const SCORE_ACCURACY = 100;
async function getLhPerfAvg(
page: Page,
options: {
url: string;
times: number;
interval: number;
}
) {
const results: number[] = [];
const uniqAudits: Map<string, number[]> = new Map();
for (let i = 0; i < options.times; i++) {
await new Promise((resolve) => setTimeout(resolve, options.interval));
const lhPerf = await getLhPerf(page, options);
lhPerf.score && results.push(lhPerf.score);
for (const audit of lhPerf.audits) {
if (!uniqAudits.has(audit.id)) {
uniqAudits.set(audit.id, []);
}
uniqAudits.get(audit.id)!.push(audit.score!);
}
}
const scores: { [key: string]: number } = {};
for (const [key, value] of uniqAudits) {
scores[key] =
Math.floor(
(SCORE_ACCURACY * value.reduce((a, b) => a + b, 0)) / value.length
) / SCORE_ACCURACY;
}
return {
avg: results.reduce((a, b) => a + b, 0) / results.length,
times: options.times,
scores,
};
}
// CLI
import { parseArgs } from "node:util";
const parsed = parseArgs({
options: {
times: {
type: "string",
short: "t",
},
interval: {
type: "string",
short: "i",
},
},
allowPositionals: true,
});
if (import.meta.main) {
let browser: puppeteer.Browser | undefined = undefined;
try {
browser = await puppeteer.launch({
headless: true,
executablePath: chromeFinder(),
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const url = parsed.positionals[0];
const page = await browser.newPage();
// await page.goto(url, { waitUntil: "networkidle2" });
const summary = await getLhPerfAvg(page, {
url,
times: Number(parsed.values.times ?? 1),
interval: Number(parsed.values.interval ?? 5000),
});
console.log(summary);
} finally {
browser?.close();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment