playbook/antigravity-awesome-skills/skills/vercel-optimize/lib/gates/cwv-poor.mjs

88 lines
3.3 KiB
JavaScript

// Thresholds are Google's "Poor" band (https://web.dev/articles/vitals): LCP p75 > 2500ms, INP > 200ms, CLS > 0.1.
// When Speed Insights isn't wired up the metrics come back empty and the gate is a no-op.
import { withRouteShapeWarnings } from '../route-normalize.mjs';
export const metadata = {
id: 'cwv_poor',
threshold: 'LCP p75>2500 OR INP p75>200 OR CLS p75>0.1, AND speed_insights count > 50',
billingDimension: 'speed-insights',
scope: 'route',
sourceCitation: 'https://web.dev/articles/vitals',
description:
'Routes where Core Web Vitals fall into Google\'s "Poor" band on real-user traffic. LCP > 2500ms, INP > 200ms, or CLS > 0.1 each hurt SEO and conversion. Surfaces one candidate per (route, metric) pair to keep recommendations focused.',
};
// Below this floor p75 is too noisy to act on.
const MIN_PER_ROUTE_SAMPLES = 50;
export function gate(signals) {
const totalSamples = sumRows(signals.metrics?.cwvCount?.rows);
if (totalSamples === 0) return [];
const countByRoute = byRoute(signals.metrics?.cwvCountByRoute?.rows);
const lcpBy = byRoute(signals.metrics?.cwvLcpByRoute?.rows);
const inpBy = byRoute(signals.metrics?.cwvInpByRoute?.rows);
const clsBy = byRoute(signals.metrics?.cwvClsByRoute?.rows);
const routes = new Set([...lcpBy.keys(), ...inpBy.keys(), ...clsBy.keys()]);
const out = [];
for (const route of routes) {
const routeSamples = countByRoute.get(route) ?? 0;
if (routeSamples < MIN_PER_ROUTE_SAMPLES) continue;
const lcp = lcpBy.get(route);
const inp = inpBy.get(route);
const cls = clsBy.get(route);
const issues = [];
if (lcp != null && lcp > 2500) issues.push({ metric: 'LCP', value: Math.round(lcp), threshold: 2500, unit: 'ms' });
if (inp != null && inp > 200) issues.push({ metric: 'INP', value: Math.round(inp), threshold: 200, unit: 'ms' });
if (cls != null && cls > 0.1) issues.push({ metric: 'CLS', value: round2(cls), threshold: 0.1, unit: '' });
if (issues.length === 0) continue;
const summary = issues.map((i) => `${i.metric}=${i.value}${i.unit}`).join(',');
out.push(withRouteShapeWarnings({
kind: metadata.id,
scope: 'route',
route,
files: [],
priority: issues.reduce((s, i) => s + ratioOverThreshold(i), 0) * 10,
confidence: 0.82,
o11ySignal: summary,
reason: 'real-user Core Web Vitals in poor band',
question: `On ${route}, ${summary}. Which client-side work (bundle weight, blocking scripts, layout shifts, hydration) is responsible, and which change would land first?`,
evidence: {
metric: 'cwv',
route,
lcpMs: lcp != null ? Math.round(lcp) : null,
inpMs: inp != null ? Math.round(inp) : null,
cls: cls != null ? round2(cls) : null,
issues,
totalSpeedInsightsSamples: totalSamples,
routeSpeedInsightsSamples: routeSamples,
},
}, signals));
}
return out;
}
function byRoute(rows) {
const m = new Map();
for (const r of rows ?? []) {
if (!r.route || r.value == null) continue;
m.set(r.route, r.value);
}
return m;
}
function sumRows(rows) {
if (!Array.isArray(rows)) return 0;
return rows.reduce((s, r) => s + (r.value ?? 0), 0);
}
function round2(n) {
return Math.round(n * 100) / 100;
}
function ratioOverThreshold(i) {
return i.value / (i.threshold || 1);
}