Next.js 性能优化指南

8 reasons your Next.js app is slow --- and how to fix them - LogRocket Blog

用户感知速度

  • 0.1 秒 --- 系统感觉瞬时的极限。在此情况下,用户不会感知到任何延迟
  • 1.0 秒 --- 保持用户思维流不间断的上限,即使他们会注意到延迟
  • 10 秒 --- 用户完全失去注意力和注意力之前的最长时间

解决方案:使用加载状态和 React Suspense

jsx 复制代码
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <DashboardContent />
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>
    </div>
  );
}

SSR 渲染影响速度

在第一次加载时,Next.js 应用的行为类似于传统的服务器呈现网站。它在服务器上获取数据,呈现完整的 HTML,并将其发送到浏览器。这对于 SEO 非常有用

js 复制代码
// First visit: Server does all the heavy lifting
export async function getServerSideProps() {
  // This runs on the server, blocking the response
  const userData = await fetchUser();
  const dashboardData = await fetchDashboard(userData.id);
  const notifications = await fetchNotifications(userData.id);

  return {
    props: {
      userData,
      dashboardData,
      notifications
    }
  };
}

但是一旦加载了初始页面并且 React 接管了,Next.js 就会切换到 SPA 模式。单击会触发客户端导航,无需重新加载整页------就像 React Router 一样

js 复制代码
// Subsequent navigation: Pure SPA behavior
function Dashboard({ userData }) {
  const router = useRouter();

  const goToProfile = () => {
    // This doesn't hit the server - pure client-side navigation
    router.push('/profile');
  };

  return (
    <div>
      <h1>Welcome, {userData.name}</h1>
      <button onClick={goToProfile}>View Profile</button>
    </div>
  );
}

性能问题出现的地方

  • 在服务器上,缓慢的数据库查询或阻塞作会延迟 HTML 响应
  • 在客户端上,过大的 JavaScript 捆绑包或低效的 API 调用会影响导航响应速度

同步获取数据

js 复制代码
// The slow way - each request waits for the previous one
export async function getServerSideProps({ req }) {
  // Step 1: Get user (300ms)
  const user = await fetchUser(req.session.userId);
  // Step 2: Wait for user, then get profile (400ms)
  const profile = await fetchUserProfile(user.id);
  // Step 3: Wait for profile, then get dashboard data (600ms)  
  const dashboardData = await fetchDashboardData(user.id, profile.preferences);
  // Step 4: Wait for dashboard, then get notifications (200ms)
  const notifications = await fetchNotifications(user.id);

  // Total time: 300 + 400 + 600 + 200 = 1,500ms (1.5 seconds!)
  return {
    props: {
      user,
      profile,
      dashboardData,
      notifications
    }
  };
}

使用 Promise.all() 优化

js 复制代码
export async function getServerSideProps({ req }) {
  // Step 1: Get user first (still needed for other requests)
  const user = await fetchUser(req.session.userId);

  // Step 2: Fetch everything else in parallel
  const [profile, dashboardData, notifications] = await Promise.all([
    fetchUserProfile(user.id),           // 400ms
    fetchDashboardData(user.id),         // 600ms  
    fetchNotifications(user.id)          // 200ms
  ]);

  // Total time: 300ms (user) + 600ms (longest parallel request) = 900ms
  // We just saved 600ms!

  return {
    props: {
      user,
      profile,
      dashboardData,
      notifications
    }
  };
}

并行获取全局数据

如果某些数据不依赖于用户,例如系统范围的设置或服务器状态,您可以与用户接口并行调用

js 复制代码
export async function getServerSideProps({ req }) {
  // Fetch user-independent data alongside user data
  const [user, globalSettings, systemStatus] = await Promise.all([
    fetchUser(req.session.userId),
    fetchGlobalSettings(),               // Doesn't need user
    fetchSystemStatus()                  // Doesn't need user
  ]);

  // Now fetch user-dependent data in parallel
  const [profile, dashboardData, notifications] = await Promise.all([
    fetchUserProfile(user.id),
    fetchDashboardData(user.id),
    fetchNotifications(user.id)
  ]);

  return {
    props: {
      user,
      profile,
      dashboardData,
      notifications,
      globalSettings,
      systemStatus
    }
  };
}

您的路由触发不必要的服务器往返

使用 Next.js App Router,每次导航都可能触发服务器端渲染。

js 复制代码
// app/products/[id]/page.js
// This runs on the SERVER for every product page visit
export default async function ProductPage({ params }) {
  // Network call to server on every navigation
  const product = await fetchProduct(params.id);
  const reviews = await fetchReviews(params.id);
  const recommendations = await fetchRecommendations(params.id);

  return (
    <div>
      <ProductDetails product={product} />
      <ReviewsList reviews={reviews} />
      <RecommendationGrid recommendations={recommendations} />
    </div>
  );
}

使用客户端路由

js 复制代码
// app/products/[id]/page.js
'use client'; // This makes it client-side rendered

import { useEffect, useState } from 'react';
import { useParams } from 'next/navigation';

export default function ProductPage() {
  const params = useParams();
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState(null);

  useEffect(() => {
    // Fetch data client-side - no server round trip
    Promise.all([
      fetch(`/api/products/${params.id}`).then(res => res.json()),
      fetch(`/api/reviews/${params.id}`).then(res => res.json())
    ]).then(([productData, reviewsData]) => {
      setProduct(productData);
      setReviews(reviewsData);
    });
  }, [params.id]);

  if (!product) return <ProductSkeleton />;

  return (
    <div>
      <ProductDetails product={product} />
      <ReviewsList reviews={reviews} />
    </div>
  );
}

何时使用服务器端渲染与客户端渲染

  • 在以下情况下使用服务器端渲染(SSR)

    • SEO 至关重要(产品页面、博客文章)
    • 您显示的内容是和用户敏感数据有关
    • 初始页面加载速度比导航速度更重要
    • 内容不经常更改
  • 在以下情况下使用客户端渲染(CSR)

    • 用户经常在页面之间导航
    • 您可以有效地缓存数据
    • SEO 不是优先事项(用户仪表板、管理面板)
    • 您想要即时、类似应用程序的导航

混合渲染

有时,您需要 SSR 来加载初始页面,但希望在后续交互中获得 CSR 的好处。

js 复制代码
// app/products/[id]/page.js
// Server-render the initial page for SEO
export default async function ProductPage({ params }) {
  const initialProduct = await fetchProduct(params.id);

  return (
    <div>
      <ProductClient initialData={initialProduct} productId={params.id} />
    </div>
  );
}

// components/ProductClient.js  
'use client';
export default function ProductClient({ initialData, productId }) {
  const [product, setProduct] = useState(initialData);

  // Subsequent navigation is client-side
  const router = useRouter();

  const navigateToProduct = async (newId) => {
    // Update URL immediately (feels instant)
    router.push(`/products/${newId}`);

    // Fetch new data in background
    const newProduct = await fetch(`/api/products/${newId}`).then(res => res.json());
    setProduct(newProduct);
  };

  return (
    <div>
      <ProductDetails product={product} />
      <RelatedProducts onProductClick={navigateToProduct} />
    </div>
  );
}

JavaScript 捆绑包

JavaScript 捆绑包大小直接影响您的交互时间(TTI),这是衡量页面何时完全发挥作用的指标。

js 复制代码
// First Bundle bloater: Importing entire libraries;

import _ from 'lodash'; // Imports the entire 70KB library
import * as dateFns from 'date-fns'; // Another massive import

// Second Bundle bloater: Importing heavy components everywhere
import { DataVisualization } from './DataVisualization'; // 500KB component
import { VideoPlayer } from './VideoPlayer'; // 300KB component
import { RichTextEditor } from './RichTextEditor'; // 400KB component

export default function HomePage() {
  return (
    <div>
      <h1>Welcome</h1>
      {/* These components might not even be visible on initial load */}
      <DataVisualization />
      <VideoPlayer />
      <RichTextEditor />
    </div>
  );
}

代码拆分和动态导入

Next.js 支持开箱即用的智能代码拆分。仅在需要时使用动态导入来加载代码。

基于路由的代码拆分

默认情况下,Next.js 按路由拆分代码。但是您可以使用 next/dynamic 优化它

js 复制代码
// pages/dashboard.js - Only loads when users visit /dashboard
import dynamic from 'next/dynamic';

// Heavy components loaded only when needed
const AnalyticsChart = dynamic(() => import('../components/AnalyticsChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false // Skip server-side rendering for client-only components
});

const DataExporter = dynamic(() => import('../components/DataExporter'), {
  loading: () => <p>Loading exporter...</p>
});

export default function Dashboard() {
  const [showAnalytics, setShowAnalytics] = useState(false);
  const [showExporter, setShowExporter] = useState(false);

  return (
    <div>
      <h1>Dashboard</h1>

      <button onClick={() => setShowAnalytics(true)}>
        View Analytics
      </button>

      {showAnalytics && <AnalyticsChart />}

      <button onClick={() => setShowExporter(true)}>
        Export Data
      </button>

      {showExporter && <DataExporter />}
    </div>
  );
}

组件级代码拆分

跨路由共享的组件,仅在特定情况下需要,可以延迟加载这些组件

js 复制代码
// components/ConditionalFeatures.js
import dynamic from 'next/dynamic';

// Load only when user has premium subscription
const PremiumChart = dynamic(() => import('./PremiumChart'), {
  loading: () => <div>Loading premium features...</div>
});

// Load only when user clicks "Advanced Settings"
const AdvancedSettings = dynamic(() => import('./AdvancedSettings'));

export function ConditionalFeatures({ user, showAdvanced }) {
  return (
    <div>
      {user.isPremium && <PremiumChart />}
      {showAdvanced && <AdvancedSettings />}
    </div>
  );
}

使用 @next/bundle-analyzer 分析捆绑包

要查看什么正在占用您的捆绑包大小,请使用官方捆绑包分析器:

js 复制代码
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
});

module.exports = withBundleAnalyzer({
  // Your Next.js config
});

水合作用

在服务器将 HTML 发送到浏览器后,React 需要通过附加事件侦听器并将其虚拟 DOM 与服务器渲染的标记进行协调来"水合"它。此过程可能会阻止交互性并影响性能。

js 复制代码
// The traditional Next.js page with hydration bottlenecks

export default function ProductPage({ products }) {
  return (
    <div>
      <Header /> {/* Must hydrate before user can interact */}
      <ProductGrid products={products} /> {/* Large component tree */}
      <FilterSidebar /> {/* Complex interactive components */}
      <Footer /> {/* Static content that doesn't need JS */}

      {/* Everything hydrates at once, blocking interactivity */}
    </div>
  );
}

使用 React 服务器组件和部分水合

Next.js App Router 带来了 React Server 组件,它通过让您选择应用程序的哪些部分需要客户端 JavaScript

js 复制代码
// app/products/page.js - Server Component (no JS sent to client)
import { ProductGrid } from './components/ProductGrid';
import { ClientSideFilter } from './components/ClientSideFilter';

// This component runs on the server and sends only HTML
export default async function ProductPage() {
  // Data fetching happens on the server
  const products = await fetchProducts();

  return (
    <div>
      <h1>Products</h1>
      {/* Static parts remain as HTML only */}
      <ProductGrid products={products} />

      {/* Only interactive parts are hydrated */}
      <ClientSideFilter products={products} />
    </div>
  );
}

// components/ClientSideFilter.js
'use client'; // Marks this as needing hydration

export function ClientSideFilter({ products }) {
  const [filters, setFilters] = useState({});
  // Interactive component logic...
}

没有缓存数据

  • 重复获取相同的数据
js 复制代码
export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  // This runs on every component mount - no caching!
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser);
  }, [userId]);
  return user ? user.name : "Loading...";
}
  • 在每次页面加载时提取新数据
js 复制代码
export async function getServerSideProps({ params }) {
  // This hits the database on every single request
  const posts = await db.posts.findMany({
    where: { published: true },
    orderBy: { createdAt: "desc" },
  });
  return { props: { posts } };
}

尽可能使用 SSG 和 SWR

有效的缓存适用于不同的级别:API 路由、页面渲染,甚至数据库查询。

使用 ISR 的服务器端缓存

如果您的数据不是实时更新,请不要实时重新提取它。使用增量静态重新生成 (ISR) 提供预构建的页面并偶尔刷新它们

js 复制代码
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);

  return {
    props: { post },
    revalidate: 3600, // Regenerate at most once per hour
  };
}

export async function getStaticPaths() {
  // Generate paths for popular posts
  const popularPosts = await fetchPopularPosts();

  return {
    paths: popularPosts.map((post) => ({
      params: { slug: post.slug }
    })),
    fallback: 'blocking' // Generate other pages on-demand
  };
}

在 API 上使用 cache-control

对于昂贵的 API 作,请使用 unstable_cache 缓存服务器端逻辑

js 复制代码
// pages/api/posts.js
import { unstable_cache } from 'next/cache';

const getCachedPosts = unstable_cache(
  async () => {
    // Expensive database query
    return await db.posts.findMany({
      include: {
        author: true,
        comments: { take: 5 },
        tags: true
      },
      orderBy: { createdAt: 'desc' }
    });
  },
  ['posts-list'],
  {
    revalidate: 300, // Cache for 5 minutes
    tags: ['posts']
  }
);

export default async function handler(req, res) {
  const posts = await getCachedPosts();

  res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
  res.json(posts);
}

静态资源

原始 <img> 标签

javascript 复制代码
export default function ProductCard({ product }) {
  return (
    <>
      <img src={product.image} alt={product.name} />
      {product.name}
      {product.price}
    </>
  );
}

热切加载所有内容

js 复制代码
export default function Gallery({ images }) {
  return (
    <div className="gallery">
      {images.map((image, index) => (
        // All 50 images load at once, even if users only see 6
        <img key={index} src={image.url} alt={image.caption} />
      ))}
    </div>
  );
}

使用响应式的 next/image

Next.js 提供了一个图像组件,用于处理响应式大小调整、延迟加载和格式转换(如 WebP/AVIF)。它更快、更易于访问,并节省了大量带宽。

基础优化

js 复制代码
// components/ProductCard.js
import Image from "next/image";

export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={300}
        height={200}
        priority={product.featured} // Load featured products immediately
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q=="
        className="rounded-lg object-cover"
      />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  );
}

响应式图片

对于在不同屏幕上以不同尺寸显示的图像

js 复制代码
// components/HeroSection.js
import Image from "next/image";

export default function HeroSection() {
  return (
    <div className="hero relative h-screen">
      <Image
        src="/hero-image.jpg"
        alt="Hero image"
        fill
        priority
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        className="object-cover"
      />
      <div className="absolute inset-0 flex items-center justify-center">
        <h1 className="text-white text-6xl font-bold">Welcome</h1>
      </div>
    </div>
  );
}

延迟加载

js 复制代码
// components/ImageGallery.js
import Image from "next/image";
import { useState } from "react";

export default function ImageGallery({ images }) {
  const [visibleCount, setVisibleCount] = useState(6);

  const loadMore = () => {
    setVisibleCount((prev) => Math.min(prev + 6, images.length));
  };

  return (
    <div className="gallery">
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {images.slice(0, visibleCount).map((image, index) => (
          <div key={image.id} className="aspect-square relative">
            <Image
              src={image.url}
              alt={image.caption}
              fill
              sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
              className="object-cover rounded-lg"
              priority={index < 6} // Prioritize first 6 images
            />
          </div>
        ))}
      </div>

      {visibleCount < images.length && (
        <button
          onClick={loadMore}
          className="mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg"
        >
          Load More ({images.length - visibleCount} remaining)
        </button>
      )}
    </div>
  );
}
相关推荐
Mintopia5 天前
Next.js 内置后端能力扩展 —— 重定向与路由保护
前端·javascript·next.js
JacksonGao6 天前
Next.js的App router和Pages router的区别你知道多少?
react.js·next.js
Mintopia7 天前
🚪 当 Next.js 中间件穿上保安制服:请求拦截与权限控制的底层奇幻之旅
前端·后端·next.js
Mintopia10 天前
🚀 Next.js 后端能力扩展:错误处理与 HTTP 状态码规范
前端·javascript·next.js
Mintopia10 天前
🧭 新一代 Next.js App Router 下的 Route Handlers —— 从原理到优雅实践
前端·javascript·next.js
OEC小胖胖11 天前
SEO 优化:元数据 (Metadata) API 和站点地图 (Sitemap) 生成
前端·javascript·前端框架·html·web·next.js
可乐爱宅着12 天前
如何在next.js中处理表单提交
前端·next.js
Mintopia12 天前
在 Next.js 中开垦后端的第一块菜地:/pages/api 的 REST 接口
前端·javascript·next.js
Mintopia15 天前
Next.js + AI-SDK + DeepSeek:3 分钟建设属于你的 AI 问答 Demo
前端·javascript·next.js