白い紙粘土でできた、蓋を閉じた小箱と、その横に立てかけられた文字盤のない丸い時計

「またこのページか……」

管理している WordPress サイトに、表示のたびに一呼吸待たされるページがあります。

WordPress は、同じ仕事を毎回やり直す

原因は、外部 API からのデータ取得がリクエストのたびに走っていることでした。WordPress は律儀で、同じページを開かれるたびに、同じ処理を最初からやり直します。結果が1時間変わらない API 問い合わせでも、毎回実行される。表示時間は、そのぶん延びていきます。

結果を取っておいて、期限まで使い回す

重い処理の結果に有効期限をつけて保存し、期限内は使い回します。基本形はこれだけです。

$events = get_transient( 'upcoming_events' );

if ( false === $events ) {
	// キャッシュがないときだけ、重い処理を実行する
	$events = fetch_events_from_api();
	set_transient( 'upcoming_events', $events, HOUR_IN_SECONDS );
}

get_transient() で保存済みのデータを探し、なければ(false が返ってきたら)重い処理を実行して、set_transient() で結果を保存しておく。次にページが開かれたときは、保存済みのデータを返すだけです。API への問い合わせは走りません。

transient は英語で「つかのま」という意味です。有効期限は秒で渡し、期限が切れると取得できなくなる。上の例の HOUR_IN_SECONDS は1時間で、DAY_IN_SECONDSWEEK_IN_SECONDS といった定数も用意されています。これで WordPress は、1時間に一度だけ働けば済むようになります。

向くデータと、向かないデータ

効くのは「誰が見ても同じで、多少古くても困らない」データです。

  • 外部 API のレスポンス(イベント情報、天気、SNS の埋め込みなど)
  • 重い WP_Query の結果(人気記事ランキング、関連記事の一覧など)
  • 集計に時間がかかる値(投稿数、レビューの平均点など)

逆に、ユーザーごとに内容が変わるものや、常に最新でなければ困るもの(在庫数、価格、決済まわり)には使いません。

この線引きは技術の問題ではありません。古い値を見せてしまったときに、誰がどう困るか。それを決めるのは業務の側です。判断がつかないうちはキャッシュしない、で構いません。遅いページより、間違った値のほうが高くつきます。

期限を待つのではなく、更新の瞬間に消す

キャッシュの悩みは、いつまで信じてよいかです。イベント情報のように日々変わるものなら1時間。めったに変わらないものなら1日でも1週間でもかまいません。

ただし期限を延ばすほど速くなり、延ばすほど「たったいま更新したのに、表示が古いままだ」という声が近づいてきます。この二つは、期限の長さをどう選んでも同時には満たせません。

抜け道があります。データが変わった瞬間にキャッシュを消してしまえばいい。

add_action( 'save_post_event', function () {
	delete_transient( 'upcoming_events' );
} );

save_post_event は、投稿タイプ event の保存時に走るフックです(save_post_{投稿タイプ} 形式)。イベントが保存されたら、古いキャッシュを消す。次にページが開かれたときに重い処理がもう一度だけ走り、新しい結果が保存し直されます。

こうすると、有効期限の役割が変わります。鮮度を保つ主役は更新フックのほうで、期限は「フックを書き忘れた経路があったときの保険」になる。だから期限は長めに取っておいて構いません。表示が古いままになる時間は、更新の瞬間にゼロへ戻ります。

エラーにならない落とし穴

Transients でつまずくとき、たいてい画面にエラーは出ません。動いているように見えて、期待した結果になっていない。次の四つは、どれも気づきにくい部類です。

有効期限に 0 を渡さない

第3引数を省略したときも 0 と同じ扱いです。0 は無期限の意味になりますが、このとき WordPress はそのデータを autoload、つまりどのページの表示でも毎回読み込むようになります。速くするつもりで置いたデータが、すべてのページを少しずつ遅くしていきます。期限は必ず入れてください。

期限切れのデータは、すぐには消えない

期限が切れたデータは、次に get_transient() で取りに来たときに期限切れと判定され、そこで初めて削除されます。WordPress 4.9 以降は1日1回の cron(delete_expired_transients())でも掃除されますが、cron はサイトへの訪問がないと動きません。期限が切れた瞬間に消えるわけではない、と覚えておいてください。

キー名は 172 文字まで

WordPress は保存時に _transient__transient_timeout_ の接頭辞を付けて wp_options に書き込みます。option_name 列が varchar(191) なので、長いほうの接頭辞 19 文字を引いた 172 文字が上限です。超えると片方の名前だけが切り詰められ、対になるはずのデータが噛み合わなくなります。エラーも警告も出ません。URL のような長い文字列からキーを作るときは、'events_' . md5( $url ) のように畳んでおくと安全です。

保存先を当てにしない

普段は wp_options テーブルに保存されますが、Redis や Memcached といったオブジェクトキャッシュが有効なサイトでは、自動的にそちらへ保存されます。コードは一行も変えなくてよい、という設計です。裏を返せば、Transients を「必ずそこにあるもの」として当てにしてはいけません。オブジェクトキャッシュはサーバーの再起動で飛びますし、期限より早く追い出されることもあります。

つまり Transients が保証しているのは「速いこと」だけで、「あること」ではありません。false が返ってきたら作り直す。あの if 文が、いつでも本体です。