長時間稼働させたSPA(Single Page Application)が、徐々に重くなる現象に遭遇したことはないだろうか。画面遷移を繰り返すたびにメモリ使用量が増え続け、最終的にはブラウザがクラッシュする。その原因の多くは、コンポーネントが破棄された後もメモリ上に居座り続ける「幽霊(リーク)」にある。ここでは、Vue.jsにおける正しい後始末の方法を解説する。
メモリリークとは、不要になったオブジェクト(コンポーネントやDOM要素)への参照が残存し、ガベージコレクション(GC)によってメモリが解放されない状態を指す。
なぜコンポーネントは「居座る」のか(パーティの例え)
Concept: あなたが友人の家でパーティを開き(コンポーネントのマウント)、帰宅する(アンマウント)状況を想像してほしい。
あなたが帰宅しても、持ち込んだ荷物(イベントリスナーやタイマー)を友人の家に置きっぱなしにすれば、次の客が来るたびに部屋は荷物で埋め尽くされてしまう。Vue.jsはコンポーネント自身のDOMは自動で片付けるが、コンポーネントがグローバル領域(windowやdocument)に配置した荷物までは自動で片付けてくれない。これらは開発者が明示的に持ち帰る必要がある。
実装:Composition APIによる正しいクリーンアップ
Vue 3のComposition API(<script setup>)を使用する場合、onUnmounted(またはonBeforeUnmount)フックが「荷物を持ち帰る」タイミングである。
Bad Pattern: 解除を忘れた実装
<script setup>
import { onMounted } from 'vue';
// マウント時にウィンドウのリサイズを監視
// 警告: コンポーネントが消えても、このリスナーは永遠に残る
onMounted(() => {
window.addEventListener('resize', (e) => {
console.log('Resizing...', e);
});
});
</script>
上記コードでは、ページ遷移(コンポーネント破棄)後もリサイズするたびにログが出力され続ける。これがメモリリークの正体だ。以下が修正版である。
Good Pattern: ペアで管理する実装
<script setup>
import { onMounted, onUnmounted } from 'vue';
// 名前付き関数として定義(removeEventListenerで参照するため)
const handleResize = (e) => {
console.log('Window resized', window.innerWidth);
};
// 1. 登録 (Mount)
onMounted(() => {
window.addEventListener('resize', handleResize);
});
// 2. 解除 (Unmount)
// コンポーネント破棄時に必ず解除する
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
Watch out for: 無名関数(アロー関数)をaddEventListenerに直接渡さないこと。参照が保持できないため、removeEventListenerで解除できなくなる。
Chrome DevToolsによるリーク特定
コードレビューだけではリークを見逃すことがある。確実な証拠はChrome DevToolsのMemoryタブで掴むことができる。
- Chrome DevToolsを開き、「Memory」タブを選択する。
- 「Heap snapshot」を選択する。
- 操作前のスナップショットを撮る。
- 疑わしい画面遷移(コンポーネントの表示・非表示)を数回繰り返す。
- 操作後のスナップショットを撮る。
- 「Class filter」に
Detachedと入力する。
もし結果に Detached HTMLDivElement などが表示され、その数が操作回数に応じて増えているなら、それはメモリリークである。DOM要素がJavaScriptの参照(リスナーやタイマー)によって掴まれたまま、GC回収されていない状態を示している。
Frequently Asked Questions
Q. setIntervalもリークの原因になりますか?
A. はい、非常に一般的な原因です。setIntervalは明示的にclearIntervalされるまで永久に動き続けます。const timer = setInterval(...)でIDを保存し、onUnmountedで必ずクリアしてください。
Q. KeepAliveを使用している場合のフックはどうなりますか?
A. <KeepAlive>でキャッシュされたコンポーネントはアンマウントされません。代わりにonActivated(表示時)とonDeactivated(非表示時)フックが呼ばれます。リスナーの着脱はこれらのフックで行う必要があります。
Q. Vue 2のOptions APIではどうすればいいですか?
A. Vue 2(またはVue 3のOptions API)では、beforeDestroy(Vue 3ではbeforeUnmount)フックを使用します。ロジックは同じで、this.$elなどが消える直前にイベントリスナーを解除します。
Post a Comment