WordPressのパフォーマンス改善策のひとつに、永続オブジェクトキャッシュ(APCu)があります。この記事では、APCuを使ったオブジェクトキャッシュの仕組みと、実際の設定手順をコード付きで解説します。


    永続オブジェクトキャッシュとは

    WordPressは標準でもオブジェクトキャッシュを持っていますが、これはリクエストをまたいで保持されない非永続キャッシュです。毎回のリクエストでデータベースに同じクエリを発行することになります。

    永続オブジェクトキャッシュを使うと、データベースのクエリ結果や計算結果をサーバーのメモリに保持し、次回以降のリクエストでは再クエリなしにデータを取得できます。結果として、データベース負荷の軽減とサイトの応答速度向上が期待できます。

    APCu(Alternative PHP Cache user)はPHPの拡張モジュールのひとつで、多くのレンタルサーバーで標準搭載されています。


    設定前の確認:APCuが使えるか調べる

    管理画面の ダッシュボード → サイトヘルス を確認してください。以下のような表示があればAPCuが利用できます。

    「お使いのホスティングサービスでは、次のオブジェクトキャッシュサービスをサポートしているようです: APCu, Redis。」

    また、「永続オブジェクトキャッシュを使用中です」 と表示されていれば、すでに有効化されています。


    設定方法

    1. object-cache.php を wp-content に配置する

    WordPressのオブジェクトキャッシュは、wp-content/object-cache.php を置くことで上書きできます。以下のコードを object-cache.php として保存し、wp-content/ 直下に配置してください。

    <?php
    /**
     * WPのオブジェクトキャッシュをAPCuで使用する
     * (wp-content内のobject-cache.phpとして配置する)
     */
    
    // 直接のアクセスを防ぐ
    if (!defined('ABSPATH')) {
      exit;
    }
    
    // APCuが利用できない場合は終了
    if (!function_exists('apcu_fetch')) {
      return;
    }
    
    if (function_exists('wp_cache_add')) {
      die('<strong>ERROR:</strong> This is <em>not</em> a plugin, and it should not be activated as one.<br /><br />Instead, <code>' . str_replace($_SERVER['DOCUMENT_ROOT'], '', __FILE__) . '</code> must be moved to <code>' . str_replace($_SERVER['DOCUMENT_ROOT'], '', trailingslashit(WP_CONTENT_DIR)) . 'object-cache.php</code>');
    } else {
      // キャッシュ関数 wp_cache_*() をAPCu版に置き換える
      function wp_cache_add($key, $data, $group = '', $expire = 0) {
        global $wp_object_cache;
        return $wp_object_cache->add($key, $data, $group, (int) $expire);
      }
      function wp_cache_close() {
        return true;
      }
      function wp_cache_decr($key, $offset = 1, $group = '') {
        global $wp_object_cache;
        return $wp_object_cache->decr($key, $offset, $group);
      }
      function wp_cache_delete($key, $group = '') {
        global $wp_object_cache;
        return $wp_object_cache->delete($key, $group);
      }
      function wp_cache_flush() {
        global $wp_object_cache;
        return $wp_object_cache->flush();
      }
      function wp_cache_get($key, $group = '', $force = false, &$found = null) {
        global $wp_object_cache;
        return $wp_object_cache->get($key, $group, $force, $found);
      }
      function wp_cache_incr($key, $offset = 1, $group = '') {
        global $wp_object_cache;
        return $wp_object_cache->incr($key, $offset, $group);
      }
      function wp_cache_init() {
        if (function_exists('apcu_fetch')) {
          $GLOBALS['wp_object_cache'] = new APCu_Object_Cache();
        }
      }
      function wp_cache_replace($key, $data, $group = '', $expire = 0) {
        global $wp_object_cache;
        return $wp_object_cache->replace($key, $data, $group, (int) $expire);
      }
      function wp_cache_set($key, $data, $group = '', $expire = 0) {
        global $wp_object_cache;
        return $wp_object_cache->set($key, $data, $group, (int) $expire);
      }
      function wp_cache_switch_to_blog($blog_id) {
        global $wp_object_cache;
        $wp_object_cache->switch_to_blog($blog_id);
      }
      function wp_cache_add_global_groups($groups) {
        global $wp_object_cache;
        $wp_object_cache->add_global_groups($groups);
      }
      function wp_cache_add_non_persistent_groups($groups) {
        global $wp_object_cache;
        $wp_object_cache->wp_cache_add_non_persistent_groups($groups);
      }
      function wp_cache_reset() {
        global $wp_object_cache;
        $wp_object_cache->reset();
      }
    
      class APCu_Object_Cache {
        private $prefix = '';
        private $local_cache = array();
        private $global_groups = array();
        private $non_persistent_groups = array();
        private $multisite = false;
        private $blog_prefix = '';
    
        public function __construct() {
          global $table_prefix, $blog_id;
          $this->multisite = is_multisite();
          $this->blog_prefix = $this->multisite ? intval($blog_id) : '';
          $this->prefix = DB_HOST . '.' . DB_NAME . '.' . $table_prefix;
        }
    
        private function get_group($group) {
          return empty($group) ? 'default' : $group;
        }
    
        private function get_key($group, $key) {
          if ($this->multisite && !isset($this->global_groups[$group])) {
            return $this->prefix . '.' . $group . '.' . $this->blog_prefix . ':' . $key;
          } else {
            return $this->prefix . '.' . $group . '.' . $key;
          }
        }
    
        public function add($key, $data, $group = 'default', $expire = 0) {
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          if (function_exists('wp_suspend_cache_addition') && wp_suspend_cache_addition()) {
            return false;
          }
          if (isset($this->local_cache[$group][$key])) {
            return false;
          }
          if (!isset($this->non_persistent_groups[$group]) && apcu_exists($key)) {
            return false;
          }
          if (is_object($data)) {
            $this->local_cache[$group][$key] = clone $data;
          } else {
            $this->local_cache[$group][$key] = $data;
          }
          if (!isset($this->non_persistent_groups[$group])) {
            return apcu_add($key, $data, (int) $expire);
          }
          return true;
        }
    
        public function add_global_groups($groups) {
          if (is_array($groups)) {
            foreach ($groups as $group) {
              $this->global_groups[$group] = true;
            }
          } else {
            $this->global_groups[$groups] = true;
          }
        }
    
        public function wp_cache_add_non_persistent_groups($groups) {
          if (is_array($groups)) {
            foreach ($groups as $group) {
              $this->non_persistent_groups[$group] = true;
            }
          } else {
            $this->non_persistent_groups[$groups] = true;
          }
        }
    
        public function decr($key, $offset = 1, $group = 'default') {
          if ($offset < 0) {
            return $this->incr($key, abs($offset), $group);
          }
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          if (isset($this->local_cache[$group][$key]) && $this->local_cache[$group][$key] - $offset >= 0) {
            $this->local_cache[$group][$key] -= $offset;
          } else {
            $this->local_cache[$group][$key] = 0;
          }
          if (isset($this->non_persistent_groups[$group])) {
            return $this->local_cache[$group][$key];
          } else {
            $value = apcu_dec($key, $offset);
            if ($value < 0) {
              apcu_store($key, 0);
              return 0;
            }
            return $value;
          }
        }
    
        public function delete($key, $group = 'default', $force = false) {
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          unset($this->local_cache[$group][$key]);
          if (!isset($this->non_persistent_groups[$group])) {
            return apcu_delete($key);
          }
          return true;
        }
    
        public function flush() {
          $this->local_cache = array();
          apcu_clear_cache();
          return true;
        }
    
        public function get($key, $group = 'default', $force = false, &$found = null) {
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          if (!$force && isset($this->local_cache[$group][$key])) {
            $found = true;
            if (is_object($this->local_cache[$group][$key])) {
              return clone $this->local_cache[$group][$key];
            } else {
              return $this->local_cache[$group][$key];
            }
          } elseif (isset($this->non_persistent_groups[$group])) {
            $found = false;
            return false;
          } else {
            $value = apcu_fetch($key, $found);
            if ($found) {
              if ($force) {
                $this->local_cache[$group][$key] = $value;
              }
              return $value;
            } else {
              return false;
            }
          }
        }
    
        public function incr($key, $offset = 1, $group = 'default') {
          if ($offset < 0) {
            return $this->decr($key, abs($offset), $group);
          }
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          if (isset($this->local_cache[$group][$key]) && $this->local_cache[$group][$key] + $offset >= 0) {
            $this->local_cache[$group][$key] += $offset;
          } else {
            $this->local_cache[$group][$key] = 0;
          }
          if (isset($this->non_persistent_groups[$group])) {
            return $this->local_cache[$group][$key];
          } else {
            $value = apcu_inc($key, $offset);
            if ($value < 0) {
              apcu_store($key, 0);
              return 0;
            }
            return $value;
          }
        }
    
        public function replace($key, $data, $group = 'default', $expire = 0) {
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          if (isset($this->non_persistent_groups[$group])) {
            if (!isset($this->local_cache[$group][$key])) {
              return false;
            }
          } else {
            if (!isset($this->local_cache[$group][$key]) && !apcu_exists($key)) {
              return false;
            }
            apcu_store($key, $data, (int) $expire);
          }
          if (is_object($data)) {
            $this->local_cache[$group][$key] = clone $data;
          } else {
            $this->local_cache[$group][$key] = $data;
          }
          return true;
        }
    
        public function reset() {
          $this->flush();
        }
    
        public function set($key, $data, $group = 'default', $expire = 0) {
          $group = $this->get_group($group);
          $key = $this->get_key($group, $key);
          if (is_object($data)) {
            $this->local_cache[$group][$key] = clone $data;
          } else {
            $this->local_cache[$group][$key] = $data;
          }
          if (!isset($this->non_persistent_groups[$group])) {
            return apcu_store($key, $data, (int) $expire);
          }
          return true;
        }
    
        public function stats() {
          echo '';
        }
    
        public function switch_to_blog($blog_id) {
          $this->blog_prefix = $this->multisite ? intval($blog_id) : '';
        }
      }
    }
    

    2. functions.php にキャッシュクリアの設定を追加する

    記事の投稿・更新やメニュー変更のたびにキャッシュをクリアする処理を functions.php に追加します。過度なクリアを防ぐためにインターバルを設けています。

    /* 永続オブジェクトキャッシュ(APCu)
    -------------------------------------- */
    // 最後のクリアから一定時間経過後にクリアするように(過度のクリア防止)
    define('CACHE_CLEAR_INTERVAL', 5); // 5秒
    
    function should_clear_cache() {
      $last_cleared = get_option('last_cache_cleared_time', 0);
      return time() - $last_cleared > CACHE_CLEAR_INTERVAL;
    }
    
    // APCuキャッシュクリア関数
    function clear_apcu_cache() {
      if (function_exists('apcu_clear_cache')) {
        apcu_clear_cache();
      } elseif (function_exists('apc_clear_cache')) {
        apc_clear_cache();
      }
      update_option('last_cache_cleared_time', time());
    }
    
    // 記事投稿・更新時のキャッシュクリア
    function my_save_post($post_id, $post, $update) {
      if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
        return;
      }
      if (should_clear_cache()) {
        clear_apcu_cache();
      }
    }
    add_action('save_post', 'my_save_post', 10, 3);
    
    // メニュー更新時のキャッシュクリア
    function clear_cache_on_menu_update($menu_id, $menu_data) {
      if (should_clear_cache()) {
        clear_apcu_cache();
      }
    }
    add_action('wp_update_nav_menu', 'clear_cache_on_menu_update', 10, 2);
    
    // カスタマイザ更新時のキャッシュクリア
    function clear_cache_on_customize_save($wp_customize) {
      if (should_clear_cache()) {
        clear_apcu_cache();
      }
    }
    add_action('customize_save_after', 'clear_cache_on_customize_save');
    

    有効化の確認

    設定後、管理画面の ダッシュボード → サイトヘルス を開き、「永続オブジェクトキャッシュを使用中です」と表示されていれば有効化完了です。

    Query Monitor プラグインを使うと、Object Cache の欄に「88% hit rate」のようなヒット率が表示され、キャッシュが効いていることを確認できます。


    まとめ

    永続オブジェクトキャッシュ(APCu)の設定は、object-cache.php の配置と functions.php へのクリア処理の追加だけで完了します。データベースへのアクセスが減り、ページ生成時間の短縮が期待できます。

    サイトヘルスで「APCu」または「Redis」がサポートされていると表示されているサーバーであれば、今日からでも試せる改善策です。