普段vueを使っていて、「これキャッシュしたいしcomputed使おう」で開発をしてきました。
キャッシュをしたら毎回計算し直す必要ないよねと思いつつ、なんでキャッシュってしなきゃいけないんだろうと疑問を持ちました。
最近はNext.jsをちょこちょこ触っているのですが、ReactのuseMemoだったりNext.jsのrouterだったりと新しいキャッシュの仕方について考える必要があったため、キャッシュについてまとめてみました。
フォン・ノイマン型アーキテクチャ
初期のコンピュータ
初期のコンピュータには大きな制限がありました。
ハードウェアとソフトウェアが完全に結合していたのです。
歯車のように決まったことをするための仕組みで、各プログラムには特定のハードウェア構成が必要でした。
ハードウェアを変更せずに異なるプログラムを実行することはほぼ不可でした。今となっては考えられないですね。
フォン・ノイマン型コンピューター
そこで、かの有名なフォン・ノイマンさんはメモリユニット(記憶装置)とCPU(処理装置)を分離する設計を導入し、コンピュータアーキテクチャに革命をもたらしました。
しかし、この分離は新たな課題を生み出しました。
時間の経過とともにCPUの処理速度は急速に向上しましたが、メモリのアクセス速度はそれに追いつかず、「フォン・ノイマン・ボトルネック」として知られる問題が発生しました。
CPUとメモリ間のこの性能差は、コンピュータシステムにおいて大きなボトルネックとなりました。
(本人も考案時に懸念したそうですね。)
キャッシュの誕生
フォン・ノイマン・ボトルネックに対処するため、今回のテーマであるキャッシュメモリが導入されました。
キャッシュはCPUとメインメモリの間の中間的な記憶層として機能します。
キャッシュは以下の2つの主要な特徴を持っています。
- より小さい記憶容量:キャッシュメモリは意図的にメインメモリより小さく設計
- より高速なアクセス速度:CPUにより近くサイズが小さいので、より高速なデータアクセスが可能
キャッシュの主な目的は、処理速度を向上させるために計算結果を一時的に保存することです。
キャッシュは記憶域(名詞)としても、データを保存する行為(動詞、「キャッシング」)としても使われる単語になります。
今回はどっちかというと名詞のことがメインですね
Vue.jsのComputed
Vue.jsの開発者ならみんな普段書いているcomputedは、フロントエンドフレームワークにおけるキャッシュのいい実装例です。
特に複雑な計算や頻繁なデータ加工が必要な場合に、パフォーマンスを大幅に向上させることができます。refとwatchで処理を書くことより管理もしやすいですし、writable computedを使うとシリアライズまでできるので大好きです。
Computedの内部実装
このComputedは、以下のような特徴を持つキャッシュシステムで実装しています。
-
リアクティブな依存関係の追跡
- 各computedプロパティは内部的に「Watcher」インスタンスを作成
- 初回評価時に、使用される全てのリアクティブデータを依存関係として自動的に記録
- これらの依存関係は動的に追跡され、実行時に変更されることもある
-
遅延評価(Lazy Evaluation)
- computedプロパティは、実際に値が必要になるまで計算を行わない
- テンプレートで使用されていないcomputedプロパティは評価されない
- これで不必要な計算を回避し、パフォーマンスを最適化できる
-
キャッシュの仕組み
- 計算結果は内部的なキャッシュに保存
- キャッシュされた値は、依存関係が変更されるまで再利用
- 依存関係が変更された場合のみ、
dirty
フラグが設定され再計算
使用例
<script setup>
import { ref, computed } from 'vue';
const items = ref([1, 2, 3, 4, 5]);
const multiplier = ref(2);
// 複雑な計算を含むcomputedプロパティ
const expensiveCalculation = computed(() => {
console.log('計算実行'); // 依存関係が変更された時のみ実行
return items.value
.filter(item => item > 2)
.map(item => item * multiplier.value)
.reduce((acc, curr) => acc + curr, 0);
});
const updateMultiplier = () => {
multiplier.value += 1; // これでexpensiveCalculationが再評価される
};
</script>
Vueのドキュメントは読めば読むほど味が出るのでぜひ読んでください!
Next.jsのキャッシュ
図の通りNext.jsは4つのキャッシュメカニズムを実装していて、大きく2つのカテゴリーに分かれています。
データキャッシュ
- リクエストメモ化
- データキャッシュ
ルートキャッシュ
- フルルートキャッシュ
- ルーターキャッシュ
メモリ vs 永続的キャッシュの理解
Next.jsはメモリ内キャッシュ(In-memory)と永続的(Persistent)キャッシュ両方を活用しています。
なんで二つあるかといいますと以下のようなそれぞれ異なる目的があります。
メモリ内キャッシュ:
CPUの中にメモリを用意したイメージ
- 特徴:
- より頻繁な入出力操作
- より軽い計算の複雑さ
- 高速な一時保存に使用
- サーバー再起動時にクリア
永続的キャッシュ:
ガチメモリーとの疎通のイメージ
- 特徴:
- より少ない入出力操作
- より高い計算の複雑さ
- サーバー再起動後も維持
- ビルド時のキャッシュに使用
使用例
リクエストメモ化(メモリ内):
// 結果はレンダリングプロセス中にメモリ内にキャッシュされる
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
データキャッシュ(永続的):
// 結果は永続的にキャッシュされる
fetch('https://api.example.com/data', { next: { revalidate: 3600 } })
フルルートキャッシュ(永続的):
// 静的ページはビルド時にキャッシュされる
export default function Page() {
return <h1>これはビルド時にキャッシュされます</h1>
}
ルーターキャッシュ(メモリ内):
// クライアントサイドのナビゲーション結果はメモリ内にキャッシュされる
'use client'
import { useRouter } from 'next/navigation'
export default function NavigationExample() {
const router = useRouter()
return <button onClick={() => router.push('/dashboard')}>ナビゲート</button>
}
おわりに
vueを書いているとあまりキャッシュについて深く考えることがないのでフレームワークが素晴らしいかもしれませんね。computed大好きです。
あと、箇条書きの情報出す時に生成AIにまかせるとめっちゃ便利ですね...