Skip to content

⚡ パフォーマンス最適化テクニック - あなたのアプリを稲妻のように速く!

🚀 あなたの Vant アプリをウサギよりも速く走らせたいですか?このパフォーマンス最適化の秘訣があなたの加速装置になります!バンドルサイズの削減からファーストビューの瞬時表示、滑らかなスクロールからインテリジェントなモニタリングまで、究極のユーザー体験を創り出すお手伝いをします。準備はできていますか?一緒にパフォーマンス最適化の素晴らしい旅に出かけましょう!

📦 バンドルサイズ最適化

按需引入组件

自动按需引入(推荐)

bash
# 安装依赖
npm install unplugin-vue-components -D
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()]
    })
  ]
})
vue
<template>
  <!-- 手動でのインポートは不要、自動的に必要なものだけがロードされます -->
  <van-button type="primary">ボタン</van-button>
  <van-cell title="セル" />
</template>

手动按需引入

javascript
// main.js
import { createApp } from 'vue'
import { Button, Cell, Toast } from 'vant'
import 'vant/es/button/style'
import 'vant/es/cell/style'
import 'vant/es/toast/style'

const app = createApp()
app.use(Button)
app.use(Cell)
app.use(Toast)

代码分割

路由级代码分割

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/products',
    name: 'Products',
    component: () => import('../views/Products.vue')
  },
  {
    path: '/profile',
    name: 'Profile',
    // 懒加载 + 预加载
    component: () => import(/* webpackChunkName: "profile" */ '../views/Profile.vue')
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})

组件级代码分割

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

// 异步组件
const HeavyComponent = defineAsyncComponent(() => 
  import('./components/HeavyComponent.vue')
)

// 带加载状态的异步组件
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./components/AsyncComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <div>
    <HeavyComponent />
    <AsyncComponent />
  </div>
</template>

构建优化配置

Vite 配置优化

javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  
  build: {
    // 启用CSS代码分割
    cssCodeSplit: true,
    
    // 构建后是否生成source map文件
    sourcemap: false,
    
    // chunk大小警告的限制
    chunkSizeWarningLimit: 2000,
    
    rollupOptions: {
      output: {
        // 手动分包
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'vant-vendor': ['vant'],
          'utils-vendor': ['lodash-es', 'dayjs']
        }
      }
    },
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log']
      }
    }
  },
  
  // 开发服务器优化
  server: {
    warmup: {
      clientFiles: ['./src/components/*.vue']
    }
  }
})

⚡ ランタイムパフォーマンス最適化

画像最適化戦略

遅延読み込みの実装

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

// 画像の遅延読み込み設定
const lazyloadOptions = {
  preLoad: 1.3,
  error: '/error.png',
  loading: '/loading.gif',
  attempt: 1
}

onMounted(() => {
  app.use(Lazyload, lazyloadOptions)
})
</script>

<template>
  <!-- 画像の遅延読み込み -->
  <img v-lazy="imageUrl" alt="遅延読み込み画像" />
  
  <!-- 背景画像の遅延読み込み -->
  <div v-lazy:background-image="bgImageUrl" class="bg-container"></div>
  
  <!-- Vant 画像コンポーネントの遅延読み込み -->
  <van-image
    lazy-load
    :src="imageUrl"
    width="100"
    height="100"
    fit="cover"
  />
</template>

レスポンシブ画像

html
<!-- デバイスのピクセル比に応じて異なる解像度の画像をロード -->
<img 
  srcset="image@1x.jpg 1x, image@2x.jpg 2x, image@3x.jpg 3x" 
  src="image@1x.jpg" 
  alt="レスポンシブ画像"
>

<!-- picture要素を使用して複数の形式をサポート -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="画像">
</picture>

仮想スクロール

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

const list = ref([])
const loading = ref(false)
const finished = ref(false)

// 仮想スクロール設定
const itemHeight = 60
const visibleCount = Math.ceil(window.innerHeight / itemHeight) + 2
const startIndex = ref(0)

const visibleItems = computed(() => {
  const start = startIndex.value
  const end = start + visibleCount
  return list.value.slice(start, end)
})

const onLoad = async () => {
  loading.value = true
  
  // データロードのシミュレーション
  const newItems = Array.from({ length: 20 }, (_, i) => ({
    id: list.value.length + i,
    title: `アイテム ${list.value.length + i + 1}`
  }))
  
  list.value.push(...newItems)
  loading.value = false
  
  if (list.value.length >= 100) {
    finished.value = true
  }
}

const onScroll = (event) => {
  const scrollTop = event.target.scrollTop
  startIndex.value = Math.floor(scrollTop / itemHeight)
}
</script>

<template>
  <div class="virtual-list" @scroll="onScroll">
    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="これ以上ありません"
      @load="onLoad"
    >
      <van-cell
        v-for="item in visibleItems"
        :key="item.id"
        :title="item.title"
        :style="{ height: itemHeight + 'px' }"
      />
    </van-list>
  </div>
</template>

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

🚀 ファーストビューロードの最適化

重要リソースの最適化

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  
  <!-- DNSプリフェッチ -->
  <link rel="dns-prefetch" href="//cdn.example.com">
  <link rel="dns-prefetch" href="//api.example.com">
  
  <!-- 重要リソースのプリロード -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/css/critical.css" as="style">
  
  <!-- 重要なサードパーティのプリコネクト -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  
  <!-- クリティカルCSSのインライン -->
  <style>
    /* ファーストビューの重要スタイル */
    .loading-screen {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: #fff;
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 9999;
    }
    
    .loading-spinner {
      width: 40px;
      height: 40px;
      border: 4px solid #f3f3f3;
      border-top: 4px solid #1989fa;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }
    
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
  </style>
  
  <title>Vant移动端UI</title>
</head>
<body>
  <div id="app">
    <!-- ファーストビューロードアニメーション -->
    <div class="loading-screen">
      <div class="loading-spinner"></div>
    </div>
  </div>
</body>
</html>

スケルトンスクリーンの実装

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

const loading = ref(true)
const data = ref([])

const fetchData = async () => {
  // APIリクエストのシミュレーション
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([
        { id: 1, title: 'タイトル1', value: 'コンテンツ1' },
        { id: 2, title: 'タイトル2', value: 'コンテンツ2' },
        { id: 3, title: 'タイトル3', value: 'コンテンツ3' }
      ])
    }, 2000)
  })
}

onMounted(async () => {
  try {
    data.value = await fetchData()
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <div class="page-container">
    <!-- スケルトンスクリーン -->
    <van-skeleton 
      :loading="loading" 
      :row="3" 
      :row-width="['100%', '60%', '80%']"
      avatar
      title
    >
      <!-- 実際のコンテンツ -->
      <div class="content">
        <van-cell-group>
          <van-cell 
            v-for="item in data" 
            :key="item.id"
            :title="item.title"
            :value="item.value"
          />
        </van-cell-group>
      </div>
    </van-skeleton>
  </div>
</template>

<style scoped>
.page-container {
  padding: 16px;
}
</style>

📱 モバイル端末のパフォーマンス最適化

タッチ操作の最適化

css
/* タッチ操作の最適化 */
.touch-element {
  /* 300msのタッチ遅延を排除 */
  touch-action: manipulation;
  
  /* タッチのハイライトを無効にする */
  -webkit-tap-highlight-color: transparent;
  
  /* 長押しメニューを無効にする */
  -webkit-touch-callout: none;
  
  /* テキスト選択を無効にする */
  -webkit-user-select: none;
  user-select: none;
}

/* スクロールの最適化 */
.scroll-container {
  /* ハードウェアアクセラレーションによるスクロールを有効にする */
  -webkit-overflow-scrolling: touch;
  overflow-scrolling: touch;
  
  /* スクロールパフォーマンスを最適化 */
  will-change: scroll-position;
  
  /* スクロールの貫通を防止 */
  overscroll-behavior: contain;
}

/* 長いリストの最適化 */
.list-item {
  /* GPUアクセラレーションを有効にする */
  transform: translateZ(0);
  
  /* 再描画を最適化 */
  will-change: transform;
}

メモリの最適化

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

const data = ref([])
const observer = ref(null)

// タイマーのクリーンアップ
const timers = []

const addTimer = (callback, delay) => {
  const timer = setTimeout(callback, delay)
  timers.push(timer)
  return timer
}

// イベントリスナーのクリーンアップ
const eventListeners = []

const addEventListener = (element, event, handler) => {
  element.addEventListener(event, handler)
  eventListeners.push({ element, event, handler })
}

onMounted(() => {
  // Intersection Observerを使用してスクロールパフォーマンスを最適化
  observer.value = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // 要素がビューポートに入ったときの処理
        entry.target.classList.add('visible')
      }
    })
  })
  
  // すべてのリストアイテムを観察
  nextTick(() => {
    document.querySelectorAll('.list-item').forEach(item => {
      observer.value.observe(item)
    })
  })
})

onUnmounted(() => {
  // タイマーのクリーンアップ
  timers.forEach(timer => clearTimeout(timer))
  
  // イベントリスナーのクリーンアップ
  eventListeners.forEach(({ element, event, handler }) => {
    element.removeEventListener(event, handler)
  })
  
  // Intersection Observerのクリーンアップ
  if (observer.value) {
    observer.value.disconnect()
  }
})
</script>

📊 パフォーマンスモニタリング

パフォーマンス指標のモニタリング

javascript
// パフォーマンスモニタリングツール
class PerformanceMonitor {
  constructor() {
    this.metrics = {}
    this.init()
  }
  
  init() {
    this.observePageLoad()
    this.observeUserInteraction()
    this.observeResourceLoad()
  }
  
  // ページロードパフォーマンス
  observePageLoad() {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0]
      
      this.metrics.pageLoad = {
        dns: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp: navigation.connectEnd - navigation.connectStart,
        request: navigation.responseStart - navigation.requestStart,
        response: navigation.responseEnd - navigation.responseStart,
        dom: navigation.domContentLoadedEventEnd - navigation.responseEnd,
        load: navigation.loadEventEnd - navigation.loadEventStart,
        total: navigation.loadEventEnd - navigation.navigationStart
      }
      
      this.reportMetrics()
    })
  }
  
  // ユーザーインタラクションパフォーマンス
  observeUserInteraction() {
    // FID(First Input Delay)のモニタリング
    if ('PerformanceObserver' in window) {
      new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.name === 'first-input') {
            this.metrics.fid = entry.processingStart - entry.startTime
          }
        }
      }).observe({ type: 'first-input', buffered: true })
      
      // LCP(Largest Contentful Paint)のモニタリング
      new PerformanceObserver((list) => {
        const entries = list.getEntries()
        const lastEntry = entries[entries.length - 1]
        this.metrics.lcp = lastEntry.startTime
      }).observe({ type: 'largest-contentful-paint', buffered: true })
    }
  }
  
  // リソースロードパフォーマンス
  observeResourceLoad() {
    if ('PerformanceObserver' in window) {
      new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.initiatorType === 'img') {
            this.metrics.imageLoad = entry.duration
          }
        }
      }).observe({ type: 'resource', buffered: true })
    }
  }
  
  // パフォーマンスデータの送信
  reportMetrics() {
    // 分析サービスに送信
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/performance', JSON.stringify(this.metrics))
    } else {
      fetch('/api/performance', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(this.metrics)
      }).catch(err => console.error('Performance report failed:', err))
    }
  }
}

// パフォーマンスモニタリングの初期化
if (typeof window !== 'undefined') {
  new PerformanceMonitor()
}

Web Vitalsのモニタリング

javascript
// web-vitalsのインストール
// npm install web-vitals

import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

// コアWebパフォーマンス指標のモニタリング
function sendToAnalytics(metric) {
  // 分析サービスに送信
  fetch('/api/analytics', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(metric)
  })
}

getCLS(sendToAnalytics)  // 累積レイアウトシフト
getFID(sendToAnalytics)  // 初回入力遅延
getFCP(sendToAnalytics)  // 初回コンテンツペイント
getLCP(sendToAnalytics)  // 最大コンテンツペイント
getTTFB(sendToAnalytics) // 最初のバイト時間

🛠️ デバッグツール

パフォーマンス分析ツールの推奨

  1. Chrome DevTools

    • Performanceパネル:実行時パフォーマンスの分析
    • Networkパネル:リソースロードの分析
    • Lighthouse:総合的なパフォーマンス評価
  2. Bundle Analyzer

    bash
    # インストール
    npm install --save-dev webpack-bundle-analyzer
    
    # バンドルサイズの分析
    npm run build -- --analyze
  3. パフォーマンスモニタリングサービス

    • Google Analytics
    • Sentry Performance
    • New Relic Browser

パフォーマンス最適化チェックリスト

  • [ ] 必要なものだけをインポートし、バンドルサイズを削減
  • [ ] コード分割を設定し、ロード速度を最適化
  • [ ] 画像の遅延読み込みと圧縮を実装
  • [ ] スケルトンスクリーンを使用してユーザー体験を向上
  • [ ] クリティカルレンダリングパスを最適化
  • [ ] リソースの圧縮とキャッシュを有効にする
  • [ ] コアパフォーマンス指標をモニタリング
  • [ ] 定期的にパフォーマンステストを実行

💡 ベストプラクティスまとめ - パフォーマンス最適化の黄金律

  1. バンドルサイズの最適化 📦

    • 必要なものだけをインポートし、コード分割を使用(必要なものだけ)
    • 適切なビルド最適化を設定(極限まで圧縮)
  2. ファーストビュー最適化 🚀

    • スケルトンスクリーンと重要リソースのプリロードを実装(迅速な応答)
    • クリティカルCSSをインライン化し、非重要リソースを遅延(優先度管理)
  3. 実行時最適化

    • 仮想スクロールと画像の遅延読み込みを使用(必要に応じてレンダリング)
    • アニメーションとインタラクションのパフォーマンスを最適化(スムーズな体験)
  4. モバイル端末最適化 📱

    • タッチ体験とスクロールパフォーマンスを最適化(モバイルファースト)
    • メモリとリソースを合理的に管理(細かな計画)
  5. パフォーマンスモニタリング 📊

    • 完全なパフォーマンスモニタリングシステムを構築(データで話す)
    • コアWeb Vitals指標に注目(ユーザー体験)
  6. 継続的な最適化 🔄

    • 定期的にパフォーマンスのボトルネックを分析・最適化(止まらない)
    • パフォーマンス予算とモニタリングメカニズムを確立(未然に防ぐ)

📚 関連コンテンツ

パフォーマンス最適化の詳細な技巧を深く理解したいですか?これらのリソースは見逃せません:

上記の最適化戦略により、Vantアプリは大幅なパフォーマンス向上を実現し、ユーザーに卓越した使用体験を提供します。パフォーマンス最適化は継続的なプロセスであり、細部の改善ごとにユーザーはあなたの気配りを感じるでしょう!🎯✨

Vant に基づく企業向けモバイルソリューション