Vue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
Vue.js を使った開発では、リアクティブシステムの恩恵を受けながらも、実は気づかないうちにメモリリークを引き起こしているケースが少なくありません。特に watch、watchEffect、そして DOM イベントリスナーの登録周りは、適切なクリーンアップ処理を忘れてしまうと、アプリケーションのパフォーマンスが徐々に悪化していく原因となります。
この記事では、Vue.js におけるメモリリークの典型的なパターンと、その検知方法、そして具体的な対策について、実践的なコード例を交えながら詳しく解説していきます。
背景
メモリリークとは何か
メモリリークとは、アプリケーションが使用したメモリ領域を適切に解放せず、不要になったデータが残り続ける現象を指します。JavaScript のようなガベージコレクションを持つ言語でも、参照が残り続けることでメモリが解放されず、結果的にメモリ使用量が増加し続けることがあるのです。
Web アプリケーションにおいては、ページ遷移やコンポーネントの mount/unmount が頻繁に発生しますよね。この際、適切にリソースがクリーンアップされないと、見えない形でメモリが蓄積されていきます。
Vue.js のリアクティブシステムとメモリ管理
Vue.js は強力なリアクティブシステムを持っており、データの変更を自動的に検知して UI を更新してくれます。しかし、この便利さの裏側では、以下のような仕組みが動いています。
mermaidflowchart TB
reactive["リアクティブオブジェクト"] -->|変更検知| dep["依存関係トラッカー<br/>(Dep)"]
dep -->|通知| watcher1["Watcher 1"]
dep -->|通知| watcher2["Watcher 2"]
dep -->|通知| watcher3["Watcher N"]
watcher1 -->|コールバック実行| effect1["副作用関数"]
watcher2 -->|コールバック実行| effect2["副作用関数"]
watcher3 -->|コールバック実行| effect3["副作用関数"]
図で理解できる要点:
- リアクティブオブジェクトは依存関係トラッカーを持つ
- 各 Watcher が依存関係に登録される
- データ変更時に全ての Watcher に通知が行われる
この仕組みにより、データと UI が同期されます。しかし、コンポーネントが破棄された後も Watcher が残り続けると、不要な参照が保持され続けることになるのです。
一般的なメモリリーク発生のメカニズム
Vue.js アプリケーションでメモリリークが発生する主なシナリオは以下の通りです。
| # | シナリオ | 発生原因 | 影響度 |
|---|---|---|---|
| 1 | watch の解除忘れ | watchEffect や watch の戻り値を使わない | ★★★ |
| 2 | イベントリスナーの解除忘れ | addEventListener の removeEventListener 忘れ | ★★★ |
| 3 | タイマーの解除忘れ | setInterval / setTimeout のクリア忘れ | ★★☆ |
| 4 | グローバル状態への参照 | Store や EventBus への永続的な参照 | ★★☆ |
| 5 | クロージャによる参照保持 | コールバック内での大きなオブジェクト参照 | ★☆☆ |
これらのパターンは、開発中には気づきにくく、本番環境で長時間稼働して初めて問題が顕在化することも珍しくありません。
課題
watch と watchEffect でのメモリリーク
Vue 3 の Composition API では、watch と watchEffect を使ってリアクティブなデータの変更を監視できます。しかし、これらは内部的に Watcher インスタンスを作成し、リアクティブシステムに登録されるため、適切に停止しないと参照が残り続けてしまうのです。
以下は典型的な問題のあるコードです。
typescriptimport { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
// この watchEffect は停止されない
watchEffect(() => {
console.log(`count is now: ${count.value}`);
});
// コンポーネントが破棄されても watchEffect は動き続ける
},
};
上記のコードは一見問題なさそうですが、Composition API の setup() 関数内で作成された watchEffect は、通常コンポーネントのライフサイクルと自動的に紐づけられます。しかし、以下のような場合は自動的にクリーンアップされません。
問題が発生するケース:
| # | ケース | 詳細 |
|---|---|---|
| 1 | 非同期処理後の watch 作成 | setTimeout や fetch のコールバック内での watch 作成 |
| 2 | 条件付きの watch 作成 | if 文などの条件分岐内での watch 作成 |
| 3 | ループ内での watch 作成 | v-for や配列の forEach 内での watch 作成 |
| 4 | setup 外での watch 作成 | グローバルスコープやユーティリティ関数内での watch 作成 |
イベントリスナーの登録と解除
DOM イベントリスナーは、Vue のリアクティブシステムの外側で動作するため、より明示的な管理が必要です。addEventListener で登録したイベントは、必ず removeEventListener で解除しなければなりません。
typescriptimport { onMounted } from 'vue';
export default {
setup() {
// 問題のあるコード
onMounted(() => {
window.addEventListener('resize', handleResize);
// removeEventListener が呼ばれない!
});
const handleResize = () => {
console.log('Window resized');
};
},
};
上記のコードでは、コンポーネントが破棄された後も handleResize 関数への参照が残り続けます。特に、イベントハンドラーが大きなオブジェクトやクロージャを参照している場合、メモリリークの影響は深刻になるでしょう。
タイマー関数の管理
setInterval や setTimeout も同様の問題を引き起こします。
typescriptimport { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
// 5秒ごとにデータを取得
setInterval(() => {
fetchData();
}, 5000);
// clearInterval が呼ばれない!
});
},
};
このコードは、コンポーネントが破棄された後も 5 秒ごとに fetchData() を実行し続けてしまいます。
グローバル EventBus パターンの罠
Vue 2 の時代によく使われていた EventBus パターンも、メモリリークの温床となります。
typescript// eventBus.ts
import { ref } from 'vue';
export const eventBus = ref<Map<string, Function[]>>(
new Map()
);
export function emit(event: string, data: any) {
const handlers = eventBus.value.get(event);
handlers?.forEach((handler) => handler(data));
}
export function on(event: string, handler: Function) {
const handlers = eventBus.value.get(event) || [];
handlers.push(handler);
eventBus.value.set(event, handlers);
// off する仕組みがない、または使われていない
}
このような実装では、コンポーネントが破棄されてもイベントハンドラーが EventBus に残り続けてしまいます。
以下の図は、メモリリークが発生する流れを示しています。
mermaidsequenceDiagram
participant C as コンポーネント
participant W as Watcher/Listener
participant M as メモリ
C->>W: watch/effect/listener 登録
W->>M: 参照を保持
Note over C,M: コンポーネント使用中
C->>C: コンポーネント破棄
Note over W,M: しかし参照は残る
W->>M: まだ参照を保持中
Note over W,M: メモリリーク発生!
rect rgb(255, 200, 200)
Note over W,M: クリーンアップされない<br/>Watcher や Listener が<br/>メモリに残り続ける
end
図で理解できる要点:
- コンポーネント破棄後も Watcher やリスナーが残る
- メモリ上の参照が解放されない
- 時間とともにメモリ使用量が増加する
解決策
watch と watchEffect の適切な停止
watch と watchEffect は、呼び出し時に停止関数を返します。この関数を適切に呼び出すことで、メモリリークを防ぐことができるのです。
基本的な停止パターン
typescriptimport { ref, watchEffect, onUnmounted } from 'vue';
上記は、必要なインポート文です。onUnmounted はコンポーネントが破棄される際に実行されるライフサイクルフックになります。
typescriptexport default {
setup() {
const count = ref(0)
// watchEffect は停止関数を返す
const stopWatch = watchEffect(() => {
console.log(`count is now: ${count.value}`)
})
watchEffect の戻り値として停止関数が返されます。この関数を変数に保存しておくことで、後で明示的に停止できるようになるのです。
typescript // コンポーネント破棄時に停止
onUnmounted(() => {
stopWatch()
})
}
}
onUnmounted フックで停止関数を呼び出すことにより、コンポーネントが破棄される際に確実に watch を停止できます。
条件付き watch の安全な実装
条件分岐内で watch を作成する場合は、以下のように実装します。
typescriptimport { ref, watchEffect, onUnmounted } from 'vue'
export default {
setup() {
const isEnabled = ref(true)
const data = ref(null)
let stopWatch: (() => void) | null = null
停止関数を保存するための変数を宣言します。型は (() => void) | null とし、初期値は null です。
typescript// 条件によって watch を開始
if (isEnabled.value) {
stopWatch = watchEffect(() => {
console.log('Data:', data.value);
});
}
条件が満たされた場合のみ watch を作成し、停止関数を保存しておきます。
typescript // クリーンアップ
onUnmounted(() => {
if (stopWatch) {
stopWatch()
}
})
}
}
onUnmounted で、停止関数が存在する場合のみ呼び出すようにします。
非同期処理後の watch 作成
非同期処理のコールバック内で watch を作成する場合は、コンポーネントが既に破棄されている可能性を考慮する必要があります。
typescriptimport { ref, watchEffect, onUnmounted, getCurrentInstance } from 'vue'
export default {
setup() {
const data = ref(null)
const stopWatchers: Array<() => void> = []
複数の停止関数を管理するための配列を用意します。
typescript// 現在のインスタンスを取得
const instance = getCurrentInstance();
setTimeout(() => {
// コンポーネントがまだマウントされているかチェック
if (instance && !instance.isUnmounted) {
const stopWatch = watchEffect(() => {
console.log('Delayed watch:', data.value);
});
stopWatchers.push(stopWatch);
}
}, 1000);
getCurrentInstance() を使ってコンポーネントインスタンスを取得し、isUnmounted プロパティで破棄済みかどうかを確認します。これにより、非同期処理が完了した時点でコンポーネントが既に破棄されている場合は、watch を作成しないようにできるのです。
typescript // すべての watch を停止
onUnmounted(() => {
stopWatchers.forEach(stop => stop())
})
}
}
配列に保存した全ての停止関数を実行することで、確実にクリーンアップが行われます。
イベントリスナーの適切な管理
イベントリスナーは、登録と解除をペアで行う必要があります。Vue 3 では、これを簡潔に実装できるパターンがいくつかあります。
基本的なイベントリスナーの管理
typescriptimport { onMounted, onUnmounted } from 'vue'
export default {
setup() {
// イベントハンドラーを関数として定義
const handleResize = () => {
console.log('Window resized')
}
イベントハンドラー関数を定義します。関数を変数に代入することで、同じ関数の参照を addEventListener と removeEventListener の両方で使用できるようになります。
typescript// マウント時に登録
onMounted(() => {
window.addEventListener('resize', handleResize);
});
コンポーネントがマウントされた際にイベントリスナーを登録します。
typescript // アンマウント時に解除
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
}
}
コンポーネントがアンマウントされる際に、同じ関数の参照を使ってイベントリスナーを解除します。これにより、メモリリークを防げるのです。
複数のイベントリスナーを管理する Composable
複数のイベントリスナーを扱う場合は、Composable として抽象化すると便利です。
typescript// useEventListener.ts
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(
target: Window | HTMLElement,
event: string,
handler: EventListener
) {
この Composable は、イベントリスナーの登録と解除を自動化してくれます。引数として、対象要素、イベント名、ハンドラー関数を受け取ります。
typescript onMounted(() => {
target.addEventListener(event, handler)
})
onUnmounted(() => {
target.removeEventListener(event, handler)
})
}
onMounted と onUnmounted でイベントの登録と解除を行うことで、呼び出し側では意識せずともクリーンアップが行われるようになります。
使用例
typescriptimport { useEventListener } from './useEventListener';
export default {
setup() {
// シンプルな使い方
useEventListener(window, 'resize', () => {
console.log('Window resized');
});
// 複数のイベントも簡単に管理
useEventListener(window, 'scroll', () => {
console.log('Window scrolled');
});
},
};
この Composable を使うことで、イベントリスナーの解除を忘れる心配がなくなります。
タイマーの適切な管理
setInterval や setTimeout も、同様にクリーンアップが必要です。
setInterval の管理
typescriptimport { onMounted, onUnmounted } from 'vue'
export default {
setup() {
let intervalId: number | null = null
タイマー ID を保存するための変数を宣言します。
typescriptonMounted(() => {
// 5秒ごとにデータを取得
intervalId = setInterval(() => {
fetchData();
}, 5000);
});
setInterval の戻り値であるタイマー ID を保存しておきます。
typescript onUnmounted(() => {
// タイマーをクリア
if (intervalId !== null) {
clearInterval(intervalId)
}
})
}
}
コンポーネントが破棄される際に、保存しておいたタイマー ID を使って clearInterval を呼び出すことで、タイマーを停止できます。
タイマー管理用 Composable
タイマーも Composable として抽象化できます。
typescript// useInterval.ts
import { onUnmounted } from 'vue';
export function useInterval(
callback: () => void,
delay: number
) {
const intervalId = setInterval(callback, delay);
// 自動的にクリーンアップ
onUnmounted(() => {
clearInterval(intervalId);
});
// 手動で停止したい場合のために停止関数を返す
return () => clearInterval(intervalId);
}
この Composable は、タイマーの開始と自動クリーンアップを行いつつ、必要に応じて手動で停止できる関数も返してくれます。
使用例
typescriptimport { useInterval } from './useInterval';
export default {
setup() {
// 自動的にクリーンアップされる
useInterval(() => {
console.log('Polling...');
}, 5000);
},
};
とてもシンプルに、安全にタイマーを使うことができますね。
グローバル状態管理でのメモリリーク対策
グローバル状態(Store や EventBus)を使う場合も、適切なクリーンアップが必要です。
Pinia を使った安全な状態管理
typescript// stores/user.ts
import { defineStore } from 'pinia';
Pinia は Vue 公式の状態管理ライブラリです。まず、defineStore をインポートします。
typescriptexport const useUserStore = defineStore('user', {
state: () => ({
users: [] as User[],
subscribers: [] as Function[]
}),
Store の状態として、ユーザーデータと購読者のコールバック関数を管理します。
typescript actions: {
subscribe(callback: Function) {
this.subscribers.push(callback)
// 購読解除関数を返す
return () => {
const index = this.subscribers.indexOf(callback)
if (index > -1) {
this.subscribers.splice(index, 1)
}
}
}
}
})
subscribe メソッドは購読解除関数を返すため、呼び出し側で確実にクリーンアップできます。
コンポーネントでの使用例
typescriptimport { onUnmounted } from 'vue'
import { useUserStore } from '@/stores/user'
export default {
setup() {
const userStore = useUserStore()
// 購読と同時に解除関数を取得
const unsubscribe = userStore.subscribe((users) => {
console.log('Users updated:', users)
})
購読時に解除関数を受け取ることで、後でクリーンアップできるようにします。
typescript // コンポーネント破棄時に購読解除
onUnmounted(() => {
unsubscribe()
})
}
}
このパターンにより、グローバル状態を安全に使用できるのです。
以下の図は、適切なクリーンアップが行われた場合のフローを示します。
mermaidflowchart TB
mount["コンポーネント<br/>マウント"] --> register["watch/effect/<br/>listener 登録"]
register --> store["停止関数を<br/>保存"]
store --> use["コンポーネント<br/>使用中"]
use --> unmount["コンポーネント<br/>破棄"]
unmount --> cleanup["onUnmounted<br/>実行"]
cleanup --> stop["停止関数<br/>呼び出し"]
stop --> release["メモリ解放"]
style release fill:#90EE90
style cleanup fill:#87CEEB
style stop fill:#87CEEB
図で理解できる要点:
- マウント時に登録、アンマウント時に解除のペア
- 停止関数を確実に保存しておく
- onUnmounted でクリーンアップを実行
- メモリが適切に解放される
具体例
メモリリークを検知する方法
メモリリークが発生しているかどうかを確認するには、Chrome DevTools の Memory プロファイラーが非常に有効です。
Chrome DevTools での検知手順
- Chrome DevTools を開く(F12 または Cmd+Option+I)
- Memory タブを選択
- Heap snapshot を選択
- 「Take snapshot」をクリックして初期状態を記録
- アプリケーションを操作(ページ遷移、コンポーネントの表示/非表示など)
- 再度「Take snapshot」をクリック
- 2 つのスナップショットを比較
メモリリーク判定の目安
| # | 指標 | 正常 | メモリリーク疑い |
|---|---|---|---|
| 1 | Heap サイズ | 安定または微増 | 継続的に増加 |
| 2 | Detached DOM | 0 または少数 | 多数存在 |
| 3 | イベントリスナー数 | 安定 | 増加し続ける |
| 4 | オブジェクト数 | 安定 | 増加し続ける |
コンソールでの簡易チェック
以下のコードを Console に貼り付けて実行することで、現在のメモリ使用量を確認できます。
javascript// メモリ使用量を確認
if (performance.memory) {
console.log(
'Used JS Heap:',
(performance.memory.usedJSHeapSize / 1048576).toFixed(
2
) + ' MB'
);
console.log(
'Total JS Heap:',
(performance.memory.totalJSHeapSize / 1048576).toFixed(
2
) + ' MB'
);
}
このコードは、現在の JavaScript ヒープメモリの使用量と合計量を MB 単位で表示します。performance.memory は Chrome でのみ利用可能ですので、注意してください。
javascript// イベントリスナーの数を確認
const listeners = getEventListeners(window);
console.log('Window event listeners:', listeners);
getEventListeners() は Chrome DevTools Console でのみ使える関数で、指定した要素に登録されているイベントリスナーの一覧を取得できます。
実践的なデバッグ例
実際のアプリケーションでメモリリークをデバッグする具体例を見ていきましょう。
問題のあるコンポーネント
typescript// UserList.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const users = ref<User[]>([])
let intervalId: number
onMounted(() => {
// 問題1: setInterval のクリアがない
intervalId = setInterval(() => {
fetchUsers()
}, 3000)
// 問題2: イベントリスナーの解除がない
window.addEventListener('resize', handleResize)
})
このコードには、タイマーとイベントリスナーのクリーンアップがありません。
typescript// 問題3: watch の停止がない
watch(users, (newUsers) => {
// 重い処理
processUsers(newUsers)
})
const handleResize = () => {
console.log('Resized')
}
async function fetchUsers() {
// API呼び出し
const response = await fetch('/api/users')
users.value = await response.json()
}
</script>
watch も停止されていないため、コンポーネント破棄後も動作し続ける可能性があります。
修正後のコンポーネント
それでは、上記の問題を修正していきましょう。
typescript// UserList.vue (修正版)
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
const users = ref<User[]>([])
let intervalId: number | null = null
let stopWatch: (() => void) | null = null
停止関数を保存するための変数を宣言します。
typescriptonMounted(() => {
// タイマーIDを保存
intervalId = setInterval(() => {
fetchUsers();
}, 3000);
// イベントリスナーを登録
window.addEventListener('resize', handleResize);
});
マウント時の処理です。タイマー ID を保存しておくことを忘れないでください。
typescript// watch の停止関数を保存
stopWatch = watch(users, (newUsers) => {
processUsers(newUsers);
});
watch の戻り値である停止関数を保存します。
typescript// クリーンアップ処理
onUnmounted(() => {
// タイマーを停止
if (intervalId !== null) {
clearInterval(intervalId);
}
// イベントリスナーを解除
window.removeEventListener('resize', handleResize);
// watch を停止
if (stopWatch) {
stopWatch();
}
});
onUnmounted で全てのリソースをクリーンアップします。これにより、メモリリークを防げるようになりました。
typescriptconst handleResize = () => {
console.log('Resized')
}
async function fetchUsers() {
const response = await fetch('/api/users')
users.value = await response.json()
}
</script>
その他の関数は変更なしです。
汎用的なクリーンアップ用 Composable
複数のクリーンアップ処理を統一的に管理するための Composable を作成してみましょう。
typescript// useCleanup.ts
import { onUnmounted } from 'vue'
export function useCleanup() {
// クリーンアップ関数を保存する配列
const cleanupFunctions: Array<() => void> = []
複数のクリーンアップ関数を管理するための配列を用意します。
typescript// クリーンアップ関数を登録
function addCleanup(cleanup: () => void) {
cleanupFunctions.push(cleanup);
}
この関数を使って、クリーンアップ処理を登録していきます。
typescript// コンポーネント破棄時に全て実行
onUnmounted(() => {
cleanupFunctions.forEach((cleanup) => cleanup());
});
onUnmounted で、登録された全てのクリーンアップ関数を実行します。
typescript return {
addCleanup
}
}
外部から使えるように、addCleanup 関数を返します。
使用例
typescriptimport { useCleanup } from './useCleanup'
import { ref, watchEffect } from 'vue'
export default {
setup() {
const { addCleanup } = useCleanup()
const count = ref(0)
useCleanup を呼び出して、addCleanup 関数を取得します。
typescript// watch の停止関数を登録
const stopWatch = watchEffect(() => {
console.log('Count:', count.value);
});
addCleanup(stopWatch);
watch の停止関数を addCleanup に登録することで、自動的にクリーンアップされるようになります。
typescript// タイマーの停止関数を登録
const intervalId = setInterval(() => {
count.value++;
}, 1000);
addCleanup(() => clearInterval(intervalId));
タイマーのクリーンアップ処理も登録できます。
typescript // イベントリスナーの解除関数を登録
const handleClick = () => console.log('Clicked')
document.addEventListener('click', handleClick)
addCleanup(() => {
document.removeEventListener('click', handleClick)
})
// onUnmounted で自動的に全て実行される
}
}
この Composable を使うことで、様々なクリーンアップ処理を一元管理できるようになります。
パフォーマンス監視の自動化
開発中にメモリリークを早期発見するため、パフォーマンス監視を自動化することも有効です。
typescript// useMemoryMonitor.ts
import { onMounted, onUnmounted } from 'vue'
export function useMemoryMonitor(componentName: string) {
let intervalId: number | null = null
const memorySnapshots: number[] = []
メモリスナップショットを保存する配列を用意します。
typescriptonMounted(() => {
// 開発環境でのみ監視
if (process.env.NODE_ENV === 'development') {
console.log(`[${componentName}] メモリ監視開始`);
intervalId = setInterval(() => {
if (performance.memory) {
const used =
performance.memory.usedJSHeapSize / 1048576;
memorySnapshots.push(used);
// 過去5回の平均と比較
if (memorySnapshots.length > 5) {
const recent = memorySnapshots.slice(-5);
const average =
recent.reduce((a, b) => a + b) / 5;
const growth = ((used - average) / average) * 100;
// 10%以上増加していたら警告
if (growth > 10) {
console.warn(
`[${componentName}] メモリ増加検知: ${growth.toFixed(
2
)}%`
);
}
}
}
}, 5000);
}
});
5 秒ごとにメモリ使用量をチェックし、急激な増加を検知した場合に警告を出力します。
typescript onUnmounted(() => {
if (intervalId !== null) {
clearInterval(intervalId)
console.log(`[${componentName}] メモリ監視終了`)
}
})
}
コンポーネント破棄時に監視を停止します。
使用例
typescriptimport { useMemoryMonitor } from './useMemoryMonitor';
export default {
setup() {
// コンポーネント名を指定して監視開始
useMemoryMonitor('UserList');
// 通常のコンポーネントロジック
},
};
開発中にこの Composable を使うことで、メモリリークの兆候を早期に発見できるようになります。
以下の図は、メモリリークのデバッグフローを示しています。
mermaidflowchart TD
start["メモリリーク<br/>疑いあり"] --> snapshot1["初期スナップ<br/>ショット取得"]
snapshot1 --> operation["アプリケーション<br/>操作実施"]
operation --> snapshot2["2回目スナップ<br/>ショット取得"]
snapshot2 --> compare["2つのスナップ<br/>ショット比較"]
compare --> check_heap{"Heapサイズ<br/>増加?"}
check_heap -->|はい| check_detached["Detached DOM<br/>確認"]
check_heap -->|いいえ| normal["正常"]
check_detached --> check_listeners["イベントリスナー<br/>数確認"]
check_listeners --> identify["原因特定"]
identify --> fix["クリーンアップ<br/>処理追加"]
fix --> verify["再テスト"]
verify --> check_heap
style normal fill:#90EE90
style fix fill:#FFB6C1
style identify fill:#FFD700
図で理解できる要点:
- スナップショット比較で問題を特定
- Heap サイズ、Detached DOM、リスナー数を確認
- 原因を特定してクリーンアップ処理を追加
- 再テストで効果を検証
まとめ
Vue.js アプリケーションにおけるメモリリークは、watch、watchEffect、イベントリスナー、タイマーなどの適切なクリーンアップ処理を怠ることで発生します。本記事で解説した以下のポイントを押さえることで、メモリリークを防ぎ、パフォーマンスの高いアプリケーションを構築できるでしょう。
重要なポイント:
- watch と watchEffect:必ず停止関数を保存し、
onUnmountedで呼び出す - イベントリスナー:
addEventListenerとremoveEventListenerをペアで使用 - タイマー:
setIntervalやsetTimeoutは必ずclearInterval、clearTimeoutで停止 - Composable の活用:クリーンアップロジックを Composable として抽象化し、再利用性を高める
- 定期的な監視:Chrome DevTools の Memory プロファイラーを使って定期的にメモリ状態を確認
- 自動検知:開発環境でメモリ監視 Composable を使い、早期発見する
メモリリークは目に見えない問題だからこそ、開発の初期段階から意識的にクリーンアップ処理を実装することが重要です。今回紹介した Composable のパターンを活用することで、安全で保守性の高いコードを書けるようになりますよ。
適切なメモリ管理により、ユーザーにとって快適で、長時間使用しても安定したアプリケーションを提供していきましょう。
関連リンク
articleVue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
articleVue.js リアクティビティ内部解剖:Proxy/ref/computed を図で読み解く
articleVue.js 本番運用チェックリスト:CSP/SRI/Cache-Control/エラーログの要点
articleVue.js コンポーネント API 設計:props/emit/slot を最小 API でまとめる
articleVue.js `<script setup>` マクロ辞典:defineProps/defineEmits/defineExpose/withDefaults
articleVue.js を macOS + yarn で最短セットアップ:ESLint/Prettier/TS/パスエイリアス
articlePlaywright Debug モード活用:テストが落ちる原因を 5 分で特定する手順
articleVue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
articleTailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策
articlePHP 構文チートシート:配列・クロージャ・型宣言・match を一枚で把握
articleSvelte ストアエラー「store is not a function」を解決:writable/derived の落とし穴
articleNext.js の 観測可能性入門:OpenTelemetry/Sentry/Vercel Analytics 連携
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来