perf: streamline billing panel bootstrap

Remove the redundant billing account read, cache packages for one minute, prefetch the billing chunk, and parallelize independent package reads.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
jesse-ux
2026-09-24 18:22:44 +08:00
co-authored by Claude Code
parent e1452aaea5
commit 2f53fa0171
7 changed files with 134 additions and 41 deletions
+7
View File
@@ -0,0 +1,7 @@
export function loadBillingPanel() {
return import("@/components/billing-panel").then((module) => module.BillingPanel);
}
export function prefetchBillingPanel(): void {
void loadBillingPanel();
}
@@ -0,0 +1,68 @@
import type { MembershipProduct } from "./membership";
export const PAYMENT_PACKAGES_CACHE_TTL_MS = 60_000;
export type PaymentPackagesPayload = Readonly<{
enabled: boolean;
packages: MembershipProduct[];
}>;
type CacheEntry = Readonly<{
value: PaymentPackagesPayload;
fetchedAt: number;
}>;
let cache: CacheEntry | null = null;
let inflight: Promise<PaymentPackagesPayload> | null = null;
async function loadPaymentPackages(): Promise<PaymentPackagesPayload> {
const response = await fetch("/api/payment/packages", { cache: "no-store" });
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || typeof payload !== "object") {
throw new Error("套餐支付暂时不可用,请稍后重试");
}
const record = payload as { enabled?: unknown; packages?: unknown };
return {
enabled: record.enabled === true,
packages: Array.isArray(record.packages) ? record.packages as MembershipProduct[] : [],
};
}
function refreshPaymentPackages(): Promise<PaymentPackagesPayload> {
if (inflight) return inflight;
const request = loadPaymentPackages().then((value) => {
cache = { value, fetchedAt: Date.now() };
return value;
}).finally(() => {
if (inflight === request) inflight = null;
});
inflight = request;
return request;
}
/**
* Return a fresh package response, or serve the last response while refreshing
* it in the background after the 60-second freshness window expires.
*/
export function fetchPaymentPackages(
onBackgroundRefresh?: (value: PaymentPackagesPayload) => void,
): Promise<PaymentPackagesPayload> {
if (!cache) return refreshPaymentPackages();
if (Date.now() - cache.fetchedAt < PAYMENT_PACKAGES_CACHE_TTL_MS) {
return Promise.resolve(cache.value);
}
const request = refreshPaymentPackages();
void request.then((value) => onBackgroundRefresh?.(value)).catch(() => {
// Stale package data remains usable when a background refresh fails.
});
return Promise.resolve(cache.value);
}
export function prefetchPaymentPackages(): void {
void fetchPaymentPackages().catch(() => undefined);
}
export function resetPaymentPackagesCacheForTests(): void {
cache = null;
inflight = null;
}