export interface PerformanceBudgets { routeJsBytes?: number; routeCssBytes?: number; htmlBytes?: number; imageBytes?: number; hydrationMs?: number; serverRenderMs?: number; /** Largest Contentful Paint in milliseconds. Recommended <= 2500. */ lcpMs?: number; /** Interaction to Next Paint in milliseconds. Recommended <= 200. */ inpMs?: number; /** Cumulative Layout Shift score. Recommended <= 0.1. */ cls?: number; /** Time to First Byte in milliseconds. */ ttfbMs?: number; /** Longest main-thread task in milliseconds. Recommended <= 50. */ longTaskMs?: number; /** Number of client hydration boundaries on the route. */ hydratedComponents?: number; /** Total request count for the initial navigation. */ requests?: number; } export interface PerformanceMeasurement { routeJsBytes?: number; routeCssBytes?: number; htmlBytes?: number; imageBytes?: number; hydrationMs?: number; serverRenderMs?: number; lcpMs?: number; inpMs?: number; cls?: number; ttfbMs?: number; longTaskMs?: number; hydratedComponents?: number; requests?: number; } export interface BudgetViolation { metric: keyof PerformanceBudgets; budget: number; actual: number; overBy: number; } export const recommendedWebBudgets: Readonly = Object.freeze({ lcpMs: 2500, inpMs: 200, cls: 0.1, longTaskMs: 50, routeJsBytes: 50 * 1024, routeCssBytes: 25 * 1024, }); export function checkPerformanceBudgets( budgets: PerformanceBudgets, measurement: PerformanceMeasurement, ): BudgetViolation[] { const violations: BudgetViolation[] = []; for (const metric of Object.keys(budgets) as Array) { const budget = budgets[metric]; const actual = measurement[metric]; if (budget === undefined || actual === undefined || actual <= budget) continue; violations.push({ metric, budget, actual, overBy: actual - budget }); } return violations; }