Next.js 渲染策略及其对核心网络指标的影响

在使用 Next.js 构建快速且可扩展的 Web 应用时,理解渲染机制(尤其是结合 App Router 时)至关重要。Next.js 围绕两种主要环境组织渲染:服务器和客户端。在服务器端,有三种关键策略:静态渲染、动态渲染和流式渲染。每种策略都有其独特的权衡和性能优势,因此了解何时使用哪种策略对于提供出色的用户体验至关重要。

在本文中,我们将详细解析每种策略的特点、适用场景,以及它们对网站性能(尤其是核心网络指标)的影响。我们还将探讨混合方法,并提供实用指南,帮助您为具体用例选择合适的策略。

什么是核心网络指标?

核心网络指标是谷歌定义的一组衡量网站真实用户体验的指标。这些指标在搜索引擎排名中起着重要作用,并直接影响用户对网站速度和流畅度的感知。

  • 最大内容绘制 (LCP) :衡量加载性能,计算最大可见内容元素渲染所需的时间。良好的 LCP 应在 2.5 秒或更短。

  • 交互到下一次绘制 (INP) :衡量对用户输入的响应速度。良好的 INP 应在 200 毫秒或更短。

  • 累积布局偏移 (CLS) :衡量页面的视觉稳定性,量化加载过程中的布局不稳定性。良好的 CLS 应在 0.1 或更低。

如果您想深入了解核心网络指标及其对网站性能的影响,建议阅读这篇关于新核心网络指标及其工作原理的详细指南。

Next.js 渲染策略与核心网络指标

让我们详细探讨每种渲染策略:

1. 静态渲染(服务器渲染策略)

静态渲染是 Next.js 中服务器组件的默认策略。通过这种方法,组件在构建时(或重新验证期间)进行渲染,生成的 HTML 会被重复用于每个请求。这种预渲染发生在服务器上,而不是用户的浏览器中。静态渲染非常适合数据不针对用户个性化的路由,适用于以下场景:

  • 以内容为中心的网站:博客、文档、营销页面
  • 电子商务产品列表:当产品详情不频繁更改时
  • 对 SEO 至关重要的页面:当搜索引擎可见性是优先事项时
  • 高流量页面:当您希望最小化服务器负载时

静态渲染如何影响核心网络指标

  • 最大内容绘制 (LCP) :静态渲染通常会带来出色的 LCP 分数(通常 < 1 秒)。预渲染的 HTML 可以被缓存并从 CDN 即时交付,从而非常快速地交付初始内容(包括最大元素)。此外,无需等待客户端的数据获取或渲染。

  • 交互到下一次绘制 (INP) :静态渲染为 INP 提供了良好的基础,但不能保证最佳性能(具体取决于实现,通常在 50-150 毫秒范围内)。虽然服务器组件不需要水合,但页面内的任何客户端组件仍然需要 JavaScript 才能变得可交互。要获得非常好的 INP 分数,您需要确保页面内的客户端组件最少。

  • 累积布局偏移 (CLS) :虽然静态渲染会预先交付完整的页面结构,这对 CLS 非常有益,但要实现出色的 CLS,还需要额外的优化策略:

    • 如果资源异步加载,仅静态 HTML 无法防止布局偏移
    • 必须正确指定图像尺寸,以便在图像加载前预留空间
    • 如果不使用字体显示策略正确处理 Web 字体,可能会导致文本重排
    • 动态注入的内容(广告、嵌入、延迟加载元素)可能会破坏布局稳定性
    • CSS 实现对 CLS 有显著影响 ------ 样式信息的即时可用性有助于保持视觉稳定性

代码示例:

基本静态渲染

tsx 复制代码
// app/page.tsx(服务器组件 - 默认静态渲染)
export default async function Page() {
  const res = await fetch('https://api.example.com/static-data');
  const data = await res.json();

  return (
    <div>
      <h1>静态内容</h1>
      <p>{data.content}</p>
    </div>
  );
}

带重新验证的静态渲染(ISR)

tsx 复制代码
// app/dashboard/page.tsx
export default async function Dashboard() {
  // 每天重新验证的静态数据
  const siteStats = await fetch('https://api.example.com/site-stats', {
    next: { revalidate: 86400 } // 24 小时
  }).then(r => r.json());

  // 每小时重新验证的数据
  const popularProducts = await fetch('https://api.example.com/popular-products', {
    next: { revalidate: 3600 } // 1 小时
  }).then(r => r.json());

  // 带有缓存标签的按需重新验证数据
  const featuredContent = await fetch('https://api.example.com/featured-content', {
    next: { tags: ['featured'] }
  }).then(r => r.json());

  return (
    <div className="dashboard">
      <section className="stats">
        <h2>网站统计数据</h2>
        <p>总用户数: {siteStats.totalUsers}</p>
        <p>总订单数: {siteStats.totalOrders}</p>
      </section>

      <section className="popular">
        <h2>热门产品</h2>
        <ul>
          {popularProducts.map(product => (
            <li key={product.id}>{product.name} - {product.sales} 已售出</li>
          ))}
        </ul>
      </section>

      <section className="featured">
        <h2>精选内容</h2>
        <div>{featuredContent.html}</div>
      </section>
    </div>
  );
}

静态路径生成

tsx 复制代码
// app/products/[id]/page.tsx
export async function generateStaticParams() {
  const products = await fetch('https://api.example.com/products').then(r => r.json());

  return products.map((product) => ({
    id: product.id.toString(),
  }));
}

export default async function Product({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`).then(r => r.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

2. 动态渲染(服务器渲染策略)

动态渲染在每个请求时在服务器上为该请求生成 HTML。与静态渲染不同,内容不会被预渲染或缓存,而是为每个用户重新生成。这种渲染方式最适合以下场景:

  • 个性化内容:用户仪表盘、账户页面
  • 实时数据:股票价格、实时体育比分
  • 特定于请求的信息:使用 cookie、标头或搜索参数的页面
  • 频繁更改的数据:每次请求都需要最新的内容

动态渲染如何影响核心网络指标

  • 最大内容绘制 (LCP) :使用动态渲染时,服务器需要为每个请求生成 HTML,并且无法在 CDN 级别完全缓存。但它仍然比客户端渲染更快,因为 HTML 是在服务器上生成的。
  • 交互到下一次绘制 (INP) :页面加载后,性能与静态渲染类似。但是,如果动态内容包含许多客户端组件,它可能会变慢。
  • 累积布局偏移 (CLS) :如果请求时获取的数据与静态结构相比显著改变了页面的布局,动态渲染可能会引入 CLS。但是,如果布局稳定且动态内容的大小适合预定义的区域,则可以有效地管理 CLS。

代码示例:

显式动态渲染

tsx 复制代码
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // 强制此路由进行动态渲染
export default async function Dashboard() {
  // 这将在每个请求上运行
  const data = await fetch('https://api.example.com/dashboard-data').then(r => r.json());

  return (
    <div>
      <h1>仪表盘</h1>
      <p>上次更新: {new Date().toLocaleString()}</p>
      {/* 仪表盘内容 */}
    </div>
  );
}

带 cookie 的隐式动态渲染

tsx 复制代码
// app/profile/page.tsx
import { cookies } from 'next/headers';
export default async function Profile() {
  // 使用 cookies() 自动选择动态渲染
  const userId = cookies().get('userId')?.value;

  const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json());

  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <p>电子邮件: {user.email}</p>
      {/* 个人资料内容 */}
    </div>
  );
}

动态路由

tsx 复制代码
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
  // 对于任何未显式预渲染的 slug,它将在请求时运行
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

3. 流式渲染(服务器渲染策略)

流式渲染允许您从服务器逐步渲染 UI。服务器不必等待所有数据准备好后再发送任何 HTML,而是在数据可用时发送 HTML 块。这是通过 React 的 Suspense 边界实现的。

React Suspense 的工作原理是在组件树中创建边界,这些边界可以在等待异步操作时 "暂停" 渲染。当 Suspense 边界内的组件抛出 Promise(在 React 服务器组件中进行数据获取时会自动发生这种情况)时,React 会暂停该组件及其子组件的渲染,渲染 Suspense 组件中指定的回退 UI,继续渲染此边界外的页面其他部分,并在 Promise 解决后最终恢复并将回退替换为实际组件。

在流式渲染时,这种机制允许服务器发送带有暂停组件回退的初始 HTML,同时在后台继续处理暂停的组件。然后,服务器会在每个暂停组件解决时流式传输额外的 HTML 块,包括指示浏览器将回退无缝替换为最终内容的指令。它适用于以下场景:

  • 具有混合数据需求的页面:一些数据源快,一些慢
  • 改善感知性能:在较慢的部分加载时快速向用户显示内容
  • 复杂的仪表盘:不同的小部件有不同的加载时间
  • 处理慢速 API:防止慢速的第三方服务阻塞整个页面

流式渲染如何影响核心网络指标

  • 最大内容绘制 (LCP) :流式渲染可以改善感知的 LCP。通过快速发送初始 HTML 内容(包括可能的最大元素),浏览器可以更快地渲染它。即使页面的其他部分仍在加载,用户也能更快地看到主要内容。
  • 交互到下一次绘制 (INP) :流式渲染可以有助于更好的 INP。当与 React 的 一起使用时,页面中加载较快部分的交互元素可以更早地变得可交互,即使其他组件仍在流式传输中。这允许用户更早地与页面互动。
  • 累积布局偏移 (CLS) :当新内容流式传输时,流式渲染可能会导致布局偏移。但是,如果仔细实现,流式渲染应该不会对 CLS 产生负面影响。最初流式传输的内容应该建立主要布局,后续流式传输的块应该理想地适合此结构,而不会导致明显的重排或布局偏移。使用占位符并确保尺寸已知可以帮助防止 CLS。

代码示例:

使用 Suspense 的基本流式渲染

tsx 复制代码
// app/dashboard/page.tsx
import { Suspense } from 'react';
import UserProfile from './components/UserProfile';
import RecentActivity from './components/RecentActivity';
import PopularPosts from './components/PopularPosts';

export default function Dashboard() {
  return (
    <div className="dashboard">
      {/* 这部分加载很快 */}
      <h1>仪表盘</h1>

      {/* 用户资料首先加载 */}
      <Suspense fallback={<div className="skeleton-profile">加载资料中...</div>}>
        <UserProfile />
      </Suspense>

      {/* 最近活动可能需要更长时间 */}
      <Suspense fallback={<div className="skeleton-activity">加载活动中...</div>}>
        <RecentActivity />
      </Suspense>

      {/* 热门帖子可能是最慢的 */}
      <Suspense fallback={<div className="skeleton-posts">加载热门帖子中...</div>}>
        <PopularPosts />
      </Suspense>
    </div>
  );
}

用于更精细控制的嵌套 Suspense 边界

tsx 复制代码
// app/complex-page/page.tsx
import { Suspense } from 'react';

export default function ComplexPage() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Header />

      <div className="content-grid">
        <div className="main-content">
          <Suspense fallback={<MainContentSkeleton />}>
            <MainContent />
          </Suspense>
        </div>

        <div className="sidebar">
          <Suspense fallback={<SidebarTopSkeleton />}>
            <SidebarTopSection />
          </Suspense>

          <Suspense fallback={<SidebarBottomSkeleton />}>
            <SidebarBottomSection />
          </Suspense>
        </div>
      </div>

      <Footer />
    </Suspense>
  );
}

使用 Next.js loading.js 约定

tsx 复制代码
// app/products/loading.tsx - 这将自动用作 Suspense 回退
export default function Loading() {
  return (
    <div className="products-loading-skeleton">
      <div className="header-skeleton" />
      <div className="filters-skeleton" />
      <div className="products-grid-skeleton">
        {Array.from({ length: 12 }).map((_, i) => (
          <div key={i} className="product-card-skeleton" />
        ))}
      </div>
    </div>
  );
}

// app/products/page.tsx
export default async function ProductsPage() {
  // 这个组件可能需要时间加载
  // Next.js 会自动将其包装在 Suspense 中
  // 并使用 loading.js 作为回退
  const products = await fetchProducts();

  return <ProductsList products={products} />;
}

4. 客户端组件和客户端渲染

客户端组件使用 React 的 'use client' 指令定义。它们在服务器上预渲染,然后在客户端水合,从而实现交互性。这与纯客户端渲染 (CSR) 不同,在纯客户端渲染中,渲染完全在浏览器中进行。在传统意义上的 CSR(初始 HTML 很少,所有渲染都在浏览器中进行)中,Next.js 已不再将其作为默认方法,但仍可以通过使用动态导入并设置 ssr: false 来实现。

tsx 复制代码
// app/csr-example/page.tsx
'use client';
import { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';

// 延迟加载一个没有 SSR 的组件
const ClientOnlyComponent = dynamic(
  () => import('../components/heavy-component'),
  { ssr: false, loading: () => <p>加载中...</p> }
);

export default function CSRPage() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <div>
      <h1>客户端渲染页面</h1>
      {isClient ? (
        <ClientOnlyComponent />
      ) : (
        <p>加载客户端组件...</p>
      )}
    </div>
  );
}

尽管向服务器渲染转变,但 CSR 仍有一些有效用例:

  • 私有仪表盘:在 SEO 不重要且您希望减少服务器负载的情况下

  • 重度交互应用:如数据可视化工具或复杂编辑器

  • 仅浏览器 API:当您需要访问特定于浏览器的功能(如 localStorage 或 WebGL)时

  • 第三方集成:一些仅在浏览器中工作的第三方小部件或库

虽然这些是有效用例,但在 Next.js 中,使用客户端组件通常比纯 CSR 更可取。客户端组件为您提供了两全其美的优势:初始加载时的服务器渲染 HTML(改善 SEO 和 LCP)以及水合后的客户端交互性。纯 CSR 应保留给服务器渲染不可能或适得其反的特定场景。

客户端组件适用于:

  • 交互式 UI 元素:表单、下拉菜单、模态框、选项卡
  • 依赖状态的 UI:基于客户端状态更改的组件
  • 浏览器 API 访问:需要 localStorage、地理位置等的组件
  • 事件驱动的交互:点击处理程序、表单提交、动画
  • 实时更新:聊天界面、实时通知

客户端组件如何影响核心网络指标

  • 最大内容绘制 (LCP) :初始 HTML 包含客户端组件的服务器渲染版本,因此 LCP 相当快。水合可能会延迟交互性,但不一定会影响 LCP。
  • 交互到下一次绘制 (INP) :对于客户端组件,水合可能会在页面加载期间导致输入延迟,并且当页面水合时,性能取决于事件处理程序的效率。此外,复杂的状态管理可能会影响响应性。
  • 累积布局偏移 (CLS) :客户端数据获取可能会在新数据到达时导致布局偏移。此外,状态更改可能会意外地改变布局。使用客户端组件需要仔细实现以防止偏移。

代码示例:

基本客户端组件

tsx 复制代码
// app/components/Counter.tsx
'use client';
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

带有服务器数据的客户端组件

tsx 复制代码
// app/products/page.tsx - 服务器组件
import ProductFilter from '../components/ProductFilter';

export default async function ProductsPage() {
  // 在服务器上获取数据
  const products = await fetch('https://api.example.com/products').then(r => r.json());

  // 将服务器数据作为 props 传递给客户端组件
  return <ProductFilter initialProducts={products} />;
}

混合方法和组合模式

在实际应用中,您通常会结合使用多种渲染策略来获得最佳性能。Next.js 使服务器组件和客户端组件的组合变得容易。

带有交互孤岛的服务器组件

最有效的模式之一是将服务器组件用于大部分 UI,并仅在需要交互性的地方添加客户端组件。这种方法:

  • 最小化发送到客户端的 JavaScript

  • 提供出色的初始加载性能

  • 在需要的地方保持良好的交互性

tsx 复制代码
// app/products/[id]/page.tsx - 服务器组件
import AddToCartButton from '../../components/AddToCartButton';
import ProductReviews from '../../components/ProductReviews';
import RelatedProducts from '../../components/RelatedProducts';

export default async function ProductPage({ params }: {
  params: { id: string; }
}) {
  // 在服务器上获取产品数据
  const product = await fetch(`https://api.example.com/products/${params.id}`).then(r => r.json());

  return (
    <div className="product-page">
      <div className="product-main">
        <h1>{product.name}</h1>
        <p className="price">${product.price.toFixed(2)}</p>
        <div className="description">{product.description}</div>

        {/* 用于交互性的客户端组件 */}
        <AddToCartButton product={product} />
      </div>

      {/* 用于产品评论的服务器组件 */}
      <ProductReviews productId={params.id} />

      {/* 用于相关产品的服务器组件 */}
      <RelatedProducts categoryId={product.categoryId} />
    </div>
  );
}

部分预渲染(Next.js 15)

Next.js 15 引入了部分预渲染,这是一种新的混合渲染策略,可在单个路由中结合静态和动态内容。这使您能够:

  • 静态生成页面的框架

  • 流式传输动态、个性化内容

  • 获得静态渲染和动态渲染的最佳效果

注意:在撰写本文时,部分预渲染仍处于实验阶段,尚未准备好投入生产使用。了解更多信息

tsx 复制代码
// app/dashboard/page.tsx
import { unstable_noStore as noStore } from 'next/cache';
import StaticContent from './components/StaticContent';
import DynamicContent from './components/DynamicContent';

export default function Dashboard() {
  return (
    <div className="dashboard">
      {/* 这部分是静态生成的 */}
      <StaticContent />

      {/* 这部分是动态渲染的 */}
      <DynamicPart />
    </div>
  );
}

// 这个组件及其子组件将被动态渲染
function DynamicPart() {
  // 为此部分取消缓存
  noStore();

  return <DynamicContent />;
}

在 Next.js 中测量核心网络指标

了解渲染策略选择的影响需要在实际条件下测量核心网络指标。以下是一些方法:

1. Vercel 分析

如果您在 Vercel 上部署,您可以使用 Vercel 分析来自动跟踪生产站点的核心网络指标:

tsx 复制代码
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';

export default function RootLayout({ children }: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

2. Web Vitals API

您可以使用 web-vitals 库手动跟踪核心网络指标:

tsx 复制代码
// app/components/WebVitalsReporter.tsx
'use client';

import { useEffect } from 'react';
import { onCLS, onINP, onLCP } from 'web-vitals';

export function WebVitalsReporter() {
  useEffect(() => {
    // 报告核心网络指标
    onCLS(metric => console.log('CLS:', metric.value));
    onINP(metric => console.log('INP:', metric.value));
    onLCP(metric => console.log('LCP:', metric.value));

    // 在实际应用中,您会将这些发送到您的分析服务
  }, []);

  return null; // 这个组件不渲染任何内容
}

3. Lighthouse 和 PageSpeed Insights

对于开发和测试,使用:

  • Chrome DevTools 中的 Lighthouse 选项卡
  • PageSpeed Insights
  • Chrome 用户体验报告

做出实际决策:选择哪种渲染策略?

选择正确的渲染策略取决于您的具体要求。以下是一个决策框架:

选择静态渲染时

  • 内容对所有用户都相同
  • 数据可以在构建时确定
  • 页面不需要频繁更新
  • SEO 至关重要
  • 您希望获得最佳性能

选择动态渲染时

  • 内容为每个用户个性化
  • 数据必须在每次请求时都是最新的
  • 您需要访问请求时的信息
  • 内容频繁更改

选择流式渲染时

  • 页面有快速和慢速数据需求的混合
  • 您希望改善感知性能
  • 页面的某些部分依赖于慢速 API
  • 您希望优先显示关键 UI

选择客户端组件时

  • UI 需要是交互式的
  • 组件依赖于浏览器 API
  • UI 基于用户输入频繁更改
  • 您需要实时更新

结论

Next.js 提供了一组强大的渲染策略,使您能够针对性能和用户体验进行优化。通过了解每种策略如何影响核心网络指标,您可以就是否构建应用程序做出明智的决策。

请记住,最佳方法通常是混合方法,根据应用程序每个部分的特定要求组合不同的渲染策略。默认情况下从服务器组件开始,在可能的情况下使用静态渲染,并且仅在需要交互性的地方添加客户端组件。

通过遵循这些原则并测量您的核心网络指标,您可以创建快速、响应迅速并提供出色用户体验的 Next.js 应用程序。

相关推荐
Carlos_sam35 分钟前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖44 分钟前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby1 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js
itslife1 小时前
Fiber 架构
前端·react.js
3Katrina1 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
hubber1 小时前
一次 SPA 架构下的性能优化实践
前端
可乐只喝可乐2 小时前
从0到1构建一个Agent智能体
前端·typescript·agent
Muxxi2 小时前
shopify模板开发
前端
Yueyanc2 小时前
LobeHub桌面应用的IPC通信方案解析
前端·javascript
我是若尘2 小时前
利用资源提示关键词优化网页加载速度
前端