Nuxt 3 性能优化实战案例完全指南

HTMLPAGE 团队
28分钟 分钟阅读

通过真实项目案例,深入讲解 Nuxt 3 性能优化的核心策略,包括构建优化、渲染优化、数据获取优化、部署优化等实战技巧。

#Nuxt #性能优化 #Core Web Vitals #SSR #构建优化

案例背景

本文以一个真实的内容型网站为例,介绍 Nuxt 3 性能优化的完整过程。该网站初始性能数据如下:

指标优化前目标值
LCP4.2s<2.5s
FID180ms<100ms
CLS0.25<0.1
首屏时间3.8s<1.5s
构建体积2.1MB<500KB

通过系统性优化,最终达成了所有目标。以下是详细的优化过程。

构建优化

分析构建产物

首先需要了解当前的包体积构成:

// nuxt.config.ts
export default defineNuxtConfig({
  build: {
    analyze: true
  }
})

运行 nuxt build --analyze 后,发现以下问题:

  • lodash 完整引入(200KB)
  • moment.js 包含所有语言包(300KB)
  • 未使用的 UI 组件库组件(150KB)

Tree Shaking 优化

优化 lodash

// ❌ 优化前:完整引入
import _ from 'lodash'
_.debounce(fn, 300)

// ✅ 优化后:按需引入
import debounce from 'lodash/debounce'
debounce(fn, 300)

// 或使用 lodash-es
import { debounce } from 'lodash-es'

替换 moment.js

// ❌ 优化前:moment.js (300KB)
import moment from 'moment'
moment().format('YYYY-MM-DD')

// ✅ 优化后:dayjs (2KB)
import dayjs from 'dayjs'
dayjs().format('YYYY-MM-DD')

代码分割策略

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          // 手动分包
          manualChunks: (id) => {
            // Vue 相关
            if (id.includes('vue') || id.includes('@vue')) {
              return 'vue-vendor'
            }
            // UI 库
            if (id.includes('element-plus') || id.includes('@element-plus')) {
              return 'ui-vendor'
            }
            // 工具库
            if (id.includes('lodash') || id.includes('dayjs')) {
              return 'utils-vendor'
            }
          }
        }
      },
      // 分块大小警告阈值
      chunkSizeWarningLimit: 500
    }
  }
})

组件按需加载

// nuxt.config.ts
export default defineNuxtConfig({
  components: {
    dirs: [
      {
        path: '~/components',
        // 不自动引入的目录
        ignore: ['**/legacy/**']
      }
    ]
  }
})

对于大型组件使用动态导入:

<template>
  <div>
    <!-- 懒加载重型组件 -->
    <LazyRichTextEditor v-if="showEditor" />
    
    <!-- 条件渲染时才加载 -->
    <ClientOnly>
      <LazyChartComponent :data="chartData" />
    </ClientOnly>
  </div>
</template>

<script setup>
// 使用 defineAsyncComponent 更精细控制
const HeavyComponent = defineAsyncComponent({
  loader: () => import('~/components/HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,
  timeout: 10000
})
</script>

渲染优化

渲染模式选择

根据页面特性选择合适的渲染模式:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 首页:SSG,构建时生成
    '/': { prerender: true },
    
    // 博客文章:ISR,每小时更新
    '/blog/**': { isr: 3600 },
    
    // 用户中心:SPA,客户端渲染
    '/account/**': { ssr: false },
    
    // 产品页:SSR,每次请求渲染
    '/products/**': { ssr: true },
    
    // API 路由:无缓存
    '/api/**': { 
      cors: true,
      headers: { 'Cache-Control': 'no-cache' }
    }
  }
})

预渲染优化

对于静态内容优先使用预渲染:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      // 预渲染这些路由
      routes: [
        '/',
        '/about',
        '/contact',
        '/pricing'
      ],
      // 爬取发现的链接
      crawlLinks: true,
      // 忽略错误继续
      failOnError: false
    }
  }
})

Payload 优化

减少 SSR 传递给客户端的数据:

<script setup>
// ❌ 传递完整对象(包含不需要的字段)
const { data: posts } = await useFetch('/api/posts')

// ✅ 只获取需要的字段
const { data: posts } = await useFetch('/api/posts', {
  pick: ['id', 'title', 'summary', 'date'],
  transform: (data) => {
    // 转换数据,减少体积
    return data.map(post => ({
      id: post.id,
      title: post.title,
      summary: post.summary?.slice(0, 100),
      date: post.date
    }))
  }
})
</script>

组件缓存

使用 <KeepAlive> 缓存组件状态:

<template>
  <NuxtLayout>
    <NuxtPage :keepalive="{ 
      include: ['ProductList', 'SearchResults'],
      max: 10 
    }" />
  </NuxtLayout>
</template>

数据获取优化

并行数据获取

<script setup>
// ❌ 串行请求(慢)
const { data: user } = await useFetch('/api/user')
const { data: posts } = await useFetch('/api/posts')
const { data: notifications } = await useFetch('/api/notifications')

// ✅ 并行请求(快)
const [
  { data: user },
  { data: posts },
  { data: notifications }
] = await Promise.all([
  useFetch('/api/user'),
  useFetch('/api/posts'),
  useFetch('/api/notifications')
])
</script>

智能缓存策略

<script setup>
// 配置详细的缓存策略
const { data: products } = await useFetch('/api/products', {
  // 缓存键
  key: 'products-list',
  
  // 获取缓存的数据后立即渲染,同时后台刷新
  getCachedData(key) {
    const cached = nuxtApp.payload.data[key] || nuxtApp.static.data[key]
    if (cached) {
      // 检查缓存是否过期(5分钟)
      const cacheTime = nuxtApp.payload._cache?.[key]
      if (cacheTime && Date.now() - cacheTime < 5 * 60 * 1000) {
        return cached
      }
    }
    return null
  },
  
  // 请求去重
  dedupe: 'cancel'
})
</script>

分页数据懒加载

<script setup>
const page = ref(1)
const pageSize = 20
const allItems = ref([])

// 初始数据
const { data: initialData } = await useFetch('/api/items', {
  query: { page: 1, pageSize }
})
allItems.value = initialData.value?.items || []

// 加载更多
const loadMore = async () => {
  page.value++
  
  const { data } = await useFetch('/api/items', {
    query: { page: page.value, pageSize },
    key: `items-page-${page.value}`
  })
  
  if (data.value?.items) {
    allItems.value.push(...data.value.items)
  }
}
</script>

图片优化

Nuxt Image 配置

// nuxt.config.ts
export default defineNuxtConfig({
  image: {
    // 图片质量
    quality: 80,
    
    // 响应式断点
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
      xxl: 1536
    },
    
    // 预设配置
    presets: {
      // 博客封面图
      cover: {
        modifiers: {
          format: 'webp',
          fit: 'cover',
          quality: 80
        }
      },
      // 缩略图
      thumbnail: {
        modifiers: {
          format: 'webp',
          width: 300,
          height: 200,
          fit: 'cover',
          quality: 70
        }
      },
      // 头像
      avatar: {
        modifiers: {
          format: 'webp',
          width: 100,
          height: 100,
          fit: 'cover',
          quality: 75
        }
      }
    },
    
    // CDN 配置
    domains: ['cdn.example.com', 'images.unsplash.com']
  }
})

图片组件使用

<template>
  <!-- 响应式图片 -->
  <NuxtImg
    src="/images/hero.jpg"
    alt="Hero Image"
    sizes="sm:100vw md:50vw lg:400px"
    :placeholder="[50, 25, 75, 5]"
    loading="lazy"
    format="webp"
  />
  
  <!-- 使用预设 -->
  <NuxtImg
    :src="post.cover"
    preset="cover"
    :alt="post.title"
  />
  
  <!-- 关键图片优先加载 -->
  <NuxtImg
    src="/images/logo.svg"
    :loading="'eager'"
    fetchpriority="high"
  />
</template>

LCP 优化

确保最大内容绘制元素快速加载:

<template>
  <header>
    <!-- LCP 元素:优先加载 -->
    <NuxtImg
      :src="heroImage"
      alt="Hero"
      :loading="'eager'"
      fetchpriority="high"
      :preload="true"
    />
  </header>
</template>

<script setup>
// 预加载关键资源
useHead({
  link: [
    {
      rel: 'preload',
      href: '/images/hero.webp',
      as: 'image',
      type: 'image/webp'
    },
    {
      rel: 'preload',
      href: '/fonts/main.woff2',
      as: 'font',
      type: 'font/woff2',
      crossorigin: 'anonymous'
    }
  ]
})
</script>

字体优化

字体加载策略

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      link: [
        // 预连接字体服务
        { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
        { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
        
        // 异步加载字体
        {
          rel: 'stylesheet',
          href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
          media: 'print',
          onload: "this.media='all'"
        }
      ]
    }
  }
})

本地字体优化

/* assets/css/fonts.css */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* 关键:避免 FOIT */
}

/* 字体子集化:只包含中文常用字符 */
@font-face {
  font-family: 'ChineseFont';
  src: url('/fonts/chinese-subset.woff2') format('woff2');
  font-weight: 400;
  unicode-range: U+4E00-9FFF; /* CJK 基本区 */
  font-display: swap;
}

CLS 优化

预留空间

<template>
  <!-- 为图片预留空间 -->
  <div class="aspect-video relative">
    <NuxtImg
      :src="image"
      class="absolute inset-0 w-full h-full object-cover"
      loading="lazy"
    />
  </div>
  
  <!-- 为动态内容预留空间 -->
  <div class="min-h-[400px]">
    <ClientOnly>
      <AdBanner />
      <template #fallback>
        <div class="h-[400px] bg-gray-100 animate-pulse" />
      </template>
    </ClientOnly>
  </div>
</template>

骨架屏

<!-- components/PostListSkeleton.vue -->
<template>
  <div class="space-y-4">
    <div 
      v-for="i in 5" 
      :key="i"
      class="flex gap-4 animate-pulse"
    >
      <div class="w-24 h-24 bg-gray-200 rounded" />
      <div class="flex-1 space-y-2">
        <div class="h-4 bg-gray-200 rounded w-3/4" />
        <div class="h-4 bg-gray-200 rounded w-1/2" />
        <div class="h-4 bg-gray-200 rounded w-1/4" />
      </div>
    </div>
  </div>
</template>
<!-- pages/posts/index.vue -->
<template>
  <div>
    <PostListSkeleton v-if="pending" />
    <PostList v-else :posts="posts" />
  </div>
</template>

<script setup>
const { data: posts, pending } = await useFetch('/api/posts', {
  lazy: true // 客户端延迟加载
})
</script>

网络优化

HTTP/2 Push

// server/middleware/push.ts
export default defineEventHandler((event) => {
  const path = event.path
  
  // 为首页推送关键资源
  if (path === '/') {
    setHeader(event, 'Link', [
      '</css/critical.css>; rel=preload; as=style',
      '</js/app.js>; rel=preload; as=script'
    ].join(', '))
  }
})

缓存策略

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 静态资源长期缓存
    '/_nuxt/**': {
      headers: {
        'Cache-Control': 'public, max-age=31536000, immutable'
      }
    },
    
    // HTML 页面短期缓存
    '/**': {
      headers: {
        'Cache-Control': 'public, max-age=300, stale-while-revalidate=600'
      }
    },
    
    // API 按需缓存
    '/api/products': {
      cache: {
        maxAge: 60,
        staleMaxAge: 300
      }
    }
  }
})

监控与分析

性能指标采集

// plugins/performance.client.ts
export default defineNuxtPlugin(() => {
  if (typeof window === 'undefined') return
  
  // Core Web Vitals
  const reportWebVitals = (metric) => {
    console.log(metric)
    
    // 发送到分析服务
    fetch('/api/analytics/vitals', {
      method: 'POST',
      body: JSON.stringify({
        name: metric.name,
        value: metric.value,
        id: metric.id,
        page: window.location.pathname
      }),
      keepalive: true
    })
  }
  
  // 使用 web-vitals 库
  import('web-vitals').then(({ onCLS, onFID, onLCP, onFCP, onTTFB }) => {
    onCLS(reportWebVitals)
    onFID(reportWebVitals)
    onLCP(reportWebVitals)
    onFCP(reportWebVitals)
    onTTFB(reportWebVitals)
  })
})

性能预算

// 构建时检查性能预算
// scripts/check-bundle-size.js
import { readFileSync, readdirSync } from 'fs'
import { join } from 'path'

const BUDGET = {
  total: 500 * 1024,       // 500KB
  'vue-vendor': 100 * 1024, // 100KB
  'ui-vendor': 150 * 1024,  // 150KB
}

const distDir = '.output/public/_nuxt'
const files = readdirSync(distDir)

let totalSize = 0
const violations = []

files.forEach(file => {
  if (file.endsWith('.js')) {
    const size = readFileSync(join(distDir, file)).length
    totalSize += size
    
    // 检查单个 chunk 预算
    Object.entries(BUDGET).forEach(([name, budget]) => {
      if (file.includes(name) && size > budget) {
        violations.push(`${file}: ${(size/1024).toFixed(2)}KB > ${budget/1024}KB`)
      }
    })
  }
})

if (totalSize > BUDGET.total) {
  violations.push(`Total: ${(totalSize/1024).toFixed(2)}KB > ${BUDGET.total/1024}KB`)
}

if (violations.length > 0) {
  console.error('性能预算超标:')
  violations.forEach(v => console.error(`  - ${v}`))
  process.exit(1)
}

console.log(`✅ 构建体积: ${(totalSize/1024).toFixed(2)}KB`)

优化成果

通过以上优化,最终达成目标:

指标优化前优化后改善幅度
LCP4.2s1.8s-57%
FID180ms45ms-75%
CLS0.250.05-80%
首屏时间3.8s1.2s-68%
构建体积2.1MB380KB-82%

最佳实践清单

类别优化措施优先级
构建Tree Shaking + 按需引入
构建合理的代码分割策略
渲染根据页面特性选择渲染模式
渲染预渲染静态页面
数据并行数据获取
数据智能缓存策略
图片WebP + 响应式 + 懒加载
字体font-display: swap
CLS预留空间 + 骨架屏
网络合理的缓存头配置

总结

Nuxt 3 性能优化需要系统性思考,从构建、渲染、数据、资源多个维度入手。核心原则:

  1. 减少体积 - Tree Shaking、代码分割、压缩
  2. 加速渲染 - 选择合适的渲染模式、预渲染
  3. 优化资源 - 图片压缩、字体子集化、预加载
  4. 持续监控 - 采集指标、设置预算、CI/CD 集成

通过持续优化,可以显著提升用户体验和 SEO 表现。