mcpField
W
wp-performance-review

wp-performance-review

github.com/jorgerosalテスト・品質11370 回インストール21 週間前に更新
リポジトリ

wp-performance-review について

wp-performance-review は、WordPress のテーマ、プラグイン、カスタムコードのパフォーマンスレビューを支援します。低速なページやタイムアウト、データベース負荷の調査に向け、問題を重大度と行番号付きで報告します。

主な機能

  1. 01メモリ枯渇と無制限クエリを優先確認
  2. 02データベース、キャッシュ、外部リクエストの確認
  3. 03ファイル種別に応じた PHP と JavaScript の検査
  4. 04重大度と行番号付きの問題報告
  5. 05rg による一般的なアンチパターン検索

活用シーン

  1. 01WordPress コード変更のレビュー
  2. 02ページ遅延やリクエストタイムアウトの調査
  3. 03高トラフィックイベント前の性能リスク確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jorgerosal/wordpress-skills --skill wp-performance-review

インストール後、Claude Code で/wp-performance-reviewを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
anti-patterns.md
caching-guide.md
measurement-guide.md
wp-query-guide.md

詳細

WordPress パフォーマンスレビューのスキル

概要

WordPress のテーマ、プラグイン、カスタムコードを体系的にパフォーマンスレビューします。基本原則: まず重大な問題(OOM、上限のないクエリ、キャッシュの回避)を確認し、次に警告、最後に最適化を確認します。行番号と重要度を付けて報告します。

使用するタイミング

次の場合に使用します:

  • WordPress のテーマまたはプラグインの PR/コードをレビューするとき
  • ユーザーからページの読み込みが遅い、タイムアウトが発生する、または 500 エラーが出ると報告されたとき
  • トラフィックの多いイベント(ローンチ、セール、話題の急増)の前に監査するとき
  • WP_Query またはデータベース操作を最適化するとき
  • メモリ不足または DB ロックを調査するとき

次の場合には使用しないでください:

  • セキュリティのみを対象とする監査(利用可能な場合は wp-security-review を使用してください)
  • Gutenberg ブロックの開発パターン(利用可能な場合は wp-block-development を使用してください)
  • WordPress に特化していない一般的な PHP コードレビュー
  • WordPress のパフォーマンス動作に関係しない、製品または UX のレビュー

コードレビューのワークフロー

  1. ファイルの種類を特定し、以下の該当するチェックを適用します
  2. まず重大なパターンを確認します(OOM、上限のないクエリ、キャッシュの回避)
  3. 警告を確認します(致命的ではないものの非効率な箇所)
  4. 最適化を記録します(あれば望ましい改善点)
  5. 以下の出力形式を使用し、行番号を付けて報告します

ファイルの種類ごとのチェック

プラグイン/テーマの PHP ファイル(functions.php、plugin.php、*.php)

以下を確認します:

  • query_posts() → CRITICAL:絶対に使用しないでください。メインクエリが壊れます
  • posts_per_page.*-1 または numberposts.*-1 → CRITICAL:上限のないクエリです
  • session_start() → CRITICAL:ページキャッシュを回避します
  • add_action.*init.* または add_action.*wp_loaded → 高コストなコードがリクエストのたびに実行されるか確認します
  • 管理者以外のコンテキストでの update_option または add_option → WARNING:ページ読み込み時に DB へ書き込みます
  • キャッシュなしの wp_remote_get または wp_remote_post → WARNING:HTTP リクエストが処理をブロックします
WP_Query/データベースコード

以下を確認します:

  • posts_per_page 引数の欠落 → WARNING:ブログ設定がデフォルトで使用されます
  • 'value' による比較を行う 'meta_query' → WARNING:インデックスのない列をスキャンします
  • ページネーション/ソートを伴う大きな post__not_in 配列 → WARNING:高コストな SQL が生成される可能性があります。ケースごとに確認してください
  • LIKE '%term%'(先頭にワイルドカード)→ WARNING:テーブル全体をスキャンします
  • ページネーションを行わない場合の no_found_rows => true の欠落 → INFO:不要な件数カウントです
AJAX ハンドラー(wp_ajax_*、REST エンドポイント)

以下を確認します:

  • admin-ajax.php の使用 → INFO:代わりに REST API の使用を検討してください
  • 読み取り操作での POST メソッドの使用 → WARNING:キャッシュを回避します
  • setInterval またはポーリングのパターン → CRITICAL:自己 DDoS のリスクがあります
  • nonce 検証の欠落 → セキュリティ上の問題です(パフォーマンスの問題ではありませんが、指摘してください)
テンプレートファイル(テーマ内の *.php)

以下を確認します:

  • ループ内のカスタムクエリ、キャッシュされていない get_post_meta()、またはリモート呼び出し → WARNING:処理の繰り返し/N+1 のリスクがあります
  • ループ内のデータベースクエリ(N+1)→ CRITICAL:クエリが増大します
  • テンプレート内の wp_remote_get → WARNING:レンダリングをブロックします
JavaScript ファイル

以下を確認します:

  • 読み取り操作での $.post( → WARNING:キャッシュ可能にするため GET を使用してください
  • setInterval.*fetch\|ajax → CRITICAL:ポーリングのパターンです
  • import _ from 'lodash' → WARNING:ライブラリ全体をインポートするため、バンドルが肥大化します
  • 読み込み時に AJAX 呼び出しを行うインラインの <script> → 必要性を確認します
ブロックエディター/Gutenberg ファイル(block.json、blocks/ 内の *.js)

以下を確認します:

  • エディター側での高負荷なデータ取得またはプレビュー処理 → WARNING:エディターの読み込みが遅くなります
  • レンダリングコールバック内の wp_kses_post($content) → WARNING:InnerBlocks が壊れます
  • 大きなエディターバンドルまたは広範なインポート → WARNING:エディターの実行環境が肥大化します
アセットの登録(functions.php、*.php)

以下を確認します:

  • バージョン指定のない wp_enqueue_script → INFO:キャッシュバスティングの問題が発生します
  • defer/async 戦略のない wp_enqueue_script → INFO:レンダリングをブロックします
  • THEME_VERSION 定数の欠落 → INFO:バージョン管理の問題です
  • 条件チェックのない wp_enqueue_script → WARNING:特定のページでのみ必要なアセットが全ページで読み込まれます
Transients と Options

次の項目をスキャンします:

  • 動的なキー(例:user_{$id})を使った set_transient → 警告:オブジェクトキャッシュがないとテーブルが肥大化します
  • 頻繁に変化するデータに対する set_transient → 警告:キャッシュの目的を損ないます
  • 共有ホスティングでの Transients の大きなデータ → 警告:オブジェクトキャッシュがないと DB が肥大化します
WP-Cron

次の項目をスキャンします:

  • DISABLE_WP_CRON 定数がない → 情報:Cron はページリクエスト時に実行されます
  • 実行時間の長い Cron コールバック(すべてのユーザー/投稿にわたるループ) → 重大:Cron キューをブロックします
  • すでにスケジュール済みかを確認せずに使われている wp_schedule_event → 警告:スケジュールが重複します

すばやく検出するための検索パターン

# Critical issues - scan these first
rg -n "posts_per_page\s*.*-1|numberposts\s*.*-1" .
rg -n "query_posts\s*\(" .
rg -n "session_start\s*\(" .
rg -n "setInterval.*(fetch|ajax|\\$\\.)" .

# Database writes on frontend
rg -n "update_option|add_option" . -g '*.php'

# Uncached expensive functions
rg -n "url_to_postid|attachment_url_to_postid|count_user_posts" .

# External HTTP without caching
rg -n "wp_remote_get|wp_remote_post|file_get_contents\s*\(\s*['\"]https?://" .

# Cache bypass risks
rg -n "setcookie|session_start" .

# PHP code anti-patterns
rg -n "in_array\s*\(" . -g '*.php'    # Manually verify strict comparison
rg -n "<<<" .
rg -n "cache_results\s*=>\s*false|cache_results\s*,\s*false" .

# JavaScript bundle issues
rg -n "import\s+_\s+from\s+['\"]lodash['\"]" . -g '*.{js,jsx,ts,tsx}'

# Asset loading issues
rg -n "wp_enqueue_script|wp_enqueue_style" . -g '*.php'

# Transient misuse
rg -n "set_transient\s*\([^,]+\\$" . -g '*.php'
rg -n "set_transient" . -g '*.php'    # Manually confirm a preceding get_transient() check

# WP-Cron issues
rg -n "wp_schedule_event" . -g '*.php'  # Manually confirm wp_next_scheduled() guard

プラットフォームのコンテキスト

ホスティング環境によって、必要なアプローチは異なります。

マネージド WordPress ホスト(WP Engine、Pantheon、Pressable、WordPress VIP など):

  • 多くの場合、オブジェクトキャッシュが標準で提供されています
  • プラットフォーム固有のヘルパー関数(VIP の wpcom_vip_* など)が用意されている場合があります
  • 推奨されるパターンについて、ホストのドキュメントを確認してください

セルフホスト/標準的なホスティング:

  • コストの高い関数には、オブジェクトキャッシュのラッパーを手動で実装します
  • 永続的なオブジェクトキャッシュを利用する Redis または Memcached プラグインを検討します
  • キャッシュレイヤーの設定に対する責任がより大きくなります

共有ホスティング:

  • 上限のないクエリや外部 HTTP には特に注意してください
  • リソースが限られているため、パフォーマンスの問題がより早く表面化します
  • 永続的なオブジェクトキャッシュがまったく利用できない場合があります

クイックリファレンス:重大なアンチパターン

データベースクエリ
// ❌ CRITICAL: Unbounded query.
'posts_per_page' => -1

// ✅ GOOD: Set reasonable limit, paginate if needed.
'posts_per_page' => 100,
'no_found_rows'  => true, // Skip count if not paginating.

// ❌ CRITICAL: Never use query_posts().
query_posts( 'cat=1' ); // Breaks pagination, conditionals.

// ✅ GOOD: Use WP_Query or pre_get_posts filter.
$query = new WP_Query( array( 'cat' => 1 ) );
// Or modify main query:
add_action( 'pre_get_posts', function( $query ) {
    if ( $query->is_main_query() && ! is_admin() ) {
        $query->set( 'cat', 1 );
    }
} );

// ❌ WARNING: Unvalidated ID can hide a logic bug and trigger needless queries.
$query = new WP_Query( array( 'p' => intval( $maybe_false_id ) ) );

// ✅ GOOD: Validate ID before querying.
$post_id = absint( $maybe_false_id );
if ( $post_id > 0 ) {
    $query = new WP_Query( array( 'p' => $post_id ) );
}

// ❌ WARNING: LIKE with leading wildcard (full table scan).
$wpdb->get_results( "SELECT * FROM wp_posts WHERE post_title LIKE '%term%'" );

// ✅ GOOD: Use trailing wildcard only, or use WP_Query 's' parameter.
$wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM wp_posts WHERE post_title LIKE %s",
    $wpdb->esc_like( $term ) . '%'
) );

// ❌ WARNING: Large NOT IN queries can become expensive, especially with pagination.
'post__not_in' => $excluded_ids

// ✅ GOOD: Prefer positive inclusion, smaller exclusion lists, or precomputed candidate IDs.
'post__in' => $candidate_ids
フックとアクション
// ❌ WARNING: Code runs on every request via init.
add_action( 'init', 'expensive_function' );

// ✅ GOOD: Check context before running expensive code.
add_action( 'init', function() {
    if ( is_admin() || wp_doing_cron() ) {
        return;
    }
    // Frontend-only code here.
} );

// ❌ CRITICAL: Database writes on every page load.
add_action( 'wp_head', 'prefix_bad_tracking' );
function prefix_bad_tracking() {
    update_option( 'last_visit', time() );
}

// ✅ GOOD: Use object cache buffer, flush via cron.
add_action( 'shutdown', function() {
    wp_cache_incr( 'page_views_buffer', 1, 'counters' );
} );

// ❌ WARNING: Using admin-ajax.php instead of REST API.
// Prefer: register_rest_route() - leaner bootstrap.
PHP コード
// ❌ WARNING: O(n) lookup - use isset() with associative array.
in_array( $value, $array ); // Also missing strict = true.

// ✅ GOOD: O(1) lookup with isset().
$allowed = array( 'foo' => true, 'bar' => true );
if ( isset( $allowed[ $value ] ) ) {
    // Process.
}

// ❌ WARNING: Heredoc prevents late escaping.
$html = <<<HTML
<div>$unescaped_content</div>
HTML;

// ✅ GOOD: Escape at output.
printf( '<div>%s</div>', esc_html( $content ) );
キャッシュの問題
// ❌ WARNING: Uncached expensive function calls.
url_to_postid( $url );
attachment_url_to_postid( $attachment_url );
count_user_posts( $user_id );
wp_oembed_get( $url );

// ✅ GOOD: Wrap with object cache (works on any host).
function prefix_cached_url_to_postid( $url ) {
    $cache_key = 'url_to_postid_' . md5( $url );
    $post_id   = wp_cache_get( $cache_key, 'url_lookups' );

    if ( false === $post_id ) {
        $post_id = url_to_postid( $url );
        wp_cache_set( $cache_key, $post_id, 'url_lookups', HOUR_IN_SECONDS );
    }

    return $post_id;
}

// ✅ GOOD: On WordPress VIP, use platform helpers instead.
// wpcom_vip_url_to_postid(), wpcom_vip_attachment_url_to_postid(), etc.

// ❌ WARNING: Large autoloaded options.
add_option( 'prefix_large_data', $data ); // Add: , '', 'no' for autoload.

// ❌ INFO: Missing wp_cache_get_multiple for batch lookups.
foreach ( $ids as $id ) {
    wp_cache_get( "key_{$id}" );
}
AJAX と外部リクエスト
// ❌ WARNING: AJAX POST request (bypasses cache).
$.post( ajaxurl, data ); // Prefer: $.get() for read operations.

// ❌ CRITICAL: Polling pattern (self-DDoS).
setInterval( () => fetch( '/wp-json/...' ), 5000 );
// ❌ WARNING: Synchronous external HTTP in page load.
wp_remote_get( $url ); // Cache result or move to cron.

// ✅ GOOD: Set timeout and handle errors.
$response = wp_remote_get( $url, array( 'timeout' => 2 ) );
if ( is_wp_error( $response ) ) {
    return get_fallback_data();
}
WP Cron
// INFO: On high-traffic or cron-heavy sites, request-driven cron may not be enough.
// Consider adding to wp-config.php:
define( 'DISABLE_WP_CRON', true );
// Run via server cron: * * * * * wp cron event run --due-now

// ❌ CRITICAL: Long-running cron blocks entire queue.
add_action( 'my_daily_sync', function() {
    foreach ( get_users() as $user ) { // 50k users = hours.
        sync_user_data( $user );
    }
} );

// ✅ GOOD: Batch processing with rescheduling.
add_action( 'my_batch_sync', function() {
    $offset = (int) get_option( 'sync_offset', 0 );
    $users  = get_users( array( 'number' => 100, 'offset' => $offset ) );

    if ( empty( $users ) ) {
        delete_option( 'sync_offset' );
        return;
    }

    foreach ( $users as $user ) {
        sync_user_data( $user );
    }

    update_option( 'sync_offset', $offset + 100 );
    wp_schedule_single_event( time() + 60, 'my_batch_sync' );
} );

// ❌ WARNING: Scheduling without checking if already scheduled.
wp_schedule_event( time(), 'hourly', 'my_task' ); // Creates duplicates!

// ✅ GOOD: Check before scheduling.
if ( ! wp_next_scheduled( 'my_task' ) ) {
    wp_schedule_event( time(), 'hourly', 'my_task' );
}
キャッシュ回避の問題
// ❌ CRITICAL: Plugin starts PHP session on frontend (bypasses ALL page cache).
session_start(); // Check plugins for this - entire site becomes uncacheable!

// ❌ WARNING: Unique query params create cache misses.
// https://example.com/?utm_source=fb&utm_campaign=123&fbclid=abc
// Each unique URL = separate cache entry = cache miss.
// Solution: Strip marketing params at CDN/edge level.

// ❌ WARNING: Setting cookies on public pages.
setcookie( 'visitor_id', $id ); // Prevents caching for that user.
Transients の誤用
// ❌ WARNING: Dynamic transient keys create table bloat (without object cache).
set_transient( "user_{$user_id}_cart", $data, HOUR_IN_SECONDS );
// 10,000 users = 10,000 rows in wp_options!

// ✅ GOOD: Use object cache for user-specific data.
wp_cache_set( "cart_{$user_id}", $data, 'user_carts', HOUR_IN_SECONDS );

// ❌ WARNING: Transients for frequently-changing data defeats purpose.
set_transient( 'visitor_count', $count, 60 ); // Changes every minute.

// ✅ GOOD: Use object cache for volatile data.
wp_cache_set( 'visitor_count', $count, 'stats' );

// ❌ WARNING: Large data in transients on shared hosting.
set_transient( 'api_response', $megabytes_of_json, DAY_IN_SECONDS );
// Without object cache = serialized blob in wp_options.

// ✅ GOOD: Check hosting before using transients for large data.
if ( wp_using_ext_object_cache() ) {
    set_transient( 'api_response', $data, DAY_IN_SECONDS );
} else {
    // Store in files or skip caching on shared hosting.
}
アセットの読み込み
// ❌ WARNING: Assets load globally when only needed on specific pages.
add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script( 'contact-form-js', ... );
    wp_enqueue_style( 'contact-form-css', ... );
} );

// ✅ GOOD: Conditional enqueue based on page/template.
add_action( 'wp_enqueue_scripts', function() {
    if ( is_page( 'contact' ) || is_page_template( 'contact-template.php' ) ) {
        wp_enqueue_script( 'contact-form-js', ... );
        wp_enqueue_style( 'contact-form-css', ... );
    }
} );

// ✅ GOOD: Only load WooCommerce assets on shop pages.
add_action( 'wp_enqueue_scripts', function() {
    if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() ) {
        wp_dequeue_style( 'woocommerce-general' );
        wp_dequeue_script( 'wc-cart-fragments' );
    }
} );
外部 API リクエスト
// ❌ WARNING: No timeout set (default is 5 seconds).
wp_remote_get( $url ); // Set timeout: array( 'timeout' => 2 ).

// ❌ WARNING: Missing error handling for API failures.
$response = wp_remote_get( $url );
echo $response['body']; // Check is_wp_error() first!
サイトマップとリダイレクト
// ❌ WARNING: Generating sitemaps for deep archives (crawlers hammer these).
// Solution: Exclude old post types, cache generated sitemaps.

// ❌ CRITICAL: Redirect loops consuming CPU.
// Debug with: x-redirect-by header, wp_debug_backtrace_summary().
投稿メタクエリ
// ❌ WARNING: Searching meta_value without index.
'meta_query' => array(
    array(
        'key'   => 'color',
        'value' => 'red',
    ),
)
// Better: Use taxonomy or encode value in meta_key name.

// ❌ WARNING: Binary meta values requiring value scan.
'meta_key'   => 'featured',
'meta_value' => 'true',
// Better: Presence of 'is_featured' key = true, absence = false.

各パターンの詳しい説明については: references/anti-patterns.md を読み込みます

重大度の定義

重大度説明
Critical大規模な運用時に障害を引き起こします(OOM、500 エラー、DB ロック)
Warning高負荷時にパフォーマンスが低下します
Info最適化の機会です

出力形式

次のように指摘事項を構成します:

## Performance Review: [filename/component]

### Critical Issues
- **Line X**: [Issue] - [Explanation] - [Fix]

### Warnings  
- **Line X**: [Issue] - [Explanation] - [Fix]

### Recommendations
- [Optimization opportunities]

### Summary
- Total issues: X Critical, Y Warnings, Z Info
- Estimated impact: [High/Medium/Low]

よくある間違い

パフォーマンスレビューを行う際は、次のような誤りを避けてください。

間違い誤りである理由修正方法
管理画面のみで使われるコードの posts_per_page => -1 を問題として指摘する管理画面のクエリは一般公開時の規模に直面しませんコンテキストを確認してください。管理画面、CLI、Cron のリスクは低くなります
プラグイン内に埋もれた session_start() を見逃すキャッシュ回避はサイト全体に影響します常にすべてのコードを対象に session_start を grep してください
ページネーションを使わないクエリの no_found_rows を無視する小さな最適化ですが、積み重なります警告ではなく、情報として指摘してください
共有ホスティングでオブジェクトキャッシュを推奨する多くの共有ホストでは永続キャッシュを利用できませんまずホスティング環境を確認してください
PHP だけをレビューして、JS のポーリングを見逃すJS の setInterval と fetch の組み合わせは、自己 DDoS につながりますポーリングパターンがないか、.js ファイルをレビューしてください

詳細リファレンス

タスクに応じて、次のリファレンスを読み込みます。

タスク読み込むリファレンス
PHP コードの問題をレビューするreferences/anti-patterns.md
WP_Query 呼び出しを最適化するreferences/wp-query-guide.md
キャッシュを実装するreferences/caching-guide.md
高トラフィックイベントへの準備references/measurement-guide.md

注:標準的なコードレビューには、必要なすべてのパターンが anti-patterns.md に含まれています。ほかのリファレンスでは、クエリの最適化、キャッシュ戦略の実装、またはトラフィックイベントへの準備を行う場合に、より詳しい説明を提供します。

コメント