/** * @param {{ * initialJsGzipBytes: number, * lazyChunks: Array<{ path: string, gzipBytes: number }> * }} measurements * @param {{ initialJsGzipBytes: number, lazyChunkGzipBytes: number }} thresholds */ export function evaluateBundleBudget(measurements, thresholds) { const initialPassed = measurements.initialJsGzipBytes <= thresholds.initialJsGzipBytes; const lazyResults = measurements.lazyChunks.map((chunk) => ({ ...chunk, threshold: thresholds.lazyChunkGzipBytes, passed: chunk.gzipBytes <= thresholds.lazyChunkGzipBytes, })); return Object.freeze({ initialPassed, lazyResults: Object.freeze(lazyResults), passed: initialPassed && lazyResults.every((chunk) => chunk.passed), }); } /** * @param {{ * context?: Record, * metrics: { lcpMs: number, cls: number, namedInteractionMs: number } * }} report * @param {{ lcpMs: number, cls: number, namedInteractionMs: number }} thresholds */ export function evaluateLabBudget(report, thresholds) { const requiredContext = [ "runner", "browser", "viewport", "network", "cpu", "cache", "build", ]; const missingContext = requiredContext.filter( (field) => report.context?.[field] === undefined, ); const results = { lcp: report.metrics.lcpMs <= thresholds.lcpMs, cls: report.metrics.cls <= thresholds.cls, namedInteraction: report.metrics.namedInteractionMs <= thresholds.namedInteractionMs, }; return Object.freeze({ missingContext: Object.freeze(missingContext), results: Object.freeze(results), passed: missingContext.length === 0 && Object.values(results).every(Boolean), }); } /** @param {number[]} values */ export function percentile75(values) { if (values.length === 0) return null; const sorted = [...values].sort((left, right) => left - right); return sorted[Math.ceil(sorted.length * 0.75) - 1]; } /** * @param {{ * metrics: { p75LcpMs: number | null, p75Cls: number | null, p75InpMs: number | null }, * eligibleSamples: number * }} report * @param {{ * p75LcpMs: number, * p75Cls: number, * p75InpMs: number, * minimumEligibleSamples: number | null * }} thresholds */ export function evaluateFieldBudget(report, thresholds) { if ( thresholds.minimumEligibleSamples === null || report.eligibleSamples < thresholds.minimumEligibleSamples || Object.values(report.metrics).some((value) => value === null) ) { return Object.freeze({ status: /** @type {const} */ ("FAIL_UNVERIFIED"), passed: false, }); } const passed = /** @type {number} */ (report.metrics.p75LcpMs) <= thresholds.p75LcpMs && /** @type {number} */ (report.metrics.p75Cls) <= thresholds.p75Cls && /** @type {number} */ (report.metrics.p75InpMs) <= thresholds.p75InpMs; return Object.freeze({ status: passed ? /** @type {const} */ ("PASS") : /** @type {const} */ ("FAIL_THRESHOLD"), passed, }); }