#!/usr/bin/env node // Worked website benchmark — CometWeb, 24 September 2026. // // Applies the method from "How to benchmark a website against competitors" // (cometweb.io/blog/how-to-benchmark-a-website) to four pages that answer the // same search intent: cometweb.io/website-audit and three pages that appeared in // the US top 10 for both "free website audit tool" and "website audit" on the // day of the run (see serp-2026-09-24.json). // // Per URL: // * 1 plain GET first (pre-flight: status, security headers,
checks); // * 5 Lighthouse runs (mobile preset, simulated throttling, identical settings), // rounds interleaved and rotated so no site always goes first or last, // a fresh headless Chromium profile per run, a pause between runs; // Nothing else is requested from third-party sites. No crawling. // // Usage: // npm i lighthouse@13.5.0 chrome-launcher@1 // CHROME_PATH="/path/to/playwright/chromium" node benchmark.mjs // Writes lighthouse-runs.json, headers-and-head.json and summary.json next to itself. import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import * as chromeLauncher from 'chrome-launcher'; import lighthouse from 'lighthouse'; const here = path.dirname(fileURLToPath(import.meta.url)); const RUNS = Number(process.env.RUNS || 5); const PAUSE_MS = Number(process.env.PAUSE_MS || 8000); const CHROME_PATH = process.env.CHROME_PATH; if (!CHROME_PATH) throw new Error('Set CHROME_PATH to the Playwright Chromium binary'); const TARGETS = [ { id: 'cometweb', label: 'CometWeb', url: 'https://cometweb.io/website-audit', role: 'own page' }, // SEOptimer (mean position 2.0) was selected first but answered HTTP 525 and // Lighthouse NO_FCP at 07:07-07:10 UTC; the pre-registered reserve replaces it. // See excluded-seoptimer.json. { id: 'thehoth', label: 'The HOTH', url: 'https://www.thehoth.com/seo-audit-tool/', role: 'SERP' }, { id: 'seositecheckup', label: 'SEO Site Checkup', url: 'https://seositecheckup.com/', role: 'SERP' }, { id: 'rankmath', label: 'Rank Math', url: 'https://rankmath.com/tools/seo-analyzer/', role: 'SERP (reserve)' } ]; const ONLY = process.env.ONLY ? process.env.ONLY.split(',') : null; if (ONLY) TARGETS.splice(0, TARGETS.length, ...TARGETS.filter((t) => ONLY.includes(t.id))); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const median = (xs) => { const s = xs.filter((x) => typeof x === 'number' && Number.isFinite(x)).sort((a, b) => a - b); if (!s.length) return null; const m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; }; function extract(lhr) { const a = lhr.audits; const num = (id) => (a[id] && typeof a[id].numericValue === 'number' ? a[id].numericValue : null); const requests = a['network-requests']?.details?.items ?? []; const firstPartyOrigins = new Set(); const entities = lhr.entities ?? []; const fp = entities.find((e) => e.isFirstParty); const fpOrigins = fp ? fp.origins : []; for (const o of fpOrigins) firstPartyOrigins.add(o); let fpBytes = 0; let tpBytes = 0; for (const r of requests) { let origin = null; try { origin = new URL(r.url).origin; } catch { /* data: etc. */ } const bytes = r.transferSize || 0; if (origin && firstPartyOrigins.has(origin)) fpBytes += bytes; else tpBytes += bytes; } const failing = (cat) => (lhr.categories[cat]?.auditRefs ?? []) .filter((ref) => ref.weight > 0 && a[ref.id] && a[ref.id].score !== null && a[ref.id].score < 1) .map((ref) => ref.id); return { requestedUrl: lhr.requestedUrl, finalDisplayedUrl: lhr.finalDisplayedUrl, fetchTime: lhr.fetchTime, runWarnings: lhr.runWarnings, runtimeError: lhr.runtimeError ?? null, scores: Object.fromEntries( Object.entries(lhr.categories).map(([k, v]) => [k, v.score === null ? null : Math.round(v.score * 100)]) ), metrics: { fcp_ms: num('first-contentful-paint'), lcp_ms: num('largest-contentful-paint'), tbt_ms: num('total-blocking-time'), cls: num('cumulative-layout-shift'), si_ms: num('speed-index'), tti_ms: num('interactive') }, transfer: { total_bytes: num('total-byte-weight'), requests: requests.length, first_party_bytes: fpBytes, third_party_bytes: tpBytes }, failing_seo_audits: failing('seo'), failing_accessibility_audits: failing('accessibility'), failing_best_practices_audits: failing('best-practices'), environment: { lighthouseVersion: lhr.lighthouseVersion, hostUserAgent: lhr.environment?.hostUserAgent, networkUserAgent: lhr.environment?.networkUserAgent, benchmarkIndex: lhr.environment?.benchmarkIndex, formFactor: lhr.configSettings?.formFactor, throttlingMethod: lhr.configSettings?.throttlingMethod, throttling: lhr.configSettings?.throttling, screenEmulation: lhr.configSettings?.screenEmulation } }; } async function lighthouseRun(url) { const chrome = await chromeLauncher.launch({ chromePath: CHROME_PATH, chromeFlags: ['--headless=new', '--no-first-run', '--disable-extensions', '--no-default-browser-check'] }); try { const result = await lighthouse(url, { port: chrome.port, output: 'json', logLevel: 'error', onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'] }); return extract(result.lhr); } finally { await chrome.kill(); } } const SECURITY_HEADERS = [ 'strict-transport-security', 'content-security-policy', 'x-content-type-options', 'x-frame-options', 'referrer-policy', 'permissions-policy' ]; async function headersAndHead(target) { const started = new Date().toISOString(); const res = await fetch(target.url, { redirect: 'follow', headers: { 'user-agent': 'Mozilla/5.0 (compatible; CometWeb-benchmark/1.0; one request per page; +https://cometweb.io/blog/how-to-benchmark-a-website)', accept: 'text/html' } }); const html = await res.text(); const h = Object.fromEntries(SECURITY_HEADERS.map((k) => [k, res.headers.get(k)])); const head = html.slice(0, html.search(/<\/head>/i) > 0 ? html.search(/<\/head>/i) : 200000); const attr = (re) => (head.match(re) || [])[1] ?? null; const jsonLdTypes = []; for (const m of html.matchAll(/