T-CREATOR

Apollo Client のキャッシュ初期化戦略:既存データ注入・rehydration・GC 設定

Apollo Client のキャッシュ初期化戦略:既存データ注入・rehydration・GC 設定

Apollo Client でキャッシュを初期化する方法を学ぶことで、ページ遷移やリロード時のユーザー体験を大きく向上できます。今回は、既存データをキャッシュに注入する方法、サーバーサイドレンダリング(SSR)時の rehydration、そしてガベージコレクション(GC)の設定について、実践的なコード例とともに詳しく解説していきます。

背景

Apollo Client のキャッシュとは

Apollo Client は GraphQL クライアントとして、取得したデータを自動的にキャッシュする機能を持っています。このキャッシュは InMemoryCache として実装されており、正規化されたデータストアとして機能します。

キャッシュを適切に初期化することで、以下のようなメリットが得られるのです。

  • 初回レンダリング時のデータ表示が高速化される
  • ネットワークリクエストの削減によるパフォーマンス向上
  • ユーザーが以前閲覧したデータの即座な表示
  • サーバーサイドレンダリング(SSR)との統合が可能になる

キャッシュ初期化が必要になるケース

Apollo Client のキャッシュ初期化は、以下のようなシナリオで特に重要になります。

Next.js などの SSR 環境での利用

サーバー側で取得したデータをクライアント側のキャッシュに引き継ぐ必要があります。これにより、ページ読み込み時に既にデータが表示された状態を実現できますね。

永続化されたキャッシュの復元

LocalStorage や IndexedDB に保存したキャッシュデータを、アプリケーション起動時に復元することで、オフライン対応やページリロード時の体験が向上します。

テスト環境でのモックデータ注入

テストコードにおいて、特定の状態を再現するために初期データをキャッシュに設定する場面があります。

以下の図は、Apollo Client のキャッシュが動作する基本的なフローを示しています。

mermaidflowchart TB
  component["React<br/>コンポーネント"] -->|useQuery| client["Apollo<br/>Client"]
  client -->|キャッシュ確認| cache["InMemory<br/>Cache"]
  cache -->|キャッシュなし| network["GraphQL<br/>API"]
  network -->|レスポンス| cache
  cache -->|データ返却| component
  cache -->|キャッシュあり| component

このように、Apollo Client はキャッシュを中心としたデータフローを構築します。

課題

キャッシュ初期化における一般的な課題

Apollo Client のキャッシュを初期化する際には、いくつかの技術的な課題に直面することがあります。

データの正規化形式への変換

Apollo Client のキャッシュは正規化されたデータ構造を持ちます。通常の JSON データをそのまま注入しても、期待通りに動作しない場合があるのです。

SSR と CSR の状態同期

サーバーサイドで生成されたキャッシュデータを、クライアントサイドで正確に復元する必要があります。この際、データの形式や型の不一致が問題になることがあります。

メモリ管理とパフォーマンス

大量のデータをキャッシュに保持し続けると、メモリ消費が増大します。適切なガベージコレクション(GC)設定がないと、アプリケーションのパフォーマンスが低下してしまいますね。

タイミングの問題

キャッシュの初期化を行うタイミングが適切でないと、コンポーネントのレンダリングとデータの準備が競合する可能性があります。

以下の図は、これらの課題がどのように関連しているかを示しています。

mermaidflowchart LR
  init["キャッシュ<br/>初期化"] --> norm["正規化<br/>形式"]
  init --> sync["SSR/CSR<br/>同期"]
  init --> gc["メモリ<br/>管理"]
  norm -->|形式不一致| error1["データ取得<br/>失敗"]
  sync -->|状態不整合| error2["hydration<br/>エラー"]
  gc -->|設定不足| error3["メモリ<br/>リーク"]

これらの課題を理解することで、適切な初期化戦略を選択できるようになります。

解決策

キャッシュへの既存データ注入

Apollo Client では、writeQuerywriteFragment という 2 つの主要な API を使ってキャッシュにデータを注入できます。それぞれの使い方を見ていきましょう。

writeQuery によるクエリ単位のデータ注入

writeQuery は、GraphQL クエリと対応するデータをキャッシュに書き込むメソッドです。

typescriptimport {
  ApolloClient,
  InMemoryCache,
  gql,
} from '@apollo/client';

// Apollo Client のインスタンスを作成
const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache(),
});

次に、クエリを定義してデータを注入します。

typescript// GraphQL クエリの定義
const GET_USER_QUERY = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

実際にキャッシュへデータを書き込む処理は以下のようになります。

typescript// キャッシュにデータを注入
client.writeQuery({
  query: GET_USER_QUERY,
  variables: {
    id: '123',
  },
  data: {
    user: {
      __typename: 'User', // 型名は必須
      id: '123',
      name: '田中太郎',
      email: 'tanaka@example.com',
    },
  },
});

重要なポイント: __typename フィールドは Apollo Client がデータを正規化するために必須です。このフィールドを省略すると、キャッシュが正常に機能しない場合があります。

writeFragment による部分的なデータ更新

writeFragment は、特定のオブジェクトの一部だけを更新する際に便利です。

typescriptimport { gql } from '@apollo/client';

// フラグメントの定義
const USER_FRAGMENT = gql`
  fragment UserInfo on User {
    id
    name
    email
  }
`;

フラグメントを使ってキャッシュに書き込みます。

typescript// 特定のユーザーデータを更新
client.writeFragment({
  id: 'User:123', // キャッシュID(型名:ID)
  fragment: USER_FRAGMENT,
  data: {
    __typename: 'User',
    id: '123',
    name: '田中次郎', // 名前だけを更新
    email: 'tanaka@example.com',
  },
});

この方法は、既存のキャッシュデータの一部だけを更新したい場合に効率的です。

SSR における rehydration 戦略

Next.js などの SSR 環境では、サーバーで取得したデータをクライアントに引き継ぐ必要があります。この処理を「rehydration(再水和)」と呼びます。

サーバーサイドでのキャッシュ抽出

まず、サーバーサイドで Apollo Client を初期化し、データを取得します。

typescriptimport {
  ApolloClient,
  InMemoryCache,
  HttpLink,
} from '@apollo/client';
import { GetServerSideProps } from 'next';

// サーバーサイド用の Apollo Client を作成
function createApolloClient() {
  return new ApolloClient({
    ssrMode: true, // SSR モードを有効化
    link: new HttpLink({
      uri: 'https://api.example.com/graphql',
      credentials: 'same-origin',
    }),
    cache: new InMemoryCache(),
  });
}

Next.js の getServerSideProps でデータを取得し、キャッシュを抽出します。

typescriptexport const getServerSideProps: GetServerSideProps =
  async (context) => {
    const apolloClient = createApolloClient();

    // クエリを実行してデータを取得
    await apolloClient.query({
      query: GET_USER_QUERY,
      variables: { id: '123' },
    });

    // キャッシュの状態を抽出
    const apolloState = apolloClient.cache.extract();

    return {
      props: {
        apolloState, // クライアントに渡す
      },
    };
  };

cache.extract() メソッドは、キャッシュの内部状態を JSON 形式で取り出します。

クライアントサイドでのキャッシュ復元

次に、クライアントサイドで受け取ったキャッシュデータを復元します。

typescriptimport {
  ApolloClient,
  InMemoryCache,
  HttpLink,
} from '@apollo/client';

// クライアントサイド用の Apollo Client を作成
function createApolloClient(initialState = {}) {
  return new ApolloClient({
    ssrMode: false, // クライアントサイドでは false
    link: new HttpLink({
      uri: 'https://api.example.com/graphql',
      credentials: 'same-origin',
    }),
    cache: new InMemoryCache().restore(initialState), // キャッシュを復元
  });
}

Next.js のページコンポーネントで Apollo Client を初期化します。

typescriptimport { ApolloProvider } from '@apollo/client';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  // サーバーから渡された状態でクライアントを初期化
  const client = createApolloClient(
    pageProps.apolloState || {}
  );

  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;

この仕組みにより、サーバーで取得したデータがクライアントのキャッシュに即座に反映され、初回レンダリング時からデータが表示されます。

以下の図は、SSR における rehydration の流れを示しています。

mermaidsequenceDiagram
  participant Server as Next.js<br/>Server
  participant API as GraphQL<br/>API
  participant Browser as Browser
  participant Client as Apollo<br/>Client

  Server->>API: クエリ実行
  API->>Server: データ返却
  Server->>Server: cache.extract()
  Server->>Browser: HTML + apolloState
  Browser->>Client: cache.restore(apolloState)
  Client->>Browser: データ表示(即座)

このように、サーバーとクライアント間でキャッシュ状態をシームレスに引き継ぐことができます。

ガベージコレクション(GC)の設定

Apollo Client のキャッシュは、使われなくなったデータを自動的に削除する GC 機能を持っています。適切に設定することで、メモリ使用量を最適化できますね。

typePolicies による GC の制御

InMemoryCache の初期化時に、各型のキャッシュポリシーを設定できます。

typescriptimport { InMemoryCache } from '@apollo/client';

const cache = new InMemoryCache({
  typePolicies: {
    User: {
      // User 型のキーフィールドを指定
      keyFields: ['id'],

      // フィールドごとのポリシー設定
      fields: {
        friends: {
          merge(existing = [], incoming: any[]) {
            return [...existing, ...incoming];
          },
        },
      },
    },
  },
});

evict と gc メソッドの使用

特定のオブジェクトをキャッシュから削除するには evict メソッドを使います。

typescript// 特定のユーザーをキャッシュから削除
cache.evict({
  id: 'User:123', // 削除対象のキャッシュID
});

削除操作の後、孤立したオブジェクトを一括削除するために gc を実行します。

typescript// ガベージコレクションを実行
// 参照されていないオブジェクトをすべて削除
const removedIds = cache.gc();

console.log('削除されたオブジェクト数:', removedIds.length);

自動 GC の設定例

定期的にガベージコレクションを実行する仕組みを作ることもできます。

typescriptimport { useEffect } from 'react';
import { useApolloClient } from '@apollo/client';

function useAutoGC(intervalMs: number = 60000) {
  const client = useApolloClient();

  useEffect(() => {
    // 定期的に GC を実行(デフォルト: 1分ごと)
    const timerId = setInterval(() => {
      const removed = client.cache.gc();

      if (removed.length > 0) {
        console.log(
          `GC実行: ${removed.length}個のオブジェクトを削除`
        );
      }
    }, intervalMs);

    // クリーンアップ
    return () => clearInterval(timerId);
  }, [client, intervalMs]);
}

このカスタムフックをアプリケーションのルートコンポーネントで使用します。

typescriptfunction App() {
  // 1分ごとに自動 GC を実行
  useAutoGC(60000);

  return <div>{/* アプリケーションのコンテンツ */}</div>;
}

possibleTypes の設定

Union 型や Interface を使用している場合、possibleTypes の設定が重要になります。

typescriptconst cache = new InMemoryCache({
  possibleTypes: {
    // SearchResult は Book または Author の可能性がある
    SearchResult: ['Book', 'Author'],

    // Node インターフェースの実装型
    Node: ['User', 'Post', 'Comment'],
  },

  typePolicies: {
    Query: {
      fields: {
        search: {
          // 検索結果のマージポリシー
          merge(existing, incoming) {
            return incoming;
          },
        },
      },
    },
  },
});

この設定により、Apollo Client が Union 型や Interface を正しく識別し、適切にキャッシュできるようになります。

具体例

実践例:ユーザーダッシュボードのキャッシュ初期化

実際のアプリケーションを想定して、ユーザーダッシュボードのキャッシュ初期化を実装してみましょう。

プロジェクトのセットアップ

まず、必要なパッケージをインストールします。

bashyarn add @apollo/client graphql
yarn add -D @types/node

Apollo Client の設定ファイル作成

プロジェクト全体で使用する Apollo Client の設定を作成します。

typescript// lib/apolloClient.ts
import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
  NormalizedCacheObject,
} from '@apollo/client';

let apolloClient: ApolloClient<NormalizedCacheObject> | null =
  null;

// Apollo Client を作成する関数
function createApolloClient(
  initialState: NormalizedCacheObject = {}
) {
  const httpLink = new HttpLink({
    uri:
      process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ||
      'http://localhost:4000/graphql',
    credentials: 'include', // Cookie を含める
  });

  return new ApolloClient({
    ssrMode: typeof window === 'undefined', // サーバーサイドか判定
    link: httpLink,
    cache: new InMemoryCache({
      typePolicies: {
        User: {
          keyFields: ['id'],
        },
        Post: {
          keyFields: ['id'],
        },
      },
    }).restore(initialState),
  });
}

次に、クライアントのシングルトンを管理する関数を追加します。

typescript// Apollo Client のシングルトンを取得・初期化
export function initializeApollo(
  initialState: NormalizedCacheObject = {}
) {
  // サーバーサイドでは毎回新しいインスタンスを作成
  const _apolloClient =
    apolloClient ?? createApolloClient(initialState);

  // クライアントサイドでは既存のキャッシュとマージ
  if (initialState && apolloClient) {
    const existingCache = apolloClient.cache.extract();

    // 既存のキャッシュと新しいデータをマージ
    apolloClient.cache.restore({
      ...existingCache,
      ...initialState,
    });
  }

  // SSR では常に新しいクライアントを返す
  if (typeof window === 'undefined') {
    return _apolloClient;
  }

  // CSR では同じインスタンスを再利用
  if (!apolloClient) {
    apolloClient = _apolloClient;
  }

  return _apolloClient;
}

GraphQL クエリの定義

ダッシュボードで使用するクエリを定義します。

typescript// graphql/queries.ts
import { gql } from '@apollo/client';

// ユーザー情報を取得するクエリ
export const GET_CURRENT_USER = gql`
  query GetCurrentUser {
    currentUser {
      id
      name
      email
      avatar
      stats {
        postsCount
        followersCount
        followingCount
      }
    }
  }
`;

ダッシュボードの投稿一覧を取得するクエリも定義します。

typescript// ユーザーの投稿一覧を取得するクエリ
export const GET_USER_POSTS = gql`
  query GetUserPosts($userId: ID!, $limit: Int = 10) {
    userPosts(userId: $userId, limit: $limit) {
      id
      title
      content
      createdAt
      likes {
        count
      }
      comments {
        count
      }
    }
  }
`;

Next.js ページコンポーネントの実装

SSR を使用したダッシュボードページを実装します。

typescript// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { ApolloProvider } from '@apollo/client';
import { initializeApollo } from '../lib/apolloClient';
import {
  GET_CURRENT_USER,
  GET_USER_POSTS,
} from '../graphql/queries';
import DashboardContent from '../components/DashboardContent';

interface DashboardPageProps {
  apolloState: any;
}

export default function DashboardPage({
  apolloState,
}: DashboardPageProps) {
  // クライアントサイドで Apollo Client を初期化
  const client = initializeApollo(apolloState);

  return (
    <ApolloProvider client={client}>
      <DashboardContent />
    </ApolloProvider>
  );
}

サーバーサイドでデータを取得する処理を実装します。

typescriptexport const getServerSideProps: GetServerSideProps =
  async (context) => {
    const apolloClient = initializeApollo();

    try {
      // 並列でデータを取得
      const [userResult, postsResult] = await Promise.all([
        apolloClient.query({
          query: GET_CURRENT_USER,
        }),
        apolloClient.query({
          query: GET_USER_POSTS,
          variables: {
            userId: '123', // 実際は認証情報から取得
            limit: 10,
          },
        }),
      ]);

      // キャッシュの状態を抽出
      return {
        props: {
          apolloState: apolloClient.cache.extract(),
        },
      };
    } catch (error) {
      console.error('データ取得エラー:', error);

      // エラー時は空の状態を返す
      return {
        props: {
          apolloState: {},
        },
      };
    }
  };

ダッシュボードコンポーネントの実装

実際にデータを表示するコンポーネントを作成します。

typescript// components/DashboardContent.tsx
import { useQuery } from '@apollo/client';
import {
  GET_CURRENT_USER,
  GET_USER_POSTS,
} from '../graphql/queries';

export default function DashboardContent() {
  // キャッシュから即座にデータを取得(SSR で注入済み)
  const { data: userData, loading: userLoading } = useQuery(
    GET_CURRENT_USER
  );

  const { data: postsData, loading: postsLoading } =
    useQuery(GET_USER_POSTS, {
      variables: {
        userId: userData?.currentUser?.id,
        limit: 10,
      },
      skip: !userData?.currentUser?.id, // ユーザーIDがない場合はスキップ
    });

  if (userLoading) {
    return <div>ユーザー情報を読み込み中...</div>;
  }

  return (
    <div className='dashboard'>
      <header className='user-info'>
        <img
          src={userData.currentUser.avatar}
          alt='アバター'
        />
        <h1>{userData.currentUser.name}</h1>
        <p>{userData.currentUser.email}</p>

        <div className='stats'>
          <span>
            投稿: {userData.currentUser.stats.postsCount}
          </span>
          <span>
            フォロワー:{' '}
            {userData.currentUser.stats.followersCount}
          </span>
          <span>
            フォロー中:{' '}
            {userData.currentUser.stats.followingCount}
          </span>
        </div>
      </header>

      <section className='posts'>
        <h2>最近の投稿</h2>
        {postsLoading ? (
          <div>投稿を読み込み中...</div>
        ) : (
          <PostList posts={postsData.userPosts} />
        )}
      </section>
    </div>
  );
}

投稿一覧を表示するサブコンポーネントも作成します。

typescriptinterface Post {
  id: string;
  title: string;
  content: string;
  createdAt: string;
  likes: { count: number };
  comments: { count: number };
}

function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul className='post-list'>
      {posts.map((post) => (
        <li key={post.id} className='post-item'>
          <h3>{post.title}</h3>
          <p>{post.content.substring(0, 100)}...</p>
          <div className='post-meta'>
            <span>いいね: {post.likes.count}</span>
            <span>コメント: {post.comments.count}</span>
            <time>
              {new Date(post.createdAt).toLocaleDateString(
                'ja-JP'
              )}
            </time>
          </div>
        </li>
      ))}
    </ul>
  );
}

キャッシュの手動更新機能

ユーザーが「いいね」ボタンをクリックした際のキャッシュ更新を実装します。

typescript// hooks/useLikePost.ts
import {
  useMutation,
  gql,
  useApolloClient,
} from '@apollo/client';

const LIKE_POST_MUTATION = gql`
  mutation LikePost($postId: ID!) {
    likePost(postId: $postId) {
      id
      likes {
        count
      }
    }
  }
`;

export function useLikePost() {
  const client = useApolloClient();

  const [likePost, { loading }] = useMutation(
    LIKE_POST_MUTATION,
    {
      // 楽観的 UI 更新
      optimisticResponse: (variables) => ({
        likePost: {
          __typename: 'Post',
          id: variables.postId,
          likes: {
            __typename: 'LikeStats',
            count: 0, // 実際の値は update で計算
          },
        },
      }),

      // キャッシュを手動更新
      update(cache, { data }) {
        if (!data?.likePost) return;

        const postId = `Post:${data.likePost.id}`;

        // 既存のデータを読み取る
        const existingPost = cache.readFragment({
          id: postId,
          fragment: gql`
            fragment PostLikes on Post {
              id
              likes {
                count
              }
            }
          `,
        });

        if (existingPost) {
          // いいね数をインクリメント
          cache.writeFragment({
            id: postId,
            fragment: gql`
              fragment PostLikes on Post {
                id
                likes {
                  count
                }
              }
            `,
            data: {
              ...existingPost,
              likes: {
                ...existingPost.likes,
                count: existingPost.likes.count + 1,
              },
            },
          });
        }
      },
    }
  );

  return { likePost, loading };
}

以下の図は、このダッシュボードアプリケーションにおけるキャッシュの流れを示しています。

mermaidflowchart TB
  ssr["SSR<br/>getServerSideProps"] -->|並列クエリ実行| api["GraphQL<br/>API"]
  api -->|ユーザー情報| cache1["Cache<br/>currentUser"]
  api -->|投稿一覧| cache2["Cache<br/>userPosts"]
  cache1 & cache2 -->|extract| state["apolloState<br/>(JSON)"]
  state -->|props| browser["Browser"]
  browser -->|restore| clientCache["Client<br/>Cache"]
  clientCache -->|即座に表示| ui["UI<br/>コンポーネント"]
  ui -->|いいねクリック| mutation["Mutation"]
  mutation -->|update| clientCache

図で理解できる要点

  • SSR でサーバー側のキャッシュを構築し、JSON として抽出
  • クライアント側でキャッシュを復元し、即座に UI に反映
  • Mutation 実行時はキャッシュを直接更新して、UI を同期

LocalStorage を使った永続化の例

キャッシュを LocalStorage に保存し、ページリロード後も復元する実装例です。

typescript// lib/persistCache.ts
import {
  ApolloClient,
  NormalizedCacheObject,
} from '@apollo/client';

const CACHE_KEY = 'apollo-cache-persist';

// キャッシュを LocalStorage に保存
export function saveCache(
  client: ApolloClient<NormalizedCacheObject>
) {
  try {
    const data = client.cache.extract();
    localStorage.setItem(CACHE_KEY, JSON.stringify(data));
    console.log('キャッシュを保存しました');
  } catch (error) {
    console.error('キャッシュ保存エラー:', error);
  }
}

// キャッシュを LocalStorage から復元
export function loadCache(): NormalizedCacheObject | null {
  try {
    const data = localStorage.getItem(CACHE_KEY);
    if (data) {
      console.log('キャッシュを復元しました');
      return JSON.parse(data);
    }
  } catch (error) {
    console.error('キャッシュ復元エラー:', error);
  }
  return null;
}

// 古いキャッシュをクリア
export function clearCache() {
  try {
    localStorage.removeItem(CACHE_KEY);
    console.log('キャッシュをクリアしました');
  } catch (error) {
    console.error('キャッシュクリアエラー:', error);
  }
}

この永続化機能をアプリケーションに統合します。

typescript// pages/_app.tsx
import { useEffect } from 'react';
import { ApolloProvider } from '@apollo/client';
import { initializeApollo } from '../lib/apolloClient';
import { saveCache, loadCache } from '../lib/persistCache';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  // 保存されたキャッシュを読み込む
  const savedCache =
    typeof window !== 'undefined' ? loadCache() : null;

  // Apollo Client を初期化(保存されたキャッシュと SSR の状態をマージ)
  const client = initializeApollo({
    ...savedCache,
    ...pageProps.apolloState,
  });

  useEffect(() => {
    // ページ遷移時にキャッシュを保存
    const handleBeforeUnload = () => {
      saveCache(client);
    };

    window.addEventListener(
      'beforeunload',
      handleBeforeUnload
    );

    // 定期的にキャッシュを保存(5分ごと)
    const intervalId = setInterval(() => {
      saveCache(client);
    }, 5 * 60 * 1000);

    return () => {
      window.removeEventListener(
        'beforeunload',
        handleBeforeUnload
      );
      clearInterval(intervalId);
    };
  }, [client]);

  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;

ガベージコレクションの実装例

メモリ管理を最適化するための GC 実装です。

typescript// hooks/useCacheManagement.ts
import { useEffect, useCallback } from 'react';
import { useApolloClient } from '@apollo/client';

interface CacheManagementOptions {
  gcInterval?: number; // GC 実行間隔(ミリ秒)
  maxCacheSize?: number; // 最大キャッシュサイズ(バイト)
  enableLogging?: boolean; // ログ出力の有効化
}

export function useCacheManagement(
  options: CacheManagementOptions = {}
) {
  const {
    gcInterval = 60000, // デフォルト: 1分
    maxCacheSize = 5 * 1024 * 1024, // デフォルト: 5MB
    enableLogging = false,
  } = options;

  const client = useApolloClient();

  // キャッシュサイズを計算
  const getCacheSize = useCallback(() => {
    const cacheData = client.cache.extract();
    const size = new Blob([JSON.stringify(cacheData)]).size;
    return size;
  }, [client]);

  // ガベージコレクションを実行
  const runGC = useCallback(() => {
    const beforeSize = getCacheSize();
    const removed = client.cache.gc();
    const afterSize = getCacheSize();

    if (enableLogging) {
      console.log('GC実行結果:', {
        removedCount: removed.length,
        beforeSize: `${(beforeSize / 1024).toFixed(2)} KB`,
        afterSize: `${(afterSize / 1024).toFixed(2)} KB`,
        freed: `${((beforeSize - afterSize) / 1024).toFixed(
          2
        )} KB`,
      });
    }

    return removed;
  }, [client, getCacheSize, enableLogging]);

  // キャッシュサイズをチェックして必要なら GC
  const checkAndCleanCache = useCallback(() => {
    const currentSize = getCacheSize();

    if (currentSize > maxCacheSize) {
      if (enableLogging) {
        console.warn(
          `キャッシュサイズが上限を超過: ${(
            currentSize /
            1024 /
            1024
          ).toFixed(2)} MB`
        );
      }
      runGC();
    }
  }, [getCacheSize, maxCacheSize, runGC, enableLogging]);

  useEffect(() => {
    // 定期的に GC を実行
    const gcTimerId = setInterval(() => {
      runGC();
    }, gcInterval);

    // 定期的にキャッシュサイズをチェック
    const checkTimerId = setInterval(() => {
      checkAndCleanCache();
    }, gcInterval / 2);

    return () => {
      clearInterval(gcTimerId);
      clearInterval(checkTimerId);
    };
  }, [gcInterval, runGC, checkAndCleanCache]);

  return {
    getCacheSize,
    runGC,
    checkAndCleanCache,
  };
}

このカスタムフックをアプリケーションで使用します。

typescript// components/Layout.tsx
import { useCacheManagement } from '../hooks/useCacheManagement';

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  // キャッシュ管理を有効化
  const { getCacheSize, runGC } = useCacheManagement({
    gcInterval: 60000, // 1分ごと
    maxCacheSize: 5 * 1024 * 1024, // 5MB
    enableLogging: process.env.NODE_ENV === 'development',
  });

  return (
    <div className='layout'>
      <header>
        <nav>{/* ナビゲーション */}</nav>
        {process.env.NODE_ENV === 'development' && (
          <div className='debug-panel'>
            <button
              onClick={() => {
                const size = getCacheSize();
                alert(
                  `現在のキャッシュサイズ: ${(
                    size / 1024
                  ).toFixed(2)} KB`
                );
              }}
            >
              キャッシュサイズ確認
            </button>
            <button
              onClick={() => {
                const removed = runGC();
                alert(
                  `${removed.length}個のオブジェクトを削除しました`
                );
              }}
            >
              手動 GC 実行
            </button>
          </div>
        )}
      </header>
      <main>{children}</main>
    </div>
  );
}

まとめ

Apollo Client のキャッシュ初期化戦略について、実践的な内容を解説してきました。

既存データの注入では、writeQuerywriteFragment を使ってキャッシュにデータを書き込む方法を学びました。これにより、アプリケーションの起動時やテスト時に必要なデータを事前に準備できます。

SSR における rehydration では、Next.js のようなフレームワークでサーバーサイドのキャッシュをクライアントに引き継ぐ方法を実装しました。cache.extract()cache.restore() を組み合わせることで、シームレスなデータ受け渡しが実現できますね。

ガベージコレクション設定では、evictgc メソッドを活用したメモリ管理の最適化手法を紹介しました。定期的な GC 実行やキャッシュサイズの監視により、アプリケーションのパフォーマンスを維持できます。

これらのテクニックを適切に組み合わせることで、以下のような効果が得られます。

#項目効果
1初回表示速度SSR と rehydration により、初回レンダリング時からデータを表示
2ネットワーク削減キャッシュの活用により、不要な API リクエストを削減
3オフライン対応LocalStorage との組み合わせで、オフライン時もデータ表示が可能
4メモリ効率GC による適切なメモリ管理で、長時間の使用でも安定動作
5開発効率テスト時のモックデータ注入が容易になり、開発速度が向上

Apollo Client のキャッシュ機能を最大限に活用することで、ユーザー体験の向上とアプリケーションのパフォーマンス最適化を同時に実現できるのです。

関連リンク