前端诊断接口设计,先统一指标和版本
性能上报接口应先统一指标口径、单位、采样规则和版本,再实现 SDK。清晰的契约能减少服务端解析分支,也让预算告警带上足够的定位上下文。
1. 原始 PerformanceEntry 不能直接作为长期接口
在早期版本中,团队曾经定义过一个极其“宽泛”的性能上报接口。开发者直接把 performance.getEntriesByType('resource') 的原始数组序列化后往后端扔:
// 导致严重返工的旧版性能上报 API (缺少类型约束与标准口径)
export interface LegacyPerformanceReport {
pageUrl: string;
metrics: any; // 致命伤:使用 any 类型,导致上报格式千奇百怪!
timestamp: number;
}
如果单位、字段名和版本没有约束,聚合查询会变得困难。应在客户端和服务端同时校验,并为协议变更保留版本字段和兼容期。
2. 确定性设计一:基于 Zod 的 Core Web Vitals 强契约数据模型
要做到接口不返工,第一步必须建立严格的数据模型与运行时 Schema 校验。
我们参照 Google Core Web Vitals 官方标准口径,使用 Zod 重新设计了确定性的性能诊断上报契约:
import { z } from "zod";
// 1. 定义 Core Web Vitals 核心性能指标数据 Schema
export const WebVitalsMetricSchema = z.object({
name: z.enum(["CLS", "FCP", "FID", "INP", "LCP", "TTFB"]),
value: z.number().min(0), // 必须为非负毫秒数(CLS 为比率数值)
rating: z.enum(["good", "needs-improvement", "poor"]),
delta: z.number().min(0),
id: z.string().uuid(), // 每次采样的唯一跟踪 ID
});
// 2. 定义性能预算 (Performance Budget) 配置契约
export const PerformanceBudgetSchema = z.object({
maxLCPMs: z.number().default(2500),
maxINPMs: z.number().default(200),
maxCLSRatio: z.number().default(0.1),
maxBundleSizeKB: z.number().default(500),
});
// 3. 全局统一上报 Payload 数据模型
export const PerformanceReportPayloadSchema = z.object({
appId: z.string().min(1),
env: z.enum(["development", "staging", "production"]),
pageUrl: z.string().url(),
metrics: z.array(WebVitalsMetricSchema),
budgetViolations: z.array(z.string()).default([]),
clientTimestamp: z.number().int().positive(),
});
export type PerformanceReportPayload = z.infer<typeof PerformanceReportPayloadSchema>;
export type PerformanceBudget = z.infer<typeof PerformanceBudgetSchema>;
探针收集后可先用 safeParse() 校验。生产环境应采样记录校验失败原因,避免大量 console 输出;服务端也必须再次校验,不能信任客户端数据。
3. 确定性设计二:结构化错误语义与预算超标告警引擎
第二步是建立清晰明确的错误语义(Error Semantics)。
当诊断探针发现页面实际指标突破了设定的性能预算(Performance Budget)时,不能仅仅静默记录,必须抛出带有明确类型标记与定位上下文的结构化异常:
export class PerformanceDiagnosticError extends Error {
public readonly code: string;
public readonly violationMetric: string;
public readonly actualValue: number;
public readonly budgetLimit: number;
constructor(
metricName: string,
actualValue: number,
budgetLimit: number,
message: string
) {
super(`[Performance Budget Exceeded] ${metricName}: ${actualValue} (Budget Limit: ${budgetLimit}) – ${message}`);
this.name = "PerformanceDiagnosticError";
this.code = "PERF_BUDGET_VIOLATION";
this.violationMetric = metricName;
this.actualValue = actualValue;
this.budgetLimit = budgetLimit;
}
}
export class PerformanceBudgetEvaluator {
private budget: PerformanceBudget;
constructor(budget: Partial<PerformanceBudget> = {}) {
this.budget = PerformanceBudgetSchema.parse(budget);
}
public evaluateMetric(metricName: "LCP" | "INP" | "CLS", value: number): void {
let limit = 0;
if (metricName === "LCP") limit = this.budget.maxLCPMs;
if (metricName === "INP") limit = this.budget.maxINPMs;
if (metricName === "CLS") limit = this.budget.maxCLSRatio;
if (value > limit) {
const error = new PerformanceDiagnosticError(
metricName,
value,
limit,
`页面当前 ${metricName} 已严重突破性能预算!`
);
console.warn(error.message);
// 可在此触发日志埋点或透传给 CI 告警通知系统
}
}
}
4. 接口设计要点
写 API 和做雕刻一样,结构定得好,后面才不需要缝缝补补。
契约明确,语义清晰,性能诊断才能真正发挥出自动守卫的作用。





