Skip to content

主题性能优化

性能优化是主题开发的重要环节。本文档将详细介绍如何优化 VitePress 主题的性能,包括加载优化、渲染优化和构建优化等方面。

版本说明

  • 本文档基于 VitePress v1.0.0+ 和 Vite 5.0+ 编写
  • 需要 Node.js 18.0.0 及以上版本
  • 涉及 Web 性能优化基础知识

性能指标

Core Web Vitals

指标说明良好值
LCP (Largest Contentful Paint)最大内容绘制时间≤ 2.5s
FID (First Input Delay)首次输入延迟≤ 100ms
CLS (Cumulative Layout Shift)累积布局偏移≤ 0.1
FCP (First Contentful Paint)首次内容绘制≤ 1.8s
TTFB (Time to First Byte)首字节时间≤ 600ms

性能测试工具

bash
# 1. Lighthouse
npx lighthouse https://your-site.com --view

# 2. WebPageTest
# 访问 https://www.webpagetest.org/

# 3. Chrome DevTools
# F12 -> Lighthouse -> Generate report

加载优化

1. 代码分割

自动代码分割

VitePress 和 Vite 会自动进行代码分割,但我们可以进一步优化:

typescript
// .vitepress/config.ts
export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          // 手动分割代码块
          manualChunks: {
            // Vue 核心库
            'vue-vendor': ['vue'],
            // VitePress 核心
            'vitepress-vendor': ['vitepress'],
            // 工具库
            'utils': ['lodash-es', 'date-fns'],
            // 图表库(按需)
            'charts': ['echarts', 'mermaid']
          }
        }
      },
      // 提高 chunk 大小警告阈值
      chunkSizeWarningLimit: 1000
    }
  }
})

组件懒加载

vue
<script setup>
import { defineAsyncComponent } from 'vue'

// 懒加载大型组件
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// 懒加载带选项
const HeavyComponent = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: () => h('div', '加载中...'),
  errorComponent: () => h('div', '加载失败'),
  delay: 200,
  timeout: 10000
})
</script>

<template>
  <Suspense>
    <template #default>
      <HeavyChart />
    </template>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

2. Tree Shaking

优化导入

typescript
// ❌ 不推荐:导入整个库
import _ from 'lodash'

// ✅ 推荐:按需导入
import { debounce, throttle } from 'lodash-es'

// ✅ 更好:使用专门的包
import debounce from 'lodash-es/debounce'

配置 Tree Shaking

typescript
// package.json
{
  "sideEffects": [
    "**/*.css",
    "**/*.vue"
  ]
}

// .vitepress/config.ts
export default defineConfig({
  vite: {
    build: {
      // 启用 Tree Shaking
      minify: 'esbuild',
      rollupOptions: {
        output: {
          // 保留 ES 模块格式
          format: 'es',
          // 保留导入语句
          exports: 'named'
        }
      }
    }
  }
})

3. 资源优化

图片优化

vue
<script setup>
import { ref } from 'vue'

const imageUrl = ref('/images/hero.webp')

// 响应式图片
const srcset = computed(() => `
  /images/hero-400.webp 400w,
  /images/hero-800.webp 800w,
  /images/hero-1200.webp 1200w
`)
</script>

<template>
  <img
    :src="imageUrl"
    :srcset="srcset"
    sizes="(max-width: 768px) 100vw, 50vw"
    alt="Hero"
    loading="lazy"
    decoding="async"
  />
</template>
typescript
// 使用 vite-plugin-image-optimizer
import { defineConfig } from 'vite'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'

export default defineConfig({
  plugins: [
    ViteImageOptimizer({
      test: /\.(jpe?g|png|gif|webp|svg)$/i,
      includePublic: true,
      logStats: true
    })
  ]
})

字体优化

css
/* 使用 font-display: swap */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* 避免字体加载时的 FOIT */
  font-weight: 400;
  font-style: normal;
}

/* 预加载关键字体 */
typescript
// .vitepress/config.ts
export default defineConfig({
  head: [
    // 预加载字体
    ['link', {
      rel: 'preload',
      href: '/fonts/custom.woff2',
      as: 'font',
      type: 'font/woff2',
      crossorigin: 'anonymous'
    }]
  ]
})

4. 预加载和预获取

typescript
// .vitepress/config.ts
export default defineConfig({
  head: [
    // 预加载关键资源
    ['link', { rel: 'preload', href: '/fonts/custom.woff2', as: 'font', crossorigin: 'anonymous' }],
    ['link', { rel: 'preload', href: '/images/hero.webp', as: 'image' }],
    
    // DNS 预解析
    ['link', { rel: 'dns-prefetch', href: 'https://fonts.googleapis.com' }],
    
    // 预连接
    ['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
    ['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous' }]
  ]
})
vue
<!-- 组件中的预获取 -->
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 预获取下一页资源
  const link = document.createElement('link')
  link.rel = 'prefetch'
  link.href = '/next-page'
  document.head.appendChild(link)
})
</script>

渲染优化

1. 虚拟滚动

对于长列表,使用虚拟滚动减少 DOM 节点:

vue
<script setup>
import { ref } from 'vue'
import { useVirtualList } from '@vueuse/core'

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  title: `Item ${i}`
})))

const { list, containerProps, wrapperProps } = useVirtualList(
  items,
  { itemHeight: 50 }
)
</script>

<template>
  <div v-bind="containerProps" class="virtual-list">
    <div v-bind="wrapperProps">
      <div
        v-for="{ data, index } in list"
        :key="index"
        class="item"
      >
        {{ data.title }}
      </div>
    </div>
  </div>
</template>

<style scoped>
.virtual-list {
  height: 500px;
  overflow-y: auto;
}

.item {
  height: 50px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}
</style>

2. 防抖和节流

vue
<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'

const searchTerm = ref('')

// 防抖:搜索输入
const handleSearchDebounced = useDebounceFn((value) => {
  console.log('搜索:', value)
  // 执行搜索
}, 300)

// 节流:滚动事件
const handleScrollThrottled = useThrottleFn(() => {
  console.log('滚动位置:', window.scrollY)
}, 100)

function handleInput(e) {
  handleSearchDebounced(e.target.value)
}

onMounted(() => {
  window.addEventListener('scroll', handleScrollThrottled)
})

onUnmounted(() => {
  window.removeEventListener('scroll', handleScrollThrottled)
})
</script>

3. 懒加载组件

vue
<script setup>
import { ref, onMounted } from 'vue'

const isVisible = ref(false)
const target = ref(null)

// IntersectionObserver 懒加载
onMounted(() => {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          isVisible.value = true
          observer.disconnect()
        }
      })
    },
    { threshold: 0.1 }
  )
  
  if (target.value) {
    observer.observe(target.value)
  }
})
</script>

<template>
  <div ref="target">
    <HeavyComponent v-if="isVisible" />
    <div v-else class="placeholder">加载中...</div>
  </div>
</template>

或使用 @vueuse/core

vue
<script setup>
import { useIntersectionObserver } from '@vueuse/core'

const target = ref(null)
const isVisible = ref(false)

useIntersectionObserver(
  target,
  ([{ isIntersecting }]) => {
    if (isIntersecting) {
      isVisible.value = true
    }
  },
  { threshold: 0.1 }
)
</script>

<template>
  <div ref="target">
    <HeavyComponent v-if="isVisible" />
  </div>
</template>

4. 计算属性缓存

vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([])

// ✅ 使用计算属性(自动缓存)
const filteredItems = computed(() => {
  return items.value.filter(item => item.active)
})

// ❌ 避免:方法调用(无缓存)
function getFilteredItems() {
  return items.value.filter(item => item.active)
}
</script>

5. v-once 和 v-memo

vue
<template>
  <!-- 静态内容使用 v-once -->
  <header v-once>
    <h1>{{ siteTitle }}</h1>
    <p>{{ siteDescription }}</p>
  </header>

  <!-- 复杂列表使用 v-memo -->
  <div
    v-for="item in items"
    :key="item.id"
    v-memo="[item.selected]"
  >
    <ExpensiveComponent :item="item" />
  </div>
</template>

构建优化

1. 压缩配置

typescript
// .vitepress/config.ts
export default defineConfig({
  vite: {
    build: {
      // 使用 esbuild 压缩(更快)
      minify: 'esbuild',
      
      // 或使用 terser(压缩率更高)
      // minify: 'terser',
      // terserOptions: {
      //   compress: {
      //     drop_console: true,
      //     drop_debugger: true
      //   }
      // },
      
      // CSS 压缩
      cssMinify: true
    }
  }
})

2. 构建分析

bash
# 安装分析工具
npm install -D rollup-plugin-visualizer
typescript
// .vitepress/config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  vite: {
    plugins: [
      visualizer({
        open: true,
        gzipSize: true,
        brotliSize: true,
        filename: 'stats.html'
      })
    ]
  }
})

3. 缓存优化

typescript
// .vitepress/config.ts
export default defineConfig({
  vite: {
    build: {
      // 生成 sourcemap(可选)
      sourcemap: false,
      
      // 缓存目录
      cacheDir: 'node_modules/.vite'
    },
    
    // 依赖预构建缓存
    optimizeDeps: {
      include: ['vue', 'vitepress']
    }
  }
})

4. 外部化依赖

typescript
// .vitepress/config.ts
export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
        // 外部化大型依赖
        external: ['vue', 'vitepress'],
        output: {
          globals: {
            vue: 'Vue',
            vitepress: 'VitePress'
          }
        }
      }
    }
  }
})

网络优化

1. CDN 加速

typescript
// .vitepress/config.ts
export default defineConfig({
  // 使用 CDN 基础路径
  base: 'https://cdn.example.com/'
})

2. 缓存策略

nginx
# nginx.conf
server {
  # 静态资源长期缓存
  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
  
  # HTML 文件短期缓存
  location ~* \.html$ {
    expires 1h;
    add_header Cache-Control "public, must-revalidate";
  }
}

3. Gzip 压缩

typescript
// .vitepress/config.ts
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  vite: {
    plugins: [
      viteCompression({
        algorithm: 'gzip',
        ext: '.gz',
        threshold: 10240, // 大于 10KB 才压缩
        deleteOriginFile: false
      }),
      // Brotli 压缩(压缩率更高)
      viteCompression({
        algorithm: 'brotliCompress',
        ext: '.br'
      })
    ]
  }
})

4. HTTP/2 推送

nginx
# nginx.conf
server {
  listen 443 ssl http2;
  
  # HTTP/2 推送关键资源
  http2_push /assets/index.js;
  http2_push /assets/index.css;
  http2_push /fonts/custom.woff2;
}

运行时优化

1. 避免不必要的响应式

vue
<script setup>
import { ref, shallowRef, shallowReactive } from 'vue'

// ❌ 深层响应式(性能开销大)
const deepObject = ref({
  nested: { data: [] }
})

// ✅ 浅层响应式(性能更好)
const shallowObject = shallowRef({
  nested: { data: [] }
})

// ✅ 对于大型数据
const largeList = shallowReactive([])
</script>

2. 使用 shallowRef 处理大型对象

vue
<script setup>
import { shallowRef, triggerRef } from 'vue'

const largeData = shallowRef({
  items: Array(10000).fill(null).map((_, i) => ({ id: i }))
})

function updateItem(index, newValue) {
  // 直接修改(不会触发更新)
  largeData.value.items[index] = newValue
  // 手动触发更新
  triggerRef(largeData)
}
</script>

3. 使用 markRaw 避免响应式转换

vue
<script setup>
import { markRaw } from 'vue'

// 标记为非响应式(如第三方库实例)
const chart = markRaw(new Chart(ctx, config))
</script>

监控和分析

1. 性能监控

vue
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // 使用 Performance API
  const timing = performance.timing
  const loadTime = timing.loadEventEnd - timing.navigationStart
  
  console.log('页面加载时间:', loadTime, 'ms')
  
  // 使用 PerformanceObserver
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log('性能条目:', entry.name, entry.duration)
    }
  })
  
  observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] })
})
</script>

2. 错误监控

typescript
// .vitepress/theme/index.ts
import { onMounted } from 'vue'

export default {
  setup() {
    onMounted(() => {
      // 捕获全局错误
      window.onerror = (message, source, lineno, colno, error) => {
        console.error('全局错误:', { message, source, lineno, colno, error })
        // 发送到错误监控服务
      }
      
      // 捕获未处理的 Promise 拒绝
      window.onunhandledrejection = (event) => {
        console.error('未处理的 Promise 拒绝:', event.reason)
      }
    })
  }
}

性能检查清单

加载性能

  • [ ] 代码分割优化
  • [ ] Tree Shaking 启用
  • [ ] 图片优化和懒加载
  • [ ] 字体优化
  • [ ] 预加载关键资源
  • [ ] Gzip/Brotli 压缩
  • [ ] CDN 加速

渲染性能

  • [ ] 虚拟滚动(长列表)
  • [ ] 懒加载组件
  • [ ] 防抖和节流
  • [ ] 计算属性缓存
  • [ ] v-once 和 v-memo
  • [ ] 避免深层响应式

构建性能

  • [ ] 压缩配置优化
  • [ ] 构建分析
  • [ ] 缓存优化
  • [ ] 外部化依赖

网络性能

  • [ ] 缓存策略
  • [ ] HTTP/2
  • [ ] 资源预加载
  • [ ] 预连接第三方域名

相关资源

下一步

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献