返回 Skill 列表
extension
分类: 开发与工程无需 API Key

vue-nuxt-best-practices

57个针对Vue3和Nuxt4应用的性能优化规则。涵盖了服务器端渲染、数据获取、响应式系统、组件设计、状态管理和打包优化等内容。

person作者: jakexiaohubgithub

Vue3 & Nuxt4 Best Practices

You are an expert Vue3 and Nuxt4 developer. When writing, reviewing, or refactoring code, always apply these best practices.

Priority Levels

  • CRITICAL: Must follow, violations cause bugs or major performance issues
  • HIGH: Strongly recommended, significant impact on performance/maintainability
  • MEDIUM: Recommended for better code quality
  • LOW: Nice to have, advanced optimizations

1. SSR & Hydration (Critical)

Avoid Hydration Mismatch

Server and client renders must produce identical HTML.

<!-- ❌ BAD: Different values on server vs client -->
<template>
  <div>{{ Date.now() }}</div>
  <div>{{ Math.random() }}</div>
  <div>{{ window.innerWidth }}</div>
</template>

<!-- ✅ GOOD: Use ClientOnly or onMounted -->
<template>
  <ClientOnly>
    <div>{{ currentTime }}</div>
    <template #fallback>Loading...</template>
  </ClientOnly>
</template>

<script setup lang="ts">
const currentTime = ref<number | null>(null)
onMounted(() => {
  currentTime.value = Date.now()
})
</script>

Use import.meta.client for Environment Checks

// ✅ GOOD: Nuxt 3/4 recommended way
if (import.meta.client) {
  // Client-only code
  initAnalytics()
}

if (import.meta.server) {
  // Server-only code
}

// ❌ BAD: Deprecated
if (process.client) { }

Use onMounted for Browser APIs

// ❌ BAD: Crashes on server
const width = window.innerWidth

// ✅ GOOD: Safe for SSR
const width = ref(0)
onMounted(() => {
  width.value = window.innerWidth
})

// ✅ BETTER: Use VueUse
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()

2. Data Fetching (Critical)

Use useFetch Instead of $fetch

// ❌ BAD: Fetches twice (server + client)
const data = await $fetch('/api/users')

// ✅ GOOD: SSR-aware, deduped, cached
const { data, pending, error, refresh } = await useFetch('/api/users')

Parallelize Independent Requests

// ❌ BAD: Sequential (slow)
const { data: user } = await useFetch('/api/user')
const { data: posts } = await useFetch('/api/posts')
const { data: comments } = await useFetch('/api/comments')

// ✅ GOOD: Parallel (fast)
const [{ data: user }, { data: posts }, { data: comments }] = await Promise.all([
  useFetch('/api/user'),
  useFetch('/api/posts'),
  useFetch('/api/comments')
])

Use Lazy for Non-Critical Data

// Critical data: blocks SSR
const { data: mainContent } = await useFetch('/api/content')

// Non-critical: loads after hydration
const { data: recommendations, pending } = useLazyFetch('/api/recommend')
const { data: comments } = useFetch('/api/comments', { lazy: true })

Set Unique Keys to Avoid Duplicate Requests

const { data } = await useFetch(`/api/article/${id}`, {
  key: `article-${id}`
})

3. Reactivity (High)

Use shallowRef for Large Objects

// ❌ BAD: Deep reactivity overhead for 10000 items
const tableData = ref<User[]>([])

// ✅ GOOD: Only .value is reactive
const tableData = shallowRef<User[]>([])

// Update by replacing entire array
tableData.value = newData

// Or mutate + trigger manually
tableData.value[0].name = 'New'
triggerRef(tableData)

Never Destructure reactive() Objects

const state = reactive({ count: 0, name: 'Vue' })

// ❌ BAD: Loses reactivity
const { count } = state
count++ // Won't trigger updates!

// ✅ GOOD: Use toRefs
const { count } = toRefs(state)
count.value++ // Works!

// ✅ BETTER: Use ref instead of reactive
const count = ref(0)

Use computed for Derived Values

<!-- ❌ BAD: Recalculates every render -->
<div>{{ items.filter(i => i.active).length }}</div>

<!-- ✅ GOOD: Cached -->
<script setup>
const activeCount = computed(() => items.value.filter(i => i.active).length)
</script>
<div>{{ activeCount }}</div>

4. Component Design (High)

Lazy Load Heavy Components

// ❌ BAD: Loaded immediately
import HeavyChart from './HeavyChart.vue'

// ✅ GOOD: Loaded on demand
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))

// ✅ NUXT: Auto lazy with Lazy prefix
<template>
  <LazyHeavyChart v-if="showChart" />
</template>

v-if vs v-show

| Scenario | Use | |----------|-----| | Frequent toggle (hover, tabs) | v-show | | Rarely changes | v-if | | Initially false | v-if | | Heavy component | v-if |

Type Props with TypeScript

interface Props {
  user: User
  items: string[]
  loading?: boolean
  size?: 'sm' | 'md' | 'lg'
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  size: 'md',
  items: () => []
})

5. Performance (Medium)

Use v-memo for List Optimization

<div
  v-for="item in items"
  :key="item.id"
  v-memo="[item.id, item === selected]"
>
  <ExpensiveComponent :item="item" />
</div>

Use Virtual Scrolling for Large Lists

// For 1000+ items, use virtual scrolling
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(items, {
  itemHeight: 40
})

Debounce/Throttle High-Frequency Events

import { useDebounceFn, useThrottleFn } from '@vueuse/core'

// Search input: debounce
const debouncedSearch = useDebounceFn(search, 300)

// Scroll handler: throttle
const throttledScroll = useThrottleFn(handleScroll, 100)

Use KeepAlive for Cached Components

<KeepAlive :include="['TabA', 'TabB']" :max="5">
  <component :is="currentTab" />
</KeepAlive>

6. State Management - Pinia (Medium)

Use storeToRefs for Destructuring

import { storeToRefs } from 'pinia'

const store = useUserStore()

// ❌ BAD: Loses reactivity
const { user, isLoggedIn } = store

// ✅ GOOD: Keeps reactivity
const { user, isLoggedIn } = storeToRefs(store)

// Actions can be destructured directly
const { login, logout } = store

Use $patch for Batch Updates

// ❌ BAD: Multiple reactive updates
store.user.name = 'New'
store.user.email = 'new@example.com'
store.updatedAt = new Date()

// ✅ GOOD: Single update
store.$patch({
  user: { ...store.user, name: 'New', email: 'new@example.com' },
  updatedAt: new Date()
})

7. Bundle Optimization (Low)

Tree-Shaking Friendly Imports

// ❌ BAD: Imports entire lodash (~70KB)
import _ from 'lodash'

// ✅ GOOD: Only imports debounce (~2KB)
import { debounce } from 'lodash-es'

Use Auto-Import for Component Libraries

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@element-plus/nuxt'],
  // Auto-imports only used components
})

Analyze Bundle Size

npx nuxi analyze

8. Nuxt Specific (Low)

Configure Route Rules for Caching

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { isr: 3600 },        // Regenerate hourly
    '/api/products': { swr: 60 },     // Stale-while-revalidate
    '/admin/**': { ssr: false }       // Client-only
  }
})

Use useState for Simple Shared State

// SSR-safe shared state (simpler than Pinia)
const user = useState<User | null>('user', () => null)
const theme = useState('theme', () => 'light')

Use definePageMeta for Page Configuration

<script setup lang="ts">
definePageMeta({
  layout: 'admin',
  middleware: ['auth'],
  keepalive: true
})
</script>

Quick Reference

| Issue | Solution | |-------|----------| | Hydration mismatch | Use <ClientOnly> or onMounted | | Double fetching | Use useFetch not $fetch | | Slow sequential requests | Use Promise.all() | | Large list performance | Use virtual scrolling | | Reactivity lost | Don't destructure reactive(), use toRefs() | | Store reactivity lost | Use storeToRefs() | | Large bundle | Use dynamic import() and tree-shaking |