优化 Vercel Fluid CPU:API加CDN缓存、缩小middleware范围、跳过公共API的Supabase身份转发

- 7条GET路由加 s-maxage + stale-while-revalidate,fetch 改为 next revalidate
- middleware matcher 从全匹配缩小到9条需auth的路径,移除 isStaticAsset
- 8条公共API路由加 includeSupabaseIdentity: false
- backend-auth getSession 为空时跳过 getUser
- subscription-help 加 I18nProvider,移除 force-dynamic → 静态预渲染
- next.config.mjs 加静态资源 immutable 缓存头

Tested: npx tsc --noEmit 通过,npm run build 通过
This commit is contained in:
2569718930@qq.com
2026-05-13 14:23:04 +08:00
parent 30b289ec8f
commit 40f231b76d
13 changed files with 112 additions and 75 deletions
+3 -1
View File
@@ -26,7 +26,9 @@ export async function POST(req: NextRequest) {
try { try {
const body = await req.json(); const body = await req.json();
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const headers = new Headers(auth.headers); const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
const res = await fetch(`${API_BASE}/api/analytics/events`, { const res = await fetch(`${API_BASE}/api/analytics/events`, {
+2 -3
View File
@@ -10,7 +10,6 @@ import {
import { buildCachedJsonResponse } from "@/lib/http-cache"; import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (!API_BASE) { if (!API_BASE) {
@@ -27,7 +26,7 @@ export async function GET(req: NextRequest) {
}); });
const res = await fetch(`${API_BASE}/api/cities`, { const res = await fetch(`${API_BASE}/api/cities`, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 60 },
}); });
if (!res.ok) { if (!res.ok) {
const raw = await res.text(); const raw = await res.text();
@@ -38,7 +37,7 @@ export async function GET(req: NextRequest) {
const response = buildCachedJsonResponse( const response = buildCachedJsonResponse(
req, req,
data, data,
"no-store, max-age=0", "public, max-age=0, s-maxage=60, stale-while-revalidate=300",
); );
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
+10 -3
View File
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
buildUpstreamErrorResponse, buildUpstreamErrorResponse,
} from "@/lib/api-proxy"; } from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -42,10 +43,12 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`; const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`;
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(url, { const res = await fetch(url, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 15 },
}); });
if (!res.ok) { if (!res.ok) {
const raw = await res.text(); const raw = await res.text();
@@ -53,7 +56,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} }
const data = await res.json(); const data = await res.json();
const response = NextResponse.json(data); const response = buildCachedJsonResponse(
req,
data,
"public, max-age=0, s-maxage=15, stale-while-revalidate=45",
);
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
const response = buildProxyExceptionResponse(error, { const response = buildProxyExceptionResponse(error, {
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
buildUpstreamErrorResponse, buildUpstreamErrorResponse,
} from "@/lib/api-proxy"; } from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -45,10 +46,12 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`; const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`;
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(url, { const res = await fetch(url, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 20 },
}); });
if (!res.ok) { if (!res.ok) {
const raw = await res.text(); const raw = await res.text();
@@ -59,11 +62,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} }
const data = await res.json(); const data = await res.json();
const response = NextResponse.json(data, { const response = buildCachedJsonResponse(
headers: { req,
"Cache-Control": "no-store", data,
}, "public, max-age=0, s-maxage=20, stale-while-revalidate=60",
}); );
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
const response = buildProxyExceptionResponse(error, { const response = buildProxyExceptionResponse(error, {
+17 -10
View File
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
buildUpstreamErrorResponse, buildUpstreamErrorResponse,
} from "@/lib/api-proxy"; } from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -131,26 +132,28 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`; const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`;
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(url, { const res = await fetch(url, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 15 },
}); });
if (!res.ok) { if (!res.ok) {
const raw = await res.text(); const raw = await res.text();
const summaryUrl = `${API_BASE}/api/city/${encodeURIComponent(name)}/summary?force_refresh=${forceRefresh}`; const summaryUrl = `${API_BASE}/api/city/${encodeURIComponent(name)}/summary?force_refresh=${forceRefresh}`;
const summaryRes = await fetch(summaryUrl, { const summaryRes = await fetch(summaryUrl, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 10 },
}); });
if (summaryRes.ok) { if (summaryRes.ok) {
const summaryData = await summaryRes.json(); const summaryData = await summaryRes.json();
const response = NextResponse.json(buildFallbackCityDetail(name, depth, summaryData), { const response = buildCachedJsonResponse(
headers: { req,
"Cache-Control": "no-store", buildFallbackCityDetail(name, depth, summaryData),
"X-PolyWeather-Fallback": "summary", "public, max-age=0, s-maxage=10, stale-while-revalidate=30",
}, );
}); response.headers.set("X-PolyWeather-Fallback", "summary");
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} }
@@ -158,7 +161,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} }
const data = normalizeCityDetailPayload(await res.json()); const data = normalizeCityDetailPayload(await res.json());
const response = NextResponse.json(data); const response = buildCachedJsonResponse(
req,
data,
"public, max-age=0, s-maxage=15, stale-while-revalidate=45",
);
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
const response = buildProxyExceptionResponse(error, { const response = buildProxyExceptionResponse(error, {
+3 -1
View File
@@ -27,7 +27,9 @@ export async function GET(
const url = `${API_BASE}/api/history/${encodeURIComponent(name)}`; const url = `${API_BASE}/api/history/${encodeURIComponent(name)}`;
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const fetchOptions = { const fetchOptions = {
headers: auth.headers, headers: auth.headers,
next: { revalidate: 60 }, next: { revalidate: 60 },
+7 -4
View File
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
buildUpstreamErrorResponse, buildUpstreamErrorResponse,
} from "@/lib/api-proxy"; } from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -21,7 +22,7 @@ export async function GET(req: NextRequest) {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/payments/config`, { const res = await fetch(`${API_BASE}/api/payments/config`, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 300 },
}); });
if (!res.ok) { if (!res.ok) {
const raw = await res.text(); const raw = await res.text();
@@ -31,9 +32,11 @@ export async function GET(req: NextRequest) {
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} }
const data = await res.json(); const data = await res.json();
const response = NextResponse.json(data, { const response = buildCachedJsonResponse(
headers: { "Cache-Control": "no-store" }, req,
}); data,
"public, max-age=0, s-maxage=300, stale-while-revalidate=900",
);
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
return buildProxyExceptionResponse(error, { return buildProxyExceptionResponse(error, {
+10 -8
View File
@@ -7,13 +7,13 @@ import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
buildUpstreamErrorResponse, buildUpstreamErrorResponse,
} from "@/lib/api-proxy"; } from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number( const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "28000", process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "28000",
); );
export const dynamic = "force-dynamic";
export const maxDuration = 30; export const maxDuration = 30;
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
@@ -50,10 +50,12 @@ export async function GET(req: NextRequest) {
const timeoutId = setTimeout(() => controller.abort(), SCAN_TERMINAL_PROXY_TIMEOUT_MS); const timeoutId = setTimeout(() => controller.abort(), SCAN_TERMINAL_PROXY_TIMEOUT_MS);
try { try {
auth = await buildBackendRequestHeaders(req); auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(url, { const res = await fetch(url, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 10 },
signal: controller.signal, signal: controller.signal,
}); });
if (!res.ok) { if (!res.ok) {
@@ -62,11 +64,11 @@ export async function GET(req: NextRequest) {
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} }
const data = await res.json(); const data = await res.json();
const response = NextResponse.json(data, { const response = buildCachedJsonResponse(
headers: { req,
"Cache-Control": "no-store", data,
}, "public, max-age=0, s-maxage=10, stale-while-revalidate=30",
}); );
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
const timedOut = controller.signal.aborted; const timedOut = controller.signal.aborted;
+10 -5
View File
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse, buildProxyExceptionResponse,
buildUpstreamErrorResponse, buildUpstreamErrorResponse,
} from "@/lib/api-proxy"; } from "@/lib/api-proxy";
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -19,10 +20,12 @@ export async function GET(req: NextRequest) {
} }
try { try {
const auth = await buildBackendRequestHeaders(req); const auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: false,
});
const res = await fetch(`${API_BASE}/api/system/status`, { const res = await fetch(`${API_BASE}/api/system/status`, {
headers: auth.headers, headers: auth.headers,
cache: "no-store", next: { revalidate: 30 },
}); });
if (!res.ok) { if (!res.ok) {
const raw = await res.text(); const raw = await res.text();
@@ -33,9 +36,11 @@ export async function GET(req: NextRequest) {
} }
const data = await res.json(); const data = await res.json();
const response = NextResponse.json(data, { const response = buildCachedJsonResponse(
headers: { "Cache-Control": "no-store" }, req,
}); data,
"public, max-age=0, s-maxage=30, stale-while-revalidate=120",
);
return applyAuthResponseCookies(response, auth.response); return applyAuthResponseCookies(response, auth.response);
} catch (error) { } catch (error) {
return buildProxyExceptionResponse(error, { return buildProxyExceptionResponse(error, {
+6 -3
View File
@@ -1,7 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { SubscriptionHelpClient } from "./SubscriptionHelpClient"; import { SubscriptionHelpClient } from "./SubscriptionHelpClient";
import { I18nProvider } from "@/hooks/useI18n";
export const dynamic = "force-dynamic";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "PolyWeather | Subscription Help", title: "PolyWeather | Subscription Help",
@@ -9,5 +8,9 @@ export const metadata: Metadata = {
}; };
export default function SubscriptionHelpPage() { export default function SubscriptionHelpPage() {
return <SubscriptionHelpClient />; return (
<I18nProvider>
<SubscriptionHelpClient />
</I18nProvider>
);
} }
+11 -7
View File
@@ -41,17 +41,21 @@ export async function buildBackendRequestHeaders(
const incomingAuth = extractBearerToken(request.headers.get("authorization")); const incomingAuth = extractBearerToken(request.headers.get("authorization"));
const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false; const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false;
if (hasSupabaseServerEnv() && includeSupabaseIdentity) { if (hasSupabaseServerEnv() && includeSupabaseIdentity) {
const passthroughResponse = new NextResponse(null, { status: 200 }); const supabase = createSupabaseRouteClient(request, new NextResponse(null, { status: 200 }));
const supabase = createSupabaseRouteClient(request, passthroughResponse);
const { const {
data: { session }, data: { session },
} = await supabase.auth.getSession(); } = await supabase.auth.getSession();
const { let user = session?.user ?? null;
data: { user }, if (session) {
} = await supabase.auth.getUser(); const {
data: { user: validated },
} = await supabase.auth.getUser();
user = validated ?? session.user;
}
const forwardedUserId = String(user?.id || session?.user?.id || "").trim(); const passthroughResponse = new NextResponse(null, { status: 200 });
const forwardedEmail = String(user?.email || session?.user?.email || "").trim(); const forwardedUserId = String(user?.id || "").trim();
const forwardedEmail = String(user?.email || "").trim();
if (forwardedUserId) { if (forwardedUserId) {
headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, forwardedUserId); headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, forwardedUserId);
} }
+12 -23
View File
@@ -20,23 +20,6 @@ const SUPABASE_AUTH_REQUIRED = readEnvBool(
SUPABASE_AUTH_ENABLED, SUPABASE_AUTH_ENABLED,
); );
function isStaticAsset(pathname: string) {
return (
pathname.startsWith("/_next/") ||
pathname.startsWith("/favicon") ||
pathname === "/apple-touch-icon.png" ||
pathname === "/manifest.webmanifest" ||
pathname === "/site.webmanifest" ||
pathname.startsWith("/android-chrome-") ||
pathname.startsWith("/robots.txt") ||
pathname.startsWith("/sitemap.xml") ||
pathname.startsWith("/icons/") ||
pathname.startsWith("/images/") ||
pathname.startsWith("/scenery/") ||
pathname.startsWith("/static/")
);
}
function isPublicPage(pathname: string) { function isPublicPage(pathname: string) {
return ( return (
pathname === "/" || pathname === "/" ||
@@ -78,7 +61,7 @@ function handleLegacyTokenGate(request: NextRequest) {
} }
const { pathname, searchParams } = request.nextUrl; const { pathname, searchParams } = request.nextUrl;
if (isStaticAsset(pathname) || isPublicPage(pathname) || isPublicApi(pathname)) { if (isPublicPage(pathname) || isPublicApi(pathname)) {
return NextResponse.next(); return NextResponse.next();
} }
@@ -178,10 +161,6 @@ async function handleSupabaseOptionalSession(request: NextRequest) {
} }
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (isStaticAsset(pathname)) {
return NextResponse.next();
}
const requestHost = const requestHost =
request.headers.get("x-forwarded-host") || request.headers.get("x-forwarded-host") ||
request.headers.get("host") || request.headers.get("host") ||
@@ -203,5 +182,15 @@ export async function middleware(request: NextRequest) {
} }
export const config = { export const config = {
matcher: ["/((?!_next/static|_next/image).*)"], matcher: [
"/account/:path*",
"/ops/:path*",
"/api/auth/:path*",
"/api/ops/:path*",
"/api/payments/:path*",
"/api/system/:path*",
"/api/history/:path*",
"/api/city/:path*/detail:path*",
"/api/scan/terminal/ai:path*",
],
}; };
+11
View File
@@ -1,6 +1,17 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
async headers() {
const cacheHeader = {
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
};
const staticExts = ["jpg", "jpeg", "png", "gif", "ico", "svg", "webp", "avif", "woff2", "ttf", "eot", "css", "js"];
return staticExts.map((ext) => ({
source: `/:path(.+\\.${ext})`,
headers: [cacheHeader],
}));
},
}; };
export default nextConfig; export default nextConfig;