はじめに
Vue 3を用いたフロントエンド開発において、数十万件に及ぶ「大量のデータ」をクライアントサイドで直接保持・操作するケースがあります。
このとき、少量のデータを扱うときとまったく同じアプローチで実装してしまうと 思わぬところでパフォーマンスのボトルネックが発生し、
「数秒間、画面のUI操作が完全に固まってしまう(フリーズする)」といった事態を引き起こします。
この記事では、実際に直面したパフォーマンス問題とその解決策をベースに、Vue 3で大量データを扱う際の注意点とチューニング手法を紹介します。
(※Vue 3のComposition APIの基本を理解している方向けの記事となります。)
注意点①:安易な ref の使用によるProxyオーバーヘッド
例えば、以下のようなリスト更新のコードがあるとします。
// ❌ refを使用して定義する
const infoList = ref([]);
// リストの更新処理
const updateInfoList = (infos: InfoClass[]) => {
infoList.value = infos;
}
少量のデータであれば、このコードでまったく問題ありません。
infoList.value が更新されると、関連するDOMやComputedが自動的にリアクティブに再計算されます。
しかし、この infos が 20万件 のような大量データだった場合、代入した瞬間に数秒間UIがフリーズしてしまいます。
原因
Vue 3の ref は、配列やオブジェクトが代入されると、その中身の隅々にまで「監視機構(Proxy)」を再帰的に付与することで深いリアクティビティを実現しています。
20万件の配列を代入した場合、Vueは裏側で 20万個のオブジェクトに対してProxyを生成する処理を実行 してしまうため、 JavaScriptのメインスレッドが占有され、長時間のフリーズを引き起こしてしまいます。
(※ リストの中身である InfoClass の構造が複雑であればあるほど、この走査コストは指数関数的に跳ね上がります。)
解決策
このような大量データの場合、配列の中身(各要素のプロパティ)の深い監視を放棄し、「配列の参照が入れ替わったか」だけを検知する shallowRef に切り替えます。
// ⭕️ shallowRefを使用して定義する
const infoList = shallowRef([]);
// リストの更新処理
const updateInfoList = (infos: InfoClass[]) => {
infoList.value = infos;
}
これにより、Vueが行う裏側のProxy化処理は「 infoList という変数自体に対する1回」のみとなり、大量データの代入であっても処理は一瞬(ミリ秒単位)で完了し、フリーズは劇的に改善されます。
※注意
shallowRef を使用した場合、infoList.value[0].name = 'hoge' のような「配列の中の特定要素のプロパティの直接更新」はリアクティブに検知されず、画面に反映されません。
データを更新する際は、infoList.value = [...newList] のように配列自体の参照を新しく入れ替える必要があります。
注意点②:Propsで受け取った巨大配列のループ処理と toRaw
注意点①で shallowRef にするだけでは不十分なケースもあります。
親コンポーネントで shallowRef とした大量データを、子コンポーネントが props として受け取り、computed 内で絞り込み(フィルタリング)などを行うケースです。
const enabledItemNameList = computed(() => {
// ❌ props.infoList に対して直接配列にアクセスする
return props.infoList
.filter(item => item.isEnabled)
.map(item => item.name);
});
原因
親で shallowRef を使っていても、Vueの仕様上props というオブジェクト自体はリアクティブなProxyになっています。
そのため、filter や map などで props.infoList の中身にアクセスするたびに、 Vue内部の「変更監視トラッキング」が発動してしまいます。
20万件のループであればこれが20万回実行され、ちりつもでメインスレッドを長時間占有し、最終的には数秒単位で画面が固まることになります。
解決策
読み取り専用の巨大な配列を操作する際は、処理の直前に toRaw() を使用して、 Vueの監視プロキシを完全に剥がし「ただの素のJavaScript配列」に戻します。
const enabledItemNameList = computed(() => {
// ⭕️ toRawでProxyを完全に剥がす
const rawList = toRaw(props.infoList);
// 純粋なJS配列に対する操作になり、トラッキングのオーバーヘッドが消滅!
return rawList
.filter(item => item.isEnabled)
.map(item => item.name);
});
これによりVueのトラッキング・オーバーヘッドがゼロになり、純粋な計算処理のみが行われるようになります。
注意点③:filter や mapによるガベージコレクション頻発
注意点②で toRaw を使いProxyのオーバーヘッドは消えましたが、実はまだ問題が残っています。
配列の操作といえば filter や map をチェーン(連続)させるほうが可読性が高く推奨される書き方ですが、 大量データにおいてはこれが命取りになります。
const enabledItemNameList = computed(() => {
const rawList = toRaw(props.infoList);
// ❌ 巨大な配列に対するメソッドチェーン
return rawList
.filter(item => item.isEnabled)
.map(item => item.name);
});
原因
filter や map は、実行されるたびに「条件に合致した新しい配列」をメモリ上に丸ごと生成します。
20万件のデータにこれを実行すると、ループが複数回実行されるだけでなく、 巨大な中間配列がメモリ上に作られては破棄されるため、ブラウザのガベージコレクション(不要メモリの掃除機能)が強制発動 し、画面が数秒フリーズします。
解決策
配列の再生成を防ぐため、メソッドチェーンを諦め、「1つの for ループで全ての処理を終わらせる」 ように書き換えます。
const enabledItemNameList = computed(() => {
const rawList = toRaw(props.infoList);
// ⭕️ メソッドチェーンをやめ、単一の for...of ループで終わらせる
const resultList = [];
for (const item of rawList) {
// filterの代わり(条件に合わなければスキップ)
if (!item.isEnabled) continue;
// mapの代わり(今回は必要なプロパティだけを抽出して push)
resultList.push(item.name);
}
return resultList;
});
コードは少し長くなりますが、メモリ消費が極限まで抑えられた結果、不要なガベージコレクションが抑えられ、処理速度が跳ね上がります。
おわりに
Vue 3は非常に優秀なフレームワークですが、「リアクティブ(監視)」の便利さの裏には、相応の計算・メモリコストが隠れています。
1.深い監視を避ける(shallowRef)
2.ループ時はProxyを剥がす(toRaw)
3.無駄な配列生成を避ける(単一 for ループ)
これらを意識するだけで、数秒かかっていたフリーズをミリ秒単位まで抑え込むことができます。
フロントエンドで大規模な配列データを扱う際は、ぜひ参考にしてみてください。

