T-CREATOR

Vue.js のパフォーマンス最適化 Tips 集

Vue.js のパフォーマンス最適化 Tips 集

Vue.js アプリケーションの開発において、パフォーマンスは成功の鍵を握る重要な要素です。ユーザーが快適にアプリケーションを利用できるかどうかは、開発者の技術力と最適化への意識にかかっています。

この記事では、Vue.js のパフォーマンス最適化に焦点を当て、実践的で効果的なテクニックを紹介します。初心者の方でも理解しやすいように、具体的なコード例とエラーケースを交えながら、段階的に学んでいける構成にしました。

Vue.js パフォーマンス最適化の重要性

現代の Web アプリケーションでは、ユーザーの期待値が年々高まっています。ページの読み込み時間が 3 秒を超えると、ユーザーの離脱率が急激に上昇するという調査結果もあります。

Vue.js は優れたフレームワークですが、適切な最適化を行わないと、アプリケーションの動作が重くなってしまう可能性があります。特に以下のような場面でパフォーマンスの問題が顕在化します:

  • 大量のデータを扱うリスト表示
  • 複雑な計算処理を含むコンポーネント
  • 頻繁に更新されるリアクティブデータ
  • ネストした深いコンポーネント構造

パフォーマンス最適化は、単に技術的な課題を解決するだけでなく、ユーザー体験の向上とビジネス価値の創出につながります。

コンポーネントレベルの最適化

v-if と v-show の使い分け

Vue.js には条件付きレンダリングのための 2 つのディレクティブがあります。適切に使い分けることで、パフォーマンスを大幅に改善できます。

v-if の特徴:

  • 条件が false の場合、DOM から完全に削除される
  • 初期化コストが高いが、表示されない時はメモリを消費しない
  • 頻繁に切り替わらない要素に適している

v-show の特徴:

  • 条件に関係なく DOM に存在し、CSS の display プロパティで制御
  • 初期化コストは低いが、常にメモリを消費する
  • 頻繁に切り替わる要素に適している
vue<!-- 頻繁に切り替わらない場合:v-ifを使用 -->
<template>
  <div>
    <div v-if="isLoggedIn" class="user-profile">
      <h2>ユーザープロフィール</h2>
      <p>ようこそ、{{ userName }}さん</p>
    </div>
  </div>
</template>
vue<!-- 頻繁に切り替わる場合:v-showを使用 -->
<template>
  <div>
    <div v-show="isLoading" class="loading-spinner">
      <p>読み込み中...</p>
    </div>
    <div v-show="!isLoading" class="content">
      <!-- メインコンテンツ -->
    </div>
  </div>
</template>

よくあるエラー:

javascript// ❌ 悪い例:頻繁に切り替わる要素にv-ifを使用
<div v-if="showTooltip" class="tooltip">ツールチップ</div>

// ✅ 良い例:v-showを使用
<div v-show="showTooltip" class="tooltip">ツールチップ</div>

キーの適切な設定

Vue.js の仮想 DOM は、効率的な更新のためにキーを使用します。適切なキーを設定しないと、予期しない動作やパフォーマンスの低下を招きます。

キーの重要性:

  • 仮想 DOM が要素を正しく識別できる
  • 不要な再レンダリングを防ぐ
  • アニメーションやトランジションが正しく動作する
vue<!-- ❌ 悪い例:インデックスをキーとして使用 -->
<template>
  <div>
    <div
      v-for="(item, index) in items"
      :key="index"
      class="item"
    >
      {{ item.name }}
    </div>
  </div>
</template>
vue<!-- ✅ 良い例:一意のIDをキーとして使用 -->
<template>
  <div>
    <div v-for="item in items" :key="item.id" class="item">
      {{ item.name }}
    </div>
  </div>
</template>

動的にキーを生成する場合:

vue<template>
  <div>
    <div
      v-for="(item, index) in items"
      :key="`item-${item.id}-${index}`"
      class="item"
    >
      {{ item.name }}
    </div>
  </div>
</template>

よくあるエラーと解決策:

javascript// ❌ エラー:重複するキー
// Vue.js Warning: Duplicate keys detected: '1'. This may cause an update error.

// ✅ 解決策:一意のキーを生成
const uniqueKey = (item, index) =>
  `${item.id}-${Date.now()}-${index}`;

計算プロパティの活用

計算プロパティは、Vue.js のパフォーマンス最適化において非常に重要な機能です。適切に使用することで、不要な再計算を防ぎ、アプリケーションの応答性を向上させます。

計算プロパティの利点:

  • 依存関係が変更された時のみ再計算される
  • キャッシュ機能により高速なアクセスが可能
  • テンプレート内の複雑なロジックを分離できる
vue<template>
  <div>
    <h2>商品一覧</h2>
    <div class="filters">
      <input
        v-model="searchTerm"
        placeholder="商品名で検索"
      />
      <select v-model="selectedCategory">
        <option value="">すべてのカテゴリ</option>
        <option value="electronics">電子機器</option>
        <option value="clothing">衣類</option>
      </select>
    </div>

    <!-- 計算プロパティを使用してフィルタリング -->
    <div
      v-for="product in filteredProducts"
      :key="product.id"
      class="product"
    >
      <h3>{{ product.name }}</h3>
      <p>{{ product.price }}円</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [
        {
          id: 1,
          name: 'スマートフォン',
          price: 50000,
          category: 'electronics',
        },
        {
          id: 2,
          name: 'Tシャツ',
          price: 2000,
          category: 'clothing',
        },
        {
          id: 3,
          name: 'ノートPC',
          price: 80000,
          category: 'electronics',
        },
      ],
      searchTerm: '',
      selectedCategory: '',
    };
  },
  computed: {
    // 検索とカテゴリフィルタリングを組み合わせた計算プロパティ
    filteredProducts() {
      return this.products.filter((product) => {
        const matchesSearch = product.name
          .toLowerCase()
          .includes(this.searchTerm.toLowerCase());
        const matchesCategory =
          !this.selectedCategory ||
          product.category === this.selectedCategory;
        return matchesSearch && matchesCategory;
      });
    },
  },
};
</script>

メソッドとの違い:

vue<script>
export default {
  // ❌ 悪い例:メソッドを使用(毎回実行される)
  methods: {
    getFilteredProducts() {
      return this.products.filter((product) => {
        const matchesSearch = product.name
          .toLowerCase()
          .includes(this.searchTerm.toLowerCase());
        const matchesCategory =
          !this.selectedCategory ||
          product.category === this.selectedCategory;
        return matchesSearch && matchesCategory;
      });
    },
  },

  // ✅ 良い例:計算プロパティを使用(キャッシュされる)
  computed: {
    filteredProducts() {
      return this.products.filter((product) => {
        const matchesSearch = product.name
          .toLowerCase()
          .includes(this.searchTerm.toLowerCase());
        const matchesCategory =
          !this.selectedCategory ||
          product.category === this.selectedCategory;
        return matchesSearch && matchesCategory;
      });
    },
  },
};
</script>

レンダリング最適化

仮想 DOM の理解と活用

Vue.js の仮想 DOM は、実際の DOM 操作を最小限に抑えるための重要な仕組みです。仮想 DOM の仕組みを理解することで、より効率的なコンポーネント設計が可能になります。

仮想 DOM の仕組み:

  1. 状態変更時に仮想 DOM ツリーを再構築
  2. 前回の仮想 DOM と比較(diff)
  3. 変更された部分のみ実際の DOM を更新
vue<!-- 効率的な仮想DOM更新の例 -->
<template>
  <div class="user-list">
    <!-- キーを適切に設定することで、仮想DOMが効率的に更新される -->
    <div
      v-for="user in users"
      :key="user.id"
      class="user-item"
    >
      <span class="name">{{ user.name }}</span>
      <span class="email">{{ user.email }}</span>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: [
        {
          id: 1,
          name: '田中太郎',
          email: 'tanaka@example.com',
        },
        {
          id: 2,
          name: '佐藤花子',
          email: 'sato@example.com',
        },
      ],
    };
  },
};
</script>

仮想 DOM の最適化テクニック:

vue<template>
  <div>
    <!-- 条件分岐を最小限に抑える -->
    <div v-if="isLoading" class="loading">
      読み込み中...
    </div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <div v-else class="content">
      <UserList :users="users" />
    </div>
  </div>
</template>

不要な再レンダリングの防止

Vue.js では、リアクティブデータが変更されると自動的にコンポーネントが再レンダリングされます。しかし、不要な再レンダリングはパフォーマンスの低下を招きます。

再レンダリングを防ぐテクニック:

vue<!-- オブジェクトの分割代入で不要な再レンダリングを防ぐ -->
<template>
  <div>
    <UserProfile :user="user" />
    <UserSettings :settings="settings" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: '田中太郎',
        email: 'tanaka@example.com',
        settings: {
          theme: 'dark',
          notifications: true,
        },
      },
    };
  },
  computed: {
    // ユーザー情報と設定を分離
    userInfo() {
      const { settings, ...userInfo } = this.user;
      return userInfo;
    },
    settings() {
      return this.user.settings;
    },
  },
};
</script>

v-once ディレクティブの活用:

vue<template>
  <div>
    <!-- 静的なコンテンツにはv-onceを使用 -->
    <div v-once class="static-content">
      <h1>アプリケーションタイトル</h1>
      <p>この内容は変更されません</p>
    </div>

    <!-- 動的なコンテンツ -->
    <div class="dynamic-content">
      <p>現在時刻: {{ currentTime }}</p>
    </div>
  </div>
</template>

よくあるエラー:

javascript// ❌ 悪い例:オブジェクト全体を渡す
<ChildComponent :data="largeObject" />

// ✅ 良い例:必要な部分のみを渡す
<ChildComponent :name="largeObject.name" :email="largeObject.email" />

メモ化の実装

メモ化は、計算結果をキャッシュして同じ入力に対する計算を避けるテクニックです。Vue.js では、計算プロパティと組み合わせることで効果的にメモ化を実装できます。

計算プロパティによるメモ化:

vue<template>
  <div>
    <h2>商品統計</h2>
    <p>総売上: {{ totalSales }}円</p>
    <p>平均価格: {{ averagePrice }}円</p>
    <p>高価格商品数: {{ expensiveProductsCount }}個</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [
        { id: 1, name: '商品A', price: 1000, sales: 50 },
        { id: 2, name: '商品B', price: 5000, sales: 20 },
        { id: 3, name: '商品C', price: 3000, sales: 30 },
      ],
    };
  },
  computed: {
    // 総売上の計算(メモ化される)
    totalSales() {
      console.log('総売上を計算中...');
      return this.products.reduce((total, product) => {
        return total + product.price * product.sales;
      }, 0);
    },

    // 平均価格の計算(メモ化される)
    averagePrice() {
      console.log('平均価格を計算中...');
      if (this.products.length === 0) return 0;
      const totalPrice = this.products.reduce(
        (sum, product) => sum + product.price,
        0
      );
      return Math.round(totalPrice / this.products.length);
    },

    // 高価格商品の数(メモ化される)
    expensiveProductsCount() {
      console.log('高価格商品数を計算中...');
      return this.products.filter(
        (product) => product.price > 3000
      ).length;
    },
  },
};
</script>

外部ライブラリを使用したメモ化:

javascript// lodashのmemoizeを使用した例
import { memoize } from 'lodash';

export default {
  data() {
    return {
      products: [],
    };
  },
  computed: {
    // 複雑な計算をメモ化
    processedData() {
      return this.memoizedProcess(this.products);
    },
  },
  methods: {
    // メモ化された処理関数
    memoizedProcess: memoize(
      function (products) {
        // 重い計算処理
        return products.map((product) => ({
          ...product,
          calculatedValue: this.heavyCalculation(product),
        }));
      },
      function (products) {
        // キー生成関数(配列の内容に基づいてキーを生成)
        return JSON.stringify(products.map((p) => p.id));
      }
    ),

    heavyCalculation(product) {
      // 重い計算処理のシミュレーション
      let result = 0;
      for (let i = 0; i < 1000000; i++) {
        result += Math.sqrt(product.price);
      }
      return result;
    },
  },
};

データ管理の最適化

リアクティブデータの最小化

Vue.js のリアクティブシステムは強力ですが、過度に使用するとパフォーマンスに影響を与えます。必要なデータのみをリアクティブにすることで、最適化を図れます。

リアクティブデータの最適化:

vue<template>
  <div>
    <h2>ユーザー情報</h2>
    <p>名前: {{ userInfo.name }}</p>
    <p>メール: {{ userInfo.email }}</p>

    <!-- 静的なデータはリアクティブにする必要がない -->
    <div class="static-info">
      <p>アプリケーション名: {{ appName }}</p>
      <p>バージョン: {{ version }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 動的に変更されるデータのみリアクティブにする
      userInfo: {
        name: '田中太郎',
        email: 'tanaka@example.com',
      },
    };
  },
  // 静的なデータはcomputedで定義
  computed: {
    appName() {
      return 'My Vue App';
    },
    version() {
      return '1.0.0';
    },
  },
};
</script>

Object.freeze()の活用:

javascriptexport default {
  data() {
    return {
      // 変更されない大きなデータはObject.freeze()で最適化
      staticData: Object.freeze({
        categories: ['electronics', 'clothing', 'books'],
        countries: ['Japan', 'USA', 'UK'],
        currencies: ['JPY', 'USD', 'EUR'],
      }),
    };
  },
};

よくあるエラー:

javascript// ❌ 悪い例:不要なリアクティブデータ
data() {
  return {
    staticConfig: {
      apiUrl: 'https://api.example.com',
      timeout: 5000
    }
  }
}

// ✅ 良い例:静的なデータはcomputedで定義
computed: {
  staticConfig() {
    return {
      apiUrl: 'https://api.example.com',
      timeout: 5000
    }
  }
}

大きなリストの効率的な処理

大量のデータを扱う際は、特別な最適化が必要です。Vue.js には、大きなリストを効率的に処理するための機能が用意されています。

v-for の最適化:

vue<template>
  <div>
    <!-- 大きなリストの場合は、表示範囲を制限する -->
    <div
      v-for="item in visibleItems"
      :key="item.id"
      class="list-item"
    >
      <h3>{{ item.name }}</h3>
      <p>{{ item.description }}</p>
    </div>

    <!-- ページネーション -->
    <div class="pagination">
      <button
        @click="prevPage"
        :disabled="currentPage === 1"
      >
        前へ
      </button>
      <span>{{ currentPage }} / {{ totalPages }}</span>
      <button
        @click="nextPage"
        :disabled="currentPage === totalPages"
      >
        次へ
      </button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [], // 大きなデータ配列
      currentPage: 1,
      itemsPerPage: 20,
    };
  },
  computed: {
    // 現在のページに表示するアイテムのみを計算
    visibleItems() {
      const start =
        (this.currentPage - 1) * this.itemsPerPage;
      const end = start + this.itemsPerPage;
      return this.items.slice(start, end);
    },

    totalPages() {
      return Math.ceil(
        this.items.length / this.itemsPerPage
      );
    },
  },
  methods: {
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
      }
    },
    nextPage() {
      if (this.currentPage < this.totalPages) {
        this.currentPage++;
      }
    },
  },
};
</script>

仮想スクロールの実装:

vue<template>
  <div
    class="virtual-list"
    ref="container"
    @scroll="handleScroll"
  >
    <div
      class="virtual-list-content"
      :style="{ height: totalHeight + 'px' }"
    >
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="virtual-list-item"
        :style="{
          transform: `translateY(${item.offset}px)`,
        }"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [], // 大きなデータ配列
      itemHeight: 50,
      visibleCount: 10,
      scrollTop: 0,
    };
  },
  computed: {
    totalHeight() {
      return this.items.length * this.itemHeight;
    },

    startIndex() {
      return Math.floor(this.scrollTop / this.itemHeight);
    },

    visibleItems() {
      const start = Math.max(0, this.startIndex - 5);
      const end = Math.min(
        this.items.length,
        this.startIndex + this.visibleCount + 5
      );

      return this.items
        .slice(start, end)
        .map((item, index) => ({
          ...item,
          offset: (start + index) * this.itemHeight,
        }));
    },
  },
  methods: {
    handleScroll(event) {
      this.scrollTop = event.target.scrollTop;
    },
  },
};
</script>

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

.virtual-list-content {
  position: relative;
}

.virtual-list-item {
  position: absolute;
  height: 50px;
  width: 100%;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
  padding: 0 16px;
}
</style>

オブジェクトの変更検知の最適化

Vue.js は、オブジェクトの変更を検知するために深い監視を行います。大きなオブジェクトやネストしたオブジェクトでは、この処理が重くなることがあります。

オブジェクトの変更検知の最適化:

vue<template>
  <div>
    <h2>ユーザー設定</h2>
    <div class="settings">
      <label>
        <input
          type="checkbox"
          v-model="settings.notifications"
        />
        通知を有効にする
      </label>
      <label>
        <input
          type="checkbox"
          v-model="settings.darkMode"
        />
        ダークモード
      </label>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 大きなオブジェクトは分割して管理
      userSettings: {
        notifications: true,
        darkMode: false,
        language: 'ja',
        timezone: 'Asia/Tokyo',
        // ... 多くの設定項目
      },
    };
  },
  computed: {
    // 必要な部分のみをリアクティブにする
    settings() {
      return {
        notifications: this.userSettings.notifications,
        darkMode: this.userSettings.darkMode,
      };
    },
  },
  methods: {
    // 設定の更新を最適化
    updateSetting(key, value) {
      this.$set(this.userSettings, key, value);
    },
  },
};
</script>

$setと$delete の活用:

javascriptexport default {
  data() {
    return {
      user: {
        name: '田中太郎',
        email: 'tanaka@example.com',
      },
    };
  },
  methods: {
    // 新しいプロパティを追加する場合
    addProperty() {
      // ❌ 悪い例:直接代入
      // this.user.newProperty = 'value';

      // ✅ 良い例:$setを使用
      this.$set(this.user, 'newProperty', 'value');
    },

    // プロパティを削除する場合
    removeProperty() {
      // ❌ 悪い例:delete演算子
      // delete this.user.name;

      // ✅ 良い例:$deleteを使用
      this.$delete(this.user, 'name');
    },
  },
};

よくあるエラー:

javascript// ❌ エラー:Vue.js Warning: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
props: ['user'],
methods: {
  updateUser() {
    this.user.name = '新しい名前'; // エラー
  }
}

// ✅ 解決策:ローカルコピーを作成
props: ['user'],
data() {
  return {
    localUser: { ...this.user }
  }
},
methods: {
  updateUser() {
    this.localUser.name = '新しい名前';
    this.$emit('update:user', this.localUser);
  }
}

ライフサイクルフックの活用

適切なタイミングでの処理実行

Vue.js のライフサイクルフックを適切に使用することで、パフォーマンスを最適化できます。各フックの特性を理解し、適切なタイミングで処理を実行することが重要です。

ライフサイクルフックの最適化:

vue<template>
  <div>
    <div v-if="isLoaded" class="content">
      <h2>{{ data.title }}</h2>
      <p>{{ data.description }}</p>
    </div>
    <div v-else class="loading">読み込み中...</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: null,
      isLoaded: false,
      timer: null,
    };
  },

  // コンポーネント作成時にデータを準備
  async created() {
    try {
      // 非同期データの取得
      this.data = await this.fetchData();
      this.isLoaded = true;
    } catch (error) {
      console.error('データの取得に失敗しました:', error);
    }
  },

  // DOMマウント後にDOM操作を実行
  mounted() {
    // DOM要素へのアクセスが必要な処理
    this.initializeChart();

    // 定期的な更新処理
    this.timer = setInterval(() => {
      this.updateData();
    }, 5000);
  },

  // コンポーネント更新前に処理を実行
  beforeUpdate() {
    // 更新前の状態を保存
    this.previousData = { ...this.data };
  },

  // コンポーネント更新後に処理を実行
  updated() {
    // DOM更新後の処理
    this.scrollToBottom();
  },

  // コンポーネント破棄前のクリーンアップ
  beforeDestroy() {
    // タイマーのクリア
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }

    // イベントリスナーの削除
    window.removeEventListener('resize', this.handleResize);
  },

  methods: {
    async fetchData() {
      const response = await fetch('/api/data');
      return response.json();
    },

    initializeChart() {
      // チャートの初期化処理
      const chartElement = this.$refs.chart;
      if (chartElement) {
        // チャートライブラリの初期化
      }
    },

    updateData() {
      // 定期的なデータ更新
      this.fetchData().then((data) => {
        this.data = data;
      });
    },

    scrollToBottom() {
      const container = this.$refs.container;
      if (container) {
        container.scrollTop = container.scrollHeight;
      }
    },

    handleResize() {
      // ウィンドウリサイズ時の処理
      this.updateLayout();
    },
  },
};
</script>

nextTick の活用:

vue<template>
  <div>
    <div ref="messageList" class="message-list">
      <div
        v-for="message in messages"
        :key="message.id"
        class="message"
      >
        {{ message.text }}
      </div>
    </div>
    <button @click="addMessage">メッセージを追加</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      messages: [],
    };
  },
  methods: {
    addMessage() {
      // メッセージを追加
      this.messages.push({
        id: Date.now(),
        text: '新しいメッセージ',
      });

      // DOM更新後にスクロール
      this.$nextTick(() => {
        this.scrollToBottom();
      });
    },

    scrollToBottom() {
      const messageList = this.$refs.messageList;
      if (messageList) {
        messageList.scrollTop = messageList.scrollHeight;
      }
    },
  },
};
</script>

よくあるエラー:

javascript// ❌ エラー:mountedでDOM要素にアクセスできない
mounted() {
  const element = document.getElementById('my-element');
  // elementがnullの可能性がある
}

// ✅ 解決策:$refsを使用
mounted() {
  const element = this.$refs.myElement;
  if (element) {
    // 安全にDOM要素にアクセス
  }
}

メモリリークの防止

Vue.js アプリケーションでは、適切にリソースをクリーンアップしないとメモリリークが発生する可能性があります。特に、イベントリスナーやタイマー、外部ライブラリの使用時には注意が必要です。

メモリリークの防止テクニック:

vue<template>
  <div>
    <div ref="chartContainer" class="chart-container"></div>
    <button @click="updateChart">チャートを更新</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      chart: null,
      resizeObserver: null,
      eventListeners: [],
    };
  },

  mounted() {
    this.initializeChart();
    this.setupEventListeners();
  },

  beforeDestroy() {
    this.cleanup();
  },

  methods: {
    initializeChart() {
      // チャートライブラリの初期化
      this.chart = new Chart(this.$refs.chartContainer, {
        // チャート設定
      });
    },

    setupEventListeners() {
      // ウィンドウリサイズイベント
      const handleResize = () => {
        if (this.chart) {
          this.chart.resize();
        }
      };

      window.addEventListener('resize', handleResize);
      this.eventListeners.push({
        element: window,
        event: 'resize',
        handler: handleResize,
      });

      // ResizeObserverの設定
      this.resizeObserver = new ResizeObserver(() => {
        if (this.chart) {
          this.chart.resize();
        }
      });

      this.resizeObserver.observe(
        this.$refs.chartContainer
      );
    },

    updateChart() {
      if (this.chart) {
        this.chart.update();
      }
    },

    cleanup() {
      // チャートの破棄
      if (this.chart) {
        this.chart.destroy();
        this.chart = null;
      }

      // イベントリスナーの削除
      this.eventListeners.forEach(
        ({ element, event, handler }) => {
          element.removeEventListener(event, handler);
        }
      );
      this.eventListeners = [];

      // ResizeObserverの切断
      if (this.resizeObserver) {
        this.resizeObserver.disconnect();
        this.resizeObserver = null;
      }
    },
  },
};
</script>

WebSocket 接続の管理:

vue<template>
  <div>
    <div v-if="isConnected" class="status connected">
      接続中
    </div>
    <div v-else class="status disconnected">切断中</div>
    <div class="messages">
      <div
        v-for="message in messages"
        :key="message.id"
        class="message"
      >
        {{ message.text }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      websocket: null,
      isConnected: false,
      messages: [],
      reconnectTimer: null,
      heartbeatTimer: null,
    };
  },

  mounted() {
    this.connectWebSocket();
  },

  beforeDestroy() {
    this.disconnectWebSocket();
  },

  methods: {
    connectWebSocket() {
      try {
        this.websocket = new WebSocket(
          'wss://example.com/ws'
        );

        this.websocket.onopen = () => {
          this.isConnected = true;
          this.startHeartbeat();
        };

        this.websocket.onmessage = (event) => {
          const message = JSON.parse(event.data);
          this.messages.push(message);
        };

        this.websocket.onclose = () => {
          this.isConnected = false;
          this.stopHeartbeat();
          this.scheduleReconnect();
        };

        this.websocket.onerror = (error) => {
          console.error('WebSocket error:', error);
        };
      } catch (error) {
        console.error('WebSocket接続エラー:', error);
      }
    },

    disconnectWebSocket() {
      // ハートビートタイマーの停止
      this.stopHeartbeat();

      // 再接続タイマーの停止
      if (this.reconnectTimer) {
        clearTimeout(this.reconnectTimer);
        this.reconnectTimer = null;
      }

      // WebSocket接続の切断
      if (this.websocket) {
        this.websocket.close();
        this.websocket = null;
      }
    },

    startHeartbeat() {
      this.heartbeatTimer = setInterval(() => {
        if (
          this.websocket &&
          this.websocket.readyState === WebSocket.OPEN
        ) {
          this.websocket.send(
            JSON.stringify({ type: 'heartbeat' })
          );
        }
      }, 30000); // 30秒間隔
    },

    stopHeartbeat() {
      if (this.heartbeatTimer) {
        clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = null;
      }
    },

    scheduleReconnect() {
      if (this.reconnectTimer) {
        clearTimeout(this.reconnectTimer);
      }

      this.reconnectTimer = setTimeout(() => {
        this.connectWebSocket();
      }, 5000); // 5秒後に再接続
    },
  },
};
</script>

よくあるエラー:

javascript// ❌ エラー:メモリリークの原因
mounted() {
  // イベントリスナーを追加
  window.addEventListener('resize', this.handleResize);
},

beforeDestroy() {
  // 削除を忘れるとメモリリーク
  // window.removeEventListener('resize', this.handleResize);
}

// ✅ 解決策:適切なクリーンアップ
mounted() {
  this.handleResize = this.handleResize.bind(this);
  window.addEventListener('resize', this.handleResize);
},

beforeDestroy() {
  window.removeEventListener('resize', this.handleResize);
}

まとめ

Vue.js のパフォーマンス最適化は、単なる技術的な課題を解決するだけでなく、ユーザー体験の向上とビジネス価値の創出につながります。

この記事で紹介した最適化テクニックを実践することで、以下のような効果が期待できます:

  • 応答性の向上: ユーザーの操作に対する反応速度が向上
  • メモリ使用量の削減: 効率的なリソース管理による安定性の向上
  • ユーザー満足度の向上: 快適なアプリケーション体験の提供
  • ビジネス価値の創出: ユーザー離脱率の低下とコンバージョン率の向上

最適化は一度で完了するものではありません。継続的な監視と改善を心がけ、ユーザーのニーズに合わせて最適化を進めていくことが重要です。

Vue.js の豊富な機能を活用しながら、パフォーマンスとユーザビリティのバランスを取ったアプリケーション開発を目指してください。

関連リンク