Next.js 性能优化:从 50 分到 99 分的调优指南

Next.js 性能优化:从 50 分到 99 分的调优指南

在如今的 Web 开发中,“快”不仅仅是用户体验,更是 SEO 排名和转化率的生命线。很多开发者做出来的 Next.js 网站,功能很强,UI 很炫,但一跑 Lighthouse 惨不忍睹,甚至在低端设备上卡顿明显。

尤其是 LCP (最大内容绘制)CLS (累积布局偏移),往往是 Next.js 应用的重灾区。

今天我们不聊虚的理论,不背 HTTP 缓存协议。直接针对 Next.js 框架特性,给出一套实战优化组合拳,看看如何把一个 50 分的网站优化到 90+。

1. LCP 杀手:极致的图片加载策略

LCP 衡量视口中最大内容的渲染时间。对于大多数电商或博客网站,这通常是首屏的 Hero Banner 或主商品图。

❌ 常见误区

  • 盲目懒加载:给首屏大图加 loading="lazy"。这听起来是优化,实则是大错特错。浏览器会等到确定图片在视口内才开始加载,这不会节省带宽,只会浪费宝贵的并发下载时间。
  • 使用普通 <img> 标签:没有 SRCSET,导致移动端下载了 4K 分辨率的 PC 端大图。

✅ 正确姿势:Priority 与 Sizes

Next.js 的 <Image> 组件是性能优化的神器,前提是你要会用。

Image Priority Strategy

<div className="relative h-[500px] w-full">
  <Image
    src="/hero-banner.png"
    alt="Hero Banner"
    fill
    style={{ objectFit: 'cover' }}
    priority={true} // 优化点 1:LCP 强心剂
    sizes="(max-width: 768px) 100vw, 
           (max-width: 1200px) 50vw, 
           33vw"   // 优化点 2:按需下载
    quality={85}    // 优化点 3:平衡画质与体积
    placeholder="blur" // 优化点 4:视觉稳定性
    blurDataURL="data:image/..." 
  />
</div>

深度解析

  1. priority={true}:这会给 <img /> 加上 fetchPriority="high" 属性,并在 <head> 中生成 preload 链接。它告诉浏览器:“别管 JS 了,先下载这张图!”这对提升 LCP 立竿见影。
  2. sizes:很多开发者只写宽和高,忽略了 sizessizes 属性告诉浏览器在不同屏幕宽度下,这张图片大概有多宽。配合 Next.js 自动生成的 srcset,浏览器就能智能选择下载 300px 的缩略图还是 1024px 的大图。

2. CLS 杀手:字体加载与 FOUT/FOIT

你有没有遇到过:页面打开,文字先是系统默认字体(比如宋体),0.5 秒后突然闪烁变成了你自定义的可爱字体?或者更惨,文字隐形了 0.5 秒才出来?

这就是 FOUT (Flash of Unstyled Text)FOIT (Flash of Invisible Text)。它们不仅难看,还会导致内容高度变化,引发布局偏移 (CLS)。

✅ 解决方案:next/font

next/font 是 Next.js 13 引入的杀手级特性。它会在 构建时 (Build Time) 自动下载 Google Fonts 或本地字体,并将字体文件和 CSS 内联到项目中。

这意味着:没有任何发往 fonts.googleapis.com 的外部请求。你的字体和 HTML 是同时到达用户浏览器的。

更神奇的是它的 adjustFontFallback 特性。

import { Inter, Lora } from 'next/font/google';

// 定义字体,开启 subset 减少体积
const inter = Inter({ 
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

const lora = Lora({
  subsets: ['latin'],
  variable: '--font-lora',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${lora.variable}`}>
      <body className="font-sans antialiased">
        {children}
      </body>
    </html>
  );
}

Next.js 会自动计算系统字体(Arial/Times)和你的自定义字体(Inter/Lora)之间的宽高比差异,通过 CSS size-adjust 属性,强制让后备字体占据和自定义字体一模一样的空间。这样,即使自定义字体没加载完,布局也不会抖动!

3. 架构级优化:局部预渲染 (PPR)

如果说前面的优化是修修补补,那么 局部预渲染 (Partial Prerendering, PPR) 就是架构层面的降维打击。

在 Next.js 14 之前,我们只能二选一:

  • 静态 (SSG):极致速度,CDN 边缘缓存。但数据陈旧,无法千人千面。
  • 动态 (SSR):实时数据,个性化。但首屏慢,因为要等数据库查询完才返回 HTML。TTFB (首字节时间) 高导致 LCP 高。

PPR 允许你将两者 混合

Partial Prerendering Architecture

import { Suspense } from 'react';
import { ProductSkeleton } from './skeletons';
import { RecommendedProducts } from './recommendations';

// 页面外壳:这是静态的!瞬间返回 200 OK!
export default function Page() {
  return (
    <main>
      <h1>My Static Product Page</h1>
      <p>This part is static and served from CDN instantly.</p>
      
      {/* 动态空洞:这是动态的! */}
      <Suspense fallback={<ProductSkeleton />}>
        {/* 这个组件里包含了 await db.query() */}
        <RecommendedProducts />
      </Suspense>
    </main>
  );
}

开启 PPR 后,用户请求页面,服务器会 立刻 返回静态外壳(包含导航栏、Footer、静态文案和骨架屏)。浏览器立刻开始渲染,LCP 几乎为 0。同时,服务器并行开始计算动态部分 (Stream),计算好一点就通过 HTTP Stream 推送一点给浏览器填补空洞。

这是目前 Web 性能优化的终极形态。

4. 第三方脚本噩梦:Script 优化

我们免不了要接入 Google Analytics、Meta Pixel、客服插件等。这些第三方脚本往往体积巨大、加载缓慢,且阻塞主线程。

Next.js 的 <Script> 组件提供了 strategy 属性来管理加载时机。

import Script from 'next/script';

// strategy="afterInteractive" (默认):页面水合后立即加载 (适合 GA)
<Script src="https://www.google-analytics.com/..." />

// strategy="lazyOnload": 浏览器空闲时才加载 (适合客服聊天窗口)
// 这能显著降低 TBT (Total Blocking Time)
<Script 
  src="https://chat-widget.com/..." 
  strategy="lazyOnload"
/>

// strategy="worker" (实验性): 将脚本扔到 Web Worker 中运行
// 完全脱离主线程,适合纯计算逻辑
<Script 
  src="..." 
  strategy="worker"
/>

5. 减肥药:@next/bundle-analyzer

有时候你的 JS 包体积变大,仅仅是因为你不小心引入了一个巨大的库(比如引入了完整的 lodash 而不是 lodash/get,或者引入了整个 moment.js)。

定期运行 Bundle Analyzer 是必修课。

  1. 安装:npm install @next/bundle-analyzer
  2. 配置 next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // your next config
});
  1. 运行:ANALYZE=true npm run build

你会看到一张五颜六色的矩形图。对着最大的方块切下去。常见的优化手段:

  • Tree Shaking:确保只引入需要的函数 (Named Import)。
  • Dynamic Import:对于首屏不需要的重型组件(如富文本编辑器、3D 模型查看器),使用 next/dynamic 懒加载。
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Loading Chart...</p>,
  ssr: false, // 如果不需要 SEO,关掉 SSR 可以进一步减小 HTML 体积
});

总结

性能优化不是玄学,是对浏览器渲染机制的尊重。记住三条原则:

  1. 重要的东西优先加载 (LCP / Priority / Preload)。
  2. 占好位置别乱动 (CLS / Fonts / 宽高比)。
  3. 别堵塞主线程 (Script Strategy / Web Worker)。

只要做对了这几点,你的 Next.js 应用不仅 Lighthouse 分数高,更能给用户带来丝般顺滑的体验。