概述

性能优化是前端开发中的重要环节。对于 Nuxt 应用而言,由于涉及服务端渲染、静态生成、客户端水合等多个环节,性能优化需要从多个维度入手。本文记录了一些实践中的优化策略。

性能指标

Core Web Vitals

Google 定义的 Core Web Vitals 是衡量网页用户体验的核心指标:

指标全称含义良好标准
LCPLargest Contentful Paint最大内容绘制时间≤ 2.5s
INPInteraction to Next Paint交互到下次绘制≤ 200ms
CLSCumulative Layout Shift累积布局偏移≤ 0.1

测量工具

图片优化

图片通常是网页中最大的资源,优化图片对性能提升最为显著。

@nuxt/image 模块

官方文档:https://image.nuxt.com

// nuxt.config.ts
image: {
  format: ['webp', 'avif', 'jpg'],
  quality: 80,
  screens: {
    xs: 320,
    sm: 640,
    md: 768,
    lg: 1024,
    xl: 1280,
    xxl: 1536
  }
}

响应式图片

使用 NuxtImg 组件自动生成响应式图片:

<template>
  <NuxtImg
    :src="imageUrl"
    :alt="alt"
    format="webp"
    quality="80"
    loading="lazy"
    sizes="sm:100vw md:50vw lg:800px"
  />
</template>

生成的 HTML 会包含 srcsetsizes 属性,浏览器会根据设备选择最合适的图片。

首屏图片优先加载

<template>
  <!-- 首屏图片:优先加载 -->
  <NuxtImg
    :src="heroImage"
    :alt="heroAlt"
    loading="eager"
    fetchpriority="high"
    format="webp"
    quality="90"
  />
  
  <!-- 非首屏图片:懒加载 -->
  <NuxtImg
    v-for="img in gallery"
    :key="img.id"
    :src="img.url"
    loading="lazy"
    format="webp"
  />
</template>

轮播图优化

对于轮播图组件,首张图片优先加载,其余图片懒加载:

<template>
  <div class="hero-slider">
    <div
      v-for="(slide, index) in slides"
      :key="index"
      class="slide"
    >
      <NuxtImg
        :src="slide.image"
        :alt="slide.alt"
        :loading="index === 0 ? 'eager' : 'lazy'"
        :fetchpriority="index === 0 ? 'high' : 'auto'"
        format="webp"
        quality="90"
      />
    </div>
  </div>
</template>

IPX 配置

@nuxt/image 使用 IPX 进行服务端图片处理。在生产环境中,需要排除 IPX 路由的预渲染:

// nuxt.config.ts
nitro: {
  prerender: {
    ignore: ['/_ipx/**']
  }
}

IPX 路由是动态的,依赖请求参数生成图片,预渲染时访问这些路由会导致构建失败。

代码分割

路由级代码分割

Nuxt 默认按页面分割代码,每个页面组件会生成独立的 chunk:

pages/
├── index.vue      → index.[hash].js
├── about.vue      → about.[hash].js
└── news/
    ├── index.vue  → news-index.[hash].js
    └── [slug].vue → news-slug.[hash].js

组件懒加载

对于大型组件或非首屏组件,使用懒加载:

<template>
  <div>
    <!-- 首屏组件:直接导入 -->
    <AppHeader />
    
    <!-- 非首屏组件:懒加载 -->
    <LazyModal v-if="showModal" />
    <LazyGallery v-if="showGallery" />
  </div>
</template>

Lazy 前缀会让 Nuxt 自动将组件分离为独立的 chunk,在需要时才加载。

条件加载模块

某些模块体积较大但使用频率低,可以条件加载:

// nuxt.config.ts
const enableFeature = process.env.ENABLE_FEATURE === 'true'

export default defineNuxtConfig({
  modules: [
    enableFeature ? 'heavy-module' : undefined
  ].filter(Boolean)
})

Bundle 分析

使用 rollup-plugin-visualizer 分析打包产物:

// nuxt.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineNuxtConfig({
  vite: {
    plugins: process.env.ANALYZE === 'true'
      ? [
          visualizer({
            filename: 'stats.html',
            gzipSize: true,
            brotliSize: true,
            open: true
          })
        ]
      : []
  }
})

运行 ANALYZE=true npm run build 会生成可视化报告,帮助识别大型依赖。

缓存策略

ISR(增量静态再生)

// nuxt.config.ts
routeRules: {
  // 静态页面:预渲染
  '/': { prerender: true },
  '/about': { prerender: true },
  
  // 动态内容:ISR
  '/news': { swr: 3600 },      // 1 小时
  '/news/**': { swr: 3600 },
  '/api/news': { swr: 1800 },  // 30 分钟
}

SWR(Stale-While-Revalidate)的工作流程:

  1. 请求到达时检查缓存是否存在
  2. 如果存在,立即返回缓存内容
  3. 检查缓存是否过期
  4. 如果过期,在返回旧缓存的同时,后台重新渲染
  5. 渲染完成后更新缓存
注意:开发时应禁用缓存,否则修改内容后可能看不到更新。
routeRules: {
  '/news': process.env.NODE_ENV === 'development' 
    ? {} 
    : { swr: 3600 }
}

API 响应缓存

对于服务端 API,同样可以应用缓存:

routeRules: {
  '/api/news': { swr: 1800 },    // 30 分钟
  '/api/cars': { swr: 7200 },    // 2 小时
  '/api/events': { swr: 3600 }   // 1 小时
}

内存缓存

对于频繁访问的数据,可以在服务端实现内存缓存:

// server/utils/cache.ts
interface CacheItem<T> {
  data: T
  timestamp: number
}

const cache = new Map<string, CacheItem<any>>()

export function useCache<T>(
  key: string, 
  ttl: number, 
  fetcher: () => Promise<T>
): Promise<T> {
  const cached = cache.get(key)
  const now = Date.now()
  
  if (cached && (now - cached.timestamp) < ttl) {
    return Promise.resolve(cached.data)
  }
  
  return fetcher().then(data => {
    cache.set(key, { data, timestamp: now })
    return data
  })
}

字体优化

字体加载策略

// nuxt.config.ts
fonts: {
  providers: {
    google: false  // 禁用 Google Fonts(国内网络问题)
  },
  families: [
    {
      name: 'Inter',
      src: '/fonts/Inter/Inter-VariableFont_opsz-wght.ttf'
    },
    {
      name: 'Outfit',
      src: '/fonts/Outfit/Outfit-VariableFont_wght.ttf'
    }
  ]
}

官方文档:https://fonts.nuxt.com

字体预加载

对于首屏渲染必需的字体,使用 <link rel="preload"> 预加载:

// nuxt.config.ts
app: {
  head: {
    link: [
      {
        rel: 'preload',
        href: '/fonts/Inter/Inter-VariableFont_opsz-wght.ttf',
        as: 'font',
        type: 'font/ttf',
        crossorigin: 'anonymous'
      }
    ]
  }
}

font-display 策略

在 CSS 中设置 font-display 控制字体加载行为:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter/Inter-VariableFont_opsz-wght.ttf') format('truetype');
  font-display: swap;
}

font-display: swap 会立即显示后备字体,字体加载后平滑过渡,不阻塞文本渲染。

CSS 优化

CSS 变量

使用 CSS 变量实现主题切换,避免重复加载样式表:

/* variables.css */
:root {
  --primary-color: #3b82f6;
  --text-color: #1f2937;
  --bg-color: #ffffff;
}

[data-theme="dark"] {
  --primary-color: #60a5fa;
  --text-color: #f3f4f6;
  --bg-color: #111827;
}

Tailwind CSS

// nuxt.config.ts
vite: {
  css: {
    devSourcemap: false  // 消除 Tailwind CSS v4 sourcemap 警告
  }
}

Tailwind CSS 文档:https://tailwindcss.com

JavaScript 优化

减少客户端 JavaScript

Nuxt 的 SSR 特性天然减少了客户端 JavaScript 的执行:首屏 HTML 在服务端渲染,客户端仅需水合交互逻辑。

避免不必要的水合

对于纯静态内容,可以使用 <ClientOnly> 组件:

<template>
  <ClientOnly>
    <HeavyInteractiveComponent />
  </ClientOnly>
</template>

延迟加载第三方脚本

// nuxt.config.ts
app: {
  head: {
    script: [
      {
        src: 'https://analytics.example.com/script.js',
        defer: true,
        async: true
      }
    ]
  }
}

预渲染优化

并发控制

当使用 Nuxt Content v3 时,预渲染需要控制并发:

// nuxt.config.ts
nitro: {
  prerender: {
    concurrency: 1,  // 串行处理,避免 SQLite 冲突
    failOnError: true,
    crawlLinks: true
  }
}

构建内存优化

大型项目构建时可能遇到内存不足:

NODE_OPTIONS='--max-old-space-size=6144' npm run build

将 Node.js 堆内存上限设为 6GB,避免 Nitro 打包阶段 OOM。

jemalloc

sharp/libvips 在某些环境下可能因内存分配器问题崩溃,使用 jemalloc 可以解决:

# 安装 jemalloc
sudo apt-get install -y libjemalloc2

# 构建时使用
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 npm run build

运行时性能

虚拟列表

对于长列表,使用虚拟列表只渲染可见项:

<template>
  <VirtualScroller :items="items" :item-height="80">
    <template #default="{ item }">
      <ItemCard :item="item" />
    </template>
  </VirtualScroller>
</template>

防抖与节流

对于频繁触发的事件,使用防抖或节流:

// composables/useDebounce.ts
export function useDebounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): T {
  let timeoutId: ReturnType<typeof setTimeout>
  
  return ((...args: Parameters<T>) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => fn(...args), delay)
  }) as T
}

动画性能

使用 CSS 动画和 transform 属性,避免触发重排:

/* 推荐:使用 transform */
.card {
  transition: transform 0.3s ease;
}
.card:hover {
  transform: translateY(-5px);
}

性能监控

真实用户监控(RUM)

使用 web-vitals 库收集真实用户数据:

// plugins/web-vitals.ts
import { onLCP, onINP, onCLS } from 'web-vitals'

export default defineNuxtPlugin(() => {
  onLCP(console.log)
  onINP(console.log)
  onCLS(console.log)
})

web-vitals 库:https://github.com/GoogleChrome/web-vitals

优化清单

首屏优化

  • 首屏图片使用 loading="eager"fetchpriority="high"
  • 关键字体预加载
  • 延迟非关键 JavaScript
  • 使用 SSR 或预渲染

资源优化

  • 图片使用 WebP/AVIF 格式
  • 响应式图片(srcset)
  • 图片懒加载
  • 代码分割和懒加载
  • 第三方脚本异步加载

缓存优化

  • 静态页面预渲染
  • 动态页面使用 ISR
  • API 响应缓存
  • CDN 部署

运行时优化

  • 长列表虚拟滚动
  • 事件防抖节流
  • 动画使用 transform
  • 避免强制同步布局

小结

性能优化是一个持续的过程,需要从多个维度入手。测量先行,使用 Lighthouse、WebPageTest 等工具建立基准。图片优化通常是最容易获得显著收益的优化点。代码分割可以减少首屏 JavaScript 体积。合理使用预渲染和 ISR 缓存策略,关注交互响应和动画流畅度。


参考资料: