2026年7月26日

2026年7月26日

WordPressのTransientキャッシュを使った高速化テクニック

はじめに

WordPressのTransient APIは、データベースへの繰り返しクエリを削減するシンプルなキャッシュ機構です。重いWP_QueryやAPI呼び出しの結果をキャッシュすることで、ページロード時間を大幅に改善できます。さらに、Redis/Memcachedを使ったObject Cacheと組み合わせることで、複数サーバー環境でも一貫したキャッシュが実現できます。本記事では、クラスベースのキャッシュ設計から本番環境対応のRedis連携まで解説します。

症状・原因

キャッシュが必要になる典型的なパフォーマンス問題:

  • 複雑なWP_Queryが毎リクエストで実行され、MySQLがボトルネックになっている
  • 外部API(天気情報・為替レートなど)への呼び出しがレスポンスを遅延させている
  • 集計クエリ(SUM/COUNT/GROUP BY)がページロードのたびに実行されている
  • 大量の投稿を処理するアーカイブページで "504 Gateway Timeout" が発生する
  • New Relicや Query Monitor でスロークエリが多数検出されている

解決手順

ステップ1:Transient APIの基本操作

set_transientget_transientdelete_transientの基本的な使い方を押さえます。

<?php
/**
 * 基本的なTransient APIの使い方
 */

// キャッシュを取得(存在しない場合は false を返す)
$featured_posts = get_transient('my_featured_posts');

if ($featured_posts === false) {
    // キャッシュが存在しない → DBからデータを取得
    $query = new WP_Query([
        'post_type'      => 'post',
        'posts_per_page' => 10,
        'meta_key'       => '_is_featured',
        'meta_value'     => '1',
        'orderby'        => 'date',
        'order'          => 'DESC',
    ]);

    $featured_posts = $query->posts;
    wp_reset_postdata();

    // 1時間(3600秒)キャッシュに保存
    set_transient('my_featured_posts', $featured_posts, HOUR_IN_SECONDS);
}

// キャッシュを手動で削除(投稿が更新されたとき)
function my_clear_featured_posts_cache(int $post_id): void {
    if (get_post_type($post_id) !== 'post') {
        return;
    }
    delete_transient('my_featured_posts');
}
add_action('save_post', 'my_clear_featured_posts_cache');
add_action('deleted_post', 'my_clear_featured_posts_cache');

// 時間定数の参照
// MINUTE_IN_SECONDS = 60
// HOUR_IN_SECONDS   = 3600
// DAY_IN_SECONDS    = 86400
// WEEK_IN_SECONDS   = 604800
// MONTH_IN_SECONDS  = 2592000 (30日)
// YEAR_IN_SECONDS   = 31536000

ステップ2:バージョンキーを使ったグループ無効化

複数のキャッシュを一括無効化するバージョンキーパターンを実装します。

<?php
/**
 * バージョンキーを使ったキャッシュグループの管理
 * カテゴリーが更新されたとき、そのカテゴリー関連の全キャッシュを一括無効化する
 */
class My_Cache_Manager {

    private const CACHE_PREFIX  = 'my_plugin_';
    private const VERSION_KEY   = 'my_plugin_cache_version';
    private const DEFAULT_TTL   = DAY_IN_SECONDS;

    /**
     * 現在のキャッシュバージョンを取得する
     */
    private static function get_version(): string {
        $version = get_transient(self::VERSION_KEY);

        if ($version === false) {
            $version = (string) time();
            set_transient(self::VERSION_KEY, $version, YEAR_IN_SECONDS);
        }

        return $version;
    }

    /**
     * バージョン付きキャッシュキーを生成する
     *
     * @param string $key ベースキー
     * @return string バージョン付きキャッシュキー
     */
    public static function make_key(string $key): string {
        $version = self::get_version();
        // MD5で短縮(WordPressのoptionsテーブルはキー長制限あり: 191文字)
        return self::CACHE_PREFIX . md5($version . '_' . $key);
    }

    /**
     * キャッシュを取得する
     *
     * @param string $key キャッシュキー
     * @return mixed キャッシュされた値、または false
     */
    public static function get(string $key): mixed {
        return get_transient(self::make_key($key));
    }

    /**
     * キャッシュを保存する
     *
     * @param string $key   キャッシュキー
     * @param mixed  $value 保存する値
     * @param int    $ttl   有効期限(秒)
     */
    public static function set(string $key, mixed $value, int $ttl = self::DEFAULT_TTL): bool {
        return set_transient(self::make_key($key), $value, $ttl);
    }

    /**
     * グループ全体を一括無効化する(バージョンを更新するだけで全キーが無効化される)
     */
    public static function flush_group(): void {
        $new_version = (string) time();
        set_transient(self::VERSION_KEY, $new_version, YEAR_IN_SECONDS);
    }
}

// カテゴリー更新時に全グループキャッシュを無効化
function my_flush_cache_on_term_change(int $term_id): void {
    My_Cache_Manager::flush_group();
}
add_action('edited_term', 'my_flush_cache_on_term_change');
add_action('delete_term', 'my_flush_cache_on_term_change');

ステップ3:リクエスト内Object Cacheで二重クエリを防ぐ

wp_cache_get/setを使ったリクエストスコープのキャッシュを実装します。

<?php
/**
 * wp_cache_get/set を使ったリクエスト内キャッシュ
 * 同一リクエスト内で同じデータを複数箇所で参照する場合に有効
 */
function my_get_user_permissions(int $user_id): array {
    $cache_key   = 'user_permissions_' . $user_id;
    $cache_group = 'my_plugin_permissions';

    // Object Cache から取得(同一リクエスト内ではメモリから返る)
    $permissions = wp_cache_get($cache_key, $cache_group);

    if ($permissions !== false) {
        return $permissions;
    }

    // データベースから取得(重いクエリ)
    global $wpdb;
    $permissions = $wpdb->get_results($wpdb->prepare(
        "SELECT pm.meta_key, pm.meta_value
         FROM {$wpdb->usermeta} pm
         WHERE pm.user_id = %d
         AND pm.meta_key LIKE 'my_plugin_perm_%'",
        $user_id
    ), ARRAY_A);

    // Object Cache に保存(TTLなし = リクエスト終了まで)
    // Redis/Memcached が有効な場合は永続化される
    wp_cache_set($cache_key, $permissions, $cache_group, HOUR_IN_SECONDS);

    return $permissions ?: [];
}

/**
 * WP_Query 結果をTransientとObject Cacheの両方でキャッシュするラッパー
 *
 * @param string $cache_key  キャッシュキー
 * @param array  $query_args WP_Queryの引数
 * @param int    $ttl        Transientの有効期限
 * @return WP_Post[] 投稿配列
 */
function my_cached_query(string $cache_key, array $query_args, int $ttl = HOUR_IN_SECONDS): array {
    // 1. Object Cache を確認(最速)
    $posts = wp_cache_get($cache_key, 'my_plugin_queries');
    if ($posts !== false) {
        return $posts;
    }

    // 2. Transient を確認(DBから読むが軽量)
    $posts = get_transient($cache_key);
    if ($posts !== false) {
        wp_cache_set($cache_key, $posts, 'my_plugin_queries');
        return $posts;
    }

    // 3. 実際のWP_Queryを実行(最も重い)
    $query = new WP_Query(array_merge(['no_found_rows' => true], $query_args));
    $posts = $query->posts;
    wp_reset_postdata();

    // 両方のキャッシュに保存
    set_transient($cache_key, $posts, $ttl);
    wp_cache_set($cache_key, $posts, 'my_plugin_queries', $ttl);

    return $posts;
}

ステップ4:期限切れTransientのクリーンアップ

DBに溜まった期限切れTransientを定期削除する処理を実装します。

<?php
/**
 * 期限切れTransientをデータベースから削除する
 * WP-Cronと組み合わせて定期実行することを推奨
 */
function my_cleanup_expired_transients(): int {
    global $wpdb;

    // Object Cache (Redis/Memcached) を使用している場合はoptionsテーブルに書き込まれないため不要
    if (wp_using_ext_object_cache()) {
        return 0;
    }

    // 期限切れのtransient_timeoutを取得して関連データを削除
    $expired_timeouts = $wpdb->get_col(
        "SELECT option_name FROM {$wpdb->options}
         WHERE option_name LIKE '_transient_timeout_%'
         AND option_value < UNIX_TIMESTAMP(NOW())
         LIMIT 500"
    );

    $deleted = 0;

    foreach ($expired_timeouts as $timeout_key) {
        // _transient_timeout_xxx → _transient_xxx
        $transient_key = str_replace('_transient_timeout_', '_transient_', $timeout_key);

        $wpdb->delete($wpdb->options, ['option_name' => $timeout_key]);
        $wpdb->delete($wpdb->options, ['option_name' => $transient_key]);
        $deleted++;
    }

    // optionsテーブルのオートロード最適化
    if ($deleted > 0) {
        $wpdb->query("OPTIMIZE TABLE {$wpdb->options}");
    }

    error_log("[my_plugin] 期限切れTransient削除: {$deleted}件");

    return $deleted;
}

// WP-Cron に登録して毎日実行
add_action('my_plugin_cleanup_transients', 'my_cleanup_expired_transients');

if (!wp_next_scheduled('my_plugin_cleanup_transients')) {
    wp_schedule_event(time(), 'daily', 'my_plugin_cleanup_transients');
}

ステップ5:Redisを使った永続Objectキャッシュの設定

Redis Object Cacheプラグインの設定とカスタムキャッシュの実装例を示します。

<?php
// wp-config.php に追加(Redis Object Cache プラグインと組み合わせる)
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_PREFIX', 'mysite_'); // マルチサイトで重複を防ぐ
define('WP_REDIS_MAXTTL', 86400);     // 最大TTL: 24時間

// Redis が有効化された後のキャッシュ使用例
function my_get_expensive_data(string $query_type): array {
    $cache_key   = 'expensive_' . $query_type . '_' . date('Ymd');
    $cache_group = 'my_plugin';

    // Redis が有効なら wp_cache_get は Redis から読む(超高速)
    $data = wp_cache_get($cache_key, $cache_group);

    if ($data !== false) {
        return $data;
    }

    // 重いデータ処理(外部API呼び出しなど)
    $response = wp_remote_get('https://api.example.com/data?type=' . urlencode($query_type), [
        'timeout' => 10,
        'headers' => ['Accept' => 'application/json']
    ]);

    if (is_wp_error($response)) {
        // エラー時は空配列をキャッシュしてリトライストームを防ぐ
        wp_cache_set($cache_key, [], $cache_group, 5 * MINUTE_IN_SECONDS);
        return [];
    }

    $data = json_decode(wp_remote_retrieve_body($response), true) ?? [];

    // Redis に15分間キャッシュ(TTLはRedisが管理するので自動削除される)
    wp_cache_set($cache_key, $data, $cache_group, 15 * MINUTE_IN_SECONDS);

    return $data;
}
# Redis Object Cache プラグインのインストール(WP-CLI)
wp plugin install redis-cache --activate

# Redis 接続テスト
wp redis status

# キャッシュのフラッシュ
wp cache flush

# Transientの一覧確認
wp transient list --format=table

# 期限切れTransientの手動削除
wp transient delete --expired

注意事項

  • get_transient() が返す false は「キャッシュなし」を意味するため、false 自体をキャッシュしたい場合は値をラップする(例: ['value' => false]
  • キャッシュキーの最大長はWordPressの制限により172文字のため、長いキーは md5() で短縮すること
  • Transientはマルチサイトで get_site_transient() / set_site_transient() を使わないと各サイトのDBに個別保存される
  • Redis/Memcachedが有効な場合、Transient APIは自動的にObject Cacheに委譲されるためoptionsテーブルを汚染しない
  • キャッシュのパージ(無効化)ロジックを設計する際は、キャッシュ依存関係を整理してから実装すること(依存関係が複雑になると整合性の保証が困難)

まとめ

WordPressのTransient APIは、set_transient/get_transientというシンプルなAPIで強力なキャッシュ機構を提供します。バージョンキーによるグループ無効化パターンを使うことで複雑なキャッシュ管理も安全に実装でき、Redisと組み合わせることで本格的な高速化が実現できます。定期的なキャッシュクリーンアップにはWP-Cronのスケジュール処理を組み合わせることで、データベースの肥大化を防げます。

お気軽にご相談ください

お見積りへ お問い合わせへ