Optimize frontend performance and add route-level web vitals tracking

This commit is contained in:
2569718930@qq.com
2026-03-18 20:38:43 +08:00
parent f0fcb1e379
commit 69f24ddc08
21 changed files with 503 additions and 1386 deletions
+2 -3
View File
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { I18nProvider } from "@/hooks/useI18n";
import { AccountCenter } from "@/components/account/AccountCenter";
import { AccountEntry } from "@/components/account/AccountEntry";
export const metadata: Metadata = {
title: "PolyWeather | Account Center",
@@ -10,8 +10,7 @@ export const metadata: Metadata = {
export default function AccountPage() {
return (
<I18nProvider>
<AccountCenter />
<AccountEntry />
</I18nProvider>
);
}
+73
View File
@@ -0,0 +1,73 @@
import { NextResponse } from "next/server";
import {
getVitalsSummary,
normalizeMetricName,
recordVitalsSample,
} from "@/lib/vitals-store";
type VitalsPayload = {
id?: string;
metric?: string;
navigationType?: string;
pathname?: string;
rating?: string;
value?: number;
};
export async function POST(request: Request) {
try {
const payload = (await request.json()) as VitalsPayload;
const metric = normalizeMetricName(payload.metric);
if (!metric) {
return NextResponse.json({ ok: false, error: "metric is required" }, { status: 400 });
}
const pathname = String(payload.pathname || "/");
const rating = String(payload.rating || "unknown");
const value = Number(payload.value);
const navigationType = String(payload.navigationType || "unknown");
const id = String(payload.id || "");
const ts = Date.now();
if (!Number.isFinite(value)) {
return NextResponse.json({ ok: false, error: "value must be finite" }, { status: 400 });
}
recordVitalsSample({
id,
metric,
navigationType,
pathname,
rating,
timestamp: ts,
value,
});
// Keep this lightweight: log for now, can be wired to a persistent sink later.
console.info(
`[vitals] metric=${metric} path=${pathname} value=${Number.isFinite(value) ? value : "NaN"} rating=${rating} nav=${navigationType} id=${id}`,
);
return NextResponse.json({ ok: true });
} catch (error) {
console.warn("[vitals] failed to parse payload", error);
return NextResponse.json({ ok: false }, { status: 400 });
}
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const targetRoute = String(searchParams.get("route") || "").trim();
const summary = getVitalsSummary();
if (!targetRoute) {
return NextResponse.json({ ok: true, ...summary });
}
return NextResponse.json({
ok: true,
generatedAt: summary.generatedAt,
route: targetRoute,
metrics: summary.routes[targetRoute] || {},
});
}
+2 -4
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { WebVitalsReporter } from "@/components/observability/WebVitalsReporter";
import "./globals.css";
export const metadata: Metadata = {
@@ -25,10 +26,6 @@ export default function RootLayout({
return (
<html lang="zh-CN" className="dark">
<head>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
@@ -38,6 +35,7 @@ export default function RootLayout({
</head>
<body className="min-h-screen font-sans antialiased">
{children}
<WebVitalsReporter />
<Analytics />
<SpeedInsights />
</body>