You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js14页面跳转与主题切换卡顿及FCP/LCP优化求助

性能优化排查:Next.js 14 + Vanilla Extract 页面跳转/主题切换卡顿问题

问题背景

使用 Next.js 14、Vanilla Extract、MongoDB 搭建的博客部署在 Vercel 上,存在以下性能问题:

  • 无缓存状态下页面跳转耗时 2.5-5 秒
  • 主题切换操作至少需要 2 秒才能生效
    已尝试删除 post 布局、移除 SVGmorph 组件、修改 fetch 逻辑等操作,问题仍未解决。

相关代码片段

RootLayout

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = cookies();
  const theme = cookieStore.get("theme");
  return (
    <html lang="ko-KR">
      <link rel="icon" href="/favicon.ico" sizes="any" />
      <body className={theme?.value === "light" ? ligthTheme : darkTheme}>
        <Topbar />
        <div className={styles.mainContentContainer}>
          <main className={styles.mainContent}>
            {children}
            <Footer />
          </main>
        </div>
        <BottomBar />
      </body>
    </html>
  );
}

主题切换按钮(ToggleBtn)

import { moon } from "../icon/moon";
import { sun } from "../icon/sun";
import { cookies } from "next/headers";
import { toggleBtn } from "./ToggleBtn.css";
import SVGmorph from "./SVGmorph";

export default function ToggleBtn() {
  const theme = cookies().get("theme");
  const handleTheme = async () => {
    "use server";
    if (theme?.value === "dark" || !theme?.value) {
      cookies().set("theme", "light");
    } else {
      cookies().set("theme", "dark");
    }
  };
  return (
    <form action={handleTheme}>
      <button className={toggleBtn}>
        <svg
          width="28"
          height="28"
          viewBox="0 0 24 24"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <SVGmorph paths={[sun, moon, sun]} theme={theme?.value || "dark"} />
        </svg>
      </button>
    </form>
  );
}

组件内 CSS 使用

background: vars.themeColor.backgroundColor.glassGradientColorTop

核心问题分析

  1. 主题切换依赖 Server Action 导致延迟:当前主题切换通过 Server Action 同步执行,每次点击都需要发起服务器请求并等待响应,网络往返时间直接导致主题切换卡顿。
  2. RootLayout 动态渲染阻塞缓存:RootLayout 作为服务器组件,每次请求都读取 Cookie 并动态设置 body 类名,导致页面无法被 Vercel 缓存,每次页面跳转都需要服务器重新生成内容,增加加载耗时。
  3. Vanilla Extract 主题样式的动态应用:若主题样式未提前静态生成,运行时动态计算样式变量可能增加渲染开销。

优化方案

1. 主题切换逻辑改为客户端优先+异步同步

将主题切换改为客户端即时生效,再异步更新服务器 Cookie,消除网络等待延迟:

改造 ToggleBtn 为客户端组件

'use client';

import { moon } from "../icon/moon";
import { sun } from "../icon/sun";
import { toggleBtn } from "./ToggleBtn.css";
import SVGmorph from "./SVGmorph";
import { useState, useEffect } from 'react';
import { ligthTheme, darkTheme } from '../styles/theme.css';

export default function ToggleBtn() {
  const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>('dark');

  // 从 Cookie 初始化主题
  useEffect(() => {
    const savedTheme = document.cookie.split('; ')
      .find(row => row.startsWith('theme='))
      ?.split('=')[1] as 'light' | 'dark';
    if (savedTheme) setCurrentTheme(savedTheme);
  }, []);

  const handleThemeToggle = async () => {
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
    
    // 1. 即时更新客户端主题
    setCurrentTheme(newTheme);
    document.body.className = newTheme === 'light' ? ligthTheme : darkTheme;

    // 2. 异步同步服务器 Cookie
    try {
      await fetch('/api/theme', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ theme: newTheme })
      });
    } catch (err) {
      console.error('主题同步失败:', err);
    }
  };

  return (
    <button onClick={handleThemeToggle} className={toggleBtn}>
      <svg
        width="28"
        height="28"
        viewBox="0 0 24 24"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <SVGmorph paths={[sun, moon, sun]} theme={currentTheme} />
      </svg>
    </button>
  );
}

创建 Cookie 同步 API(app/api/theme/route.ts)

import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { theme } = await request.json();
  cookies().set('theme', theme, {
    path: '/',
    maxAge: 30 * 24 * 60 * 60, // 缓存30天
    httpOnly: false, // 允许客户端读取
  });
  return NextResponse.json({ success: true });
}

2. 静态化 RootLayout,客户端初始化主题

修改 RootLayout 为静态可缓存结构,将主题初始化逻辑移到客户端组件:

改造 RootLayout

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko-KR">
      <link rel="icon" href="/favicon.ico" sizes="any" />
      <body>
        <ThemeInitializer />
        <Topbar />
        <div className={styles.mainContentContainer}>
          <main className={styles.mainContent}>
            {children}
            <Footer />
          </main>
        </div>
        <BottomBar />
      </body>
    </html>
  );
}

创建 ThemeInitializer 客户端组件

'use client';

import { useEffect } from 'react';
import { ligthTheme, darkTheme } from '../styles/theme.css';

export default function ThemeInitializer() {
  useEffect(() => {
    const savedTheme = document.cookie.split('; ')
      .find(row => row.startsWith('theme='))
      ?.split('=')[1];
    const targetTheme = savedTheme || 'dark';
    document.body.className = targetTheme === 'light' ? ligthTheme : darkTheme;
  }, []);

  return null;
}

3. Vanilla Extract 主题样式优化

确保主题配置为静态生成,避免运行时动态计算:

  • 使用 createTheme 提前定义主题变量
  • 导出静态主题类名,避免运行时生成样式
// styles/theme.css.ts
import { createTheme, style } from '@vanilla-extract/css';

export const [lightThemeClass, lightThemeVars] = createTheme({
  themeColor: {
    backgroundColor: {
      glassGradientColorTop: '#ffffff'
    }
  }
});

export const [darkThemeClass, darkThemeVars] = createTheme({
  themeColor: {
    backgroundColor: {
      glassGradientColorTop: '#1a1a1a'
    }
  }
});

// 全局样式绑定主题变量
export const ligthTheme = style({
  vars: lightThemeVars
});

export const darkTheme = style({
  vars: darkThemeVars
});

4. 页面缓存与渲染优化

  • 对博客文章页面使用增量静态再生(ISR),通过 generateStaticParams + revalidate 参数预渲染页面并定期更新
  • 确保数据请求使用 cache: 'force-cache' 缓存数据库查询结果,避免每次请求都访问 MongoDB
// app/post/[slug]/page.tsx
export async function generateStaticParams() {
  // 预获取所有文章slug
  const posts = await fetch('https://your-mongo-api/posts', { cache: 'force-cache' });
  return posts.map(post => ({ slug: post.slug }));
}

export async function getPosts() {
  const res = await fetch('https://your-mongo-api/posts', {
    cache: 'force-cache',
    next: { revalidate: 86400 } // 24小时重新验证
  });
  return res.json();
}

验证与后续检查

  1. 部署后检查 Vercel 控制台的缓存状态,确认静态页面已被缓存
  2. 使用 Chrome DevTools 的 Performance 面板分析页面加载耗时,定位剩余瓶颈
  3. 检查 MongoDB 查询是否添加索引,优化数据获取速度

内容的提问来源于stack exchange,提问作者Joo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 13:50:55