perf: reduce site config refresh overhead
This commit is contained in:
@@ -32,7 +32,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
|
||||
| Footer content missing or not Markdown-rendered | `src/components/site-footer.tsx`, `src/components/site-policy-page.tsx`, `src/lib/site-config.ts`, `src/app/api/site-config/route.ts` | Config response fields, Markdown renderer, fallback defaults, PUT persistence. |
|
||||
| Policy pages start mid-page after navigation | `src/components/site-policy-page.tsx`, `src/app/about/page.tsx`, `src/app/terms/page.tsx`, `src/app/privacy/page.tsx`, `src/app/help/page.tsx` | Scroll reset behavior and shared policy page wrapper. |
|
||||
| Site name/logo/favicon not updating | `src/components/site-config-sync.tsx`, `src/components/site-brand.tsx`, `src/app/api/site-config/route.ts`, `src/lib/local-storage.ts` | `site_config` row, base64 image save, generated `/api/local-storage/*` URL. |
|
||||
| Clicking navbar between home/create/gallery/profile feels slow while server-side route TTFB is normal | `src/lib/site-config.ts`, `src/components/navbar.tsx`, `src/components/site-brand.tsx`, `src/components/site-footer.tsx`, `src/app/page.tsx` | Compare production local route timings with real browser navigation. If pages and `/api/site-config` are fast on the server but browser clicks still feel delayed, check whether multiple `useSiteConfig()` consumers are issuing duplicate concurrent config requests, and confirm the shared snapshot plus in-flight request dedupe in `src/lib/site-config.ts` is present. Home is naturally heavier than other public routes because its prerendered HTML/RSC is larger. |
|
||||
| Clicking navbar between home/create/gallery/profile feels slow while server-side route TTFB is normal | `src/lib/site-config.ts`, `src/app/api/site-config/route.ts`, `src/components/navbar.tsx`, `src/components/site-brand.tsx`, `src/components/site-footer.tsx`, `src/app/page.tsx` | Compare production local route timings with real browser navigation. If pages and `/api/site-config` are fast on the server but browser clicks still feel delayed, check whether multiple `useSiteConfig()` consumers are issuing duplicate concurrent config requests, and confirm the shared snapshot, 5-minute fresh-cache network skip, and in-flight request dedupe in `src/lib/site-config.ts` are present. Also confirm `/api/site-config` is not running schema/default compatibility DDL on every GET; it should cache that check once per server process and retry only after failure. Home is naturally heavier than other public routes because its prerendered HTML/RSC is larger. |
|
||||
| Console reports CSP blocking `https://fonts.googleapis.cn/...` | `src/app/globals.css`, `src/proxy.ts` | `globals.css` imports Noto Serif SC from `fonts.googleapis.cn`; CSP must allow that stylesheet domain in `style-src` and the matching font CDN in `font-src`. |
|
||||
| Page content leaves large unused horizontal margins, or wide screens look like the UI was simply enlarged | `src/components/app-shell.tsx`, `src/components/navbar.tsx`, `src/components/site-footer.tsx`, page-level wrappers under `src/app/*/page.tsx`, `src/components/site-policy-page.tsx` | The viewport/background can be `w-full`, but product content should keep the original component scale and readable containers such as `max-w-7xl`, `max-w-4xl`, or `max-w-3xl`. Do not fix this by removing all max widths or scaling controls up on wide monitors. |
|
||||
| Scrollbars look native, stay visible when idle, or do not match glass UI in dialogs/pages | `src/app/globals.css`, `src/components/app-shell.tsx` | Global scrollbar styling is hidden by default and becomes visible only while wheel/touch scrolling through the `scrollbars-visible` class on `<html>`. `globals.css` owns both the hidden state and the rounded glass visible state for light/dark themes; `app-shell` owns the short-lived wheel/touch listener. Avoid adding one-off scrollbar styles to individual components unless there is a real exception. |
|
||||
|
||||
@@ -13,7 +13,7 @@ Use this document to jump directly to code before broad searching.
|
||||
| Navbar | `src/components/navbar.tsx`, `src/components/site-brand.tsx` | Navigation, brand display, auth-aware links. User-facing nav should not include removed feature routes. Logged-in desktop/mobile user buttons should show `AuthUser.avatarUrl` first and fall back to the display nickname initial only when the avatar is missing or fails to load. |
|
||||
| Footer | `src/components/site-footer.tsx` | Uses site config for policy/help/about links and filing text; footer background spans browser width while inner content keeps the original `max-w-7xl` scale. |
|
||||
| Announcement popup | `src/components/announcement-popup.tsx`, `src/app/api/announcements/route.ts`, `src/app/globals.css` | Frontend popup behavior plus backend announcement CRUD. Desktop dialog is intentionally wide (`max-w-5xl`) for long Markdown notices; scrollbar styling is inherited from the global glass scrollbar rules. |
|
||||
| Site config sync | `src/components/site-config-sync.tsx`, `src/lib/site-config.ts`, `src/app/api/site-config/route.ts` | Site name, tab title, logo, favicon, policy Markdown, filing, membership switch. `useSiteConfig()` keeps a shared browser snapshot and reuses the in-flight `/api/site-config` refresh so global consumers such as navbar, footer, site-brand, and policy pages do not each create their own concurrent config request during navigation. |
|
||||
| Site config sync | `src/components/site-config-sync.tsx`, `src/lib/site-config.ts`, `src/app/api/site-config/route.ts` | Site name, tab title, logo, favicon, policy Markdown, filing, membership switch. `useSiteConfig()` keeps a shared browser snapshot, skips network refresh while that snapshot is still within the 5-minute TTL, and reuses the in-flight `/api/site-config` refresh so global consumers such as navbar, footer, site-brand, and policy pages do not each create their own concurrent config request during navigation. The API route runs legacy schema/default compatibility checks once per server process and retries on failure. |
|
||||
| Visit tracking | `src/components/visit-tracker.tsx`, `src/app/api/site-stats/route.ts` | Public visit counter. |
|
||||
| Security headers and iframe embedding | `src/proxy.ts`, `.env.example` | CSP is set in the Next proxy. `frame-ancestors` controls which external platforms may embed MiaoJing in an iframe; `MIAOJING_FRAME_ANCESTORS` can override the default self + mozheAPI allowlist. When external ancestors are allowed, do not send `X-Frame-Options: SAMEORIGIN`, because it blocks third-party iframes. |
|
||||
|
||||
|
||||
@@ -35,4 +35,22 @@ await runTest('site config hook uses a shared snapshot for instant repeated moun
|
||||
assert.match(source, /siteConfigSnapshot = config;/);
|
||||
});
|
||||
|
||||
await runTest('site config hook skips fresh-cache network refreshes on route remounts', () => {
|
||||
const source = read('src/lib/site-config.ts');
|
||||
assert.match(source, /let siteConfigSnapshotTimestamp = 0;/);
|
||||
assert.match(source, /function isSiteConfigSnapshotFresh\(\): boolean/);
|
||||
assert.match(source, /if \(isSiteConfigSnapshotFresh\(\)\) \{/);
|
||||
assert.match(source, /siteConfigSnapshotTimestamp = Date\.now\(\);/);
|
||||
});
|
||||
|
||||
await runTest('site config API caches schema compatibility checks after startup', () => {
|
||||
const source = read('src/app/api/site-config/route.ts');
|
||||
assert.match(source, /let siteConfigColumnsReady = false;/);
|
||||
assert.match(source, /let siteConfigColumnsPromise: Promise<void> \| null = null;/);
|
||||
assert.match(source, /async function ensureSiteConfigColumnsOnce/);
|
||||
assert.match(source, /if \(siteConfigColumnsReady\) return;/);
|
||||
assert.match(source, /siteConfigColumnsReady = true;/);
|
||||
assert.match(source, /await ensureSiteConfigColumnsOnce\(client\);/);
|
||||
});
|
||||
|
||||
if (process.exitCode) process.exit(process.exitCode);
|
||||
|
||||
@@ -41,6 +41,9 @@ type SiteConfigRow = {
|
||||
log_retention_days?: number | null;
|
||||
};
|
||||
|
||||
let siteConfigColumnsReady = false;
|
||||
let siteConfigColumnsPromise: Promise<void> | null = null;
|
||||
|
||||
async function ensureSiteConfigColumns(client: Awaited<ReturnType<typeof getDbClient>>) {
|
||||
await client.query('ALTER TABLE site_config ADD COLUMN IF NOT EXISTS membership_enabled BOOLEAN NOT NULL DEFAULT TRUE');
|
||||
await client.query("ALTER TABLE site_config ADD COLUMN IF NOT EXISTS terms_of_service TEXT NOT NULL DEFAULT ''");
|
||||
@@ -60,6 +63,20 @@ async function ensureSiteConfigColumns(client: Awaited<ReturnType<typeof getDbCl
|
||||
await client.query("UPDATE site_config SET help_center = $1 WHERE help_center = ''", [DEFAULT_HELP_CENTER]);
|
||||
}
|
||||
|
||||
async function ensureSiteConfigColumnsOnce(client: Awaited<ReturnType<typeof getDbClient>>) {
|
||||
if (siteConfigColumnsReady) return;
|
||||
if (!siteConfigColumnsPromise) {
|
||||
siteConfigColumnsPromise = ensureSiteConfigColumns(client)
|
||||
.then(() => {
|
||||
siteConfigColumnsReady = true;
|
||||
})
|
||||
.finally(() => {
|
||||
siteConfigColumnsPromise = null;
|
||||
});
|
||||
}
|
||||
await siteConfigColumnsPromise;
|
||||
}
|
||||
|
||||
function normalizeResponse(data?: SiteConfigRow | null) {
|
||||
return {
|
||||
siteName: data?.site_name || DEFAULT_RESPONSE.siteName,
|
||||
@@ -120,7 +137,7 @@ export async function GET() {
|
||||
try {
|
||||
const client = await getDbClient();
|
||||
try {
|
||||
await ensureSiteConfigColumns(client);
|
||||
await ensureSiteConfigColumnsOnce(client);
|
||||
const result = await client.query(
|
||||
'SELECT site_name, site_tab_title, logo_url, favicon_url, membership_enabled, terms_of_service, privacy_policy, about_us, help_center, filing_info, filing_url, public_security_filing_info, public_security_filing_url, redeem_code_mall_url, log_retention_days FROM site_config WHERE id = 1'
|
||||
);
|
||||
@@ -168,7 +185,7 @@ export async function PUT(request: NextRequest) {
|
||||
|
||||
const client = await getDbClient();
|
||||
try {
|
||||
await ensureSiteConfigColumns(client);
|
||||
await ensureSiteConfigColumnsOnce(client);
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
@@ -49,6 +49,7 @@ interface CachedConfig {
|
||||
}
|
||||
|
||||
let siteConfigSnapshot: SiteConfig | null = null;
|
||||
let siteConfigSnapshotTimestamp = 0;
|
||||
let inFlightSiteConfigRequest: Promise<SiteConfig | null> | null = null;
|
||||
|
||||
function normalizeSiteConfig(data?: Partial<SiteConfig> | null): SiteConfig {
|
||||
@@ -77,6 +78,7 @@ function getCachedConfig(): SiteConfig | null {
|
||||
if (!raw) return null;
|
||||
const cached: CachedConfig = JSON.parse(raw);
|
||||
if (Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
siteConfigSnapshotTimestamp = cached.timestamp;
|
||||
return normalizeSiteConfig(cached.data);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
@@ -92,10 +94,15 @@ function setCachedConfig(config: SiteConfig) {
|
||||
|
||||
function publishConfig(config: SiteConfig) {
|
||||
siteConfigSnapshot = config;
|
||||
siteConfigSnapshotTimestamp = Date.now();
|
||||
setCachedConfig(config);
|
||||
window.dispatchEvent(new CustomEvent(EVENT_KEY, { detail: config }));
|
||||
}
|
||||
|
||||
function isSiteConfigSnapshotFresh(): boolean {
|
||||
return Boolean(siteConfigSnapshot) && Date.now() - siteConfigSnapshotTimestamp < CACHE_TTL;
|
||||
}
|
||||
|
||||
function getInitialSiteConfig(): { config: SiteConfig; loaded: boolean } {
|
||||
if (siteConfigSnapshot) return { config: siteConfigSnapshot, loaded: true };
|
||||
const cached = getCachedConfig();
|
||||
@@ -107,6 +114,9 @@ function getInitialSiteConfig(): { config: SiteConfig; loaded: boolean } {
|
||||
}
|
||||
|
||||
function fetchFreshSiteConfig(): Promise<SiteConfig | null> {
|
||||
if (isSiteConfigSnapshotFresh()) {
|
||||
return Promise.resolve(siteConfigSnapshot);
|
||||
}
|
||||
if (inFlightSiteConfigRequest) return inFlightSiteConfigRequest;
|
||||
inFlightSiteConfigRequest = fetch('/api/site-config')
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
|
||||
Reference in New Issue
Block a user