wp-performance-review について
WordPress のテーマ、プラグイン、独自コードのパフォーマンスを確認するスキルです。重大なリスクを先に調べ、行番号と重要度を添えて結果をまとめます。
主な機能
- 01重大な性能パターンを優先検出
- 02PHP、JavaScript、DBコードを確認
- 03AJAX、アセット、キャッシュ、Cronを網羅
- 04重要度と行番号付きで報告
活用シーン
- 01WordPressのテーマやプラグイン変更のレビュー
- 02表示遅延、タイムアウト、500エラーの調査
- 03高トラフィック企画前のコード監査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elvismdev/ claude-wordpress-skills --skill wp-performance-reviewインストール後、Claude Code で/wp-performance-reviewを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mdcaching-guide.mdmeasurement-guide.mdwp-query-guide.md詳細
WordPress パフォーマンスレビューのスキル
概要
WordPress のテーマ、プラグイン、カスタムコードを対象とする、体系的なパフォーマンスコードレビューです。基本原則: まず重大な問題(OOM、無制限クエリ、キャッシュの回避)をスキャンし、次に警告、最後に最適化項目を確認します。行番号と重大度を付けて報告します。
使用する場面
次の場合に使用します:
- WordPress のテーマまたはプラグインの PR/コードをレビューする
- ページの読み込みが遅い、タイムアウトする、または 500 エラーが発生するとユーザーから報告された
- トラフィックの多いイベント(ローンチ、セール、バイラルな話題)の前に監査する
- WP_Query またはデータベース操作を最適化する
- メモリ不足または DB ロックを調査する
次の場合は使用しないでください:
- セキュリティのみを対象とする監査(利用可能な場合は wp-security-review を使用してください)
- Gutenberg ブロックの開発パターン(利用可能な場合は wp-gutenberg-blocks を使用してください)
- WordPress 固有ではない一般的な PHP コードレビュー
コードレビューワークフロー
- ファイルの種類を特定し、以下の該当するチェックを適用します
- まず重大なパターンをスキャンします(OOM、無制限クエリ、キャッシュの回避)
- 警告を確認します(致命的ではないものの非効率な箇所)
- 最適化項目を記録します(あると望ましい改善)
- 以下の出力形式を使用し、行番号を付けて報告します
ファイルの種類別チェック
プラグイン/テーマの PHP ファイル(functions.php、plugin.php、*.php)
以下をスキャンします:
query_posts()→ 重大:絶対に使用しないでください。メインクエリが壊れますposts_per_page.*-1またはnumberposts.*-1→ 重大:無制限クエリsession_start()→ 重大:ページキャッシュを回避しますadd_action.*init.*またはadd_action.*wp_loaded→ 高コストなコードがリクエストごとに実行されるか確認します- 管理者以外のコンテキストでの
update_optionまたはadd_option→ 警告:ページ読み込み時に DB へ書き込みます - キャッシュのない
wp_remote_getまたはwp_remote_post→ 警告:HTTP 処理がブロックされます
WP_Query/データベースコード
以下をスキャンします:
posts_per_page引数の欠落 → 警告:ブログ設定の値がデフォルトで使われます'value'比較を含む'meta_query'→ 警告:インデックスのないカラムをスキャンします- 大きな配列を含む
post__not_in→ 警告:除外処理が遅くなります LIKE '%term%'(先頭にワイルドカード) → 警告:テーブル全体をスキャンします- ページネーションを使用しない場合の
no_found_rows => trueの欠落 → 情報:不要な件数カウントです
AJAX ハンドラー(wp_ajax_*、REST エンドポイント)
以下をスキャンします:
admin-ajax.phpの使用 → 情報:代わりに REST API を検討してください- 読み取り操作での POST メソッド → 警告:キャッシュを回避します
setIntervalまたはポーリングパターン → 重大:自己 DDoS のリスクがあります- nonce 検証の欠落 → セキュリティ上の問題(パフォーマンスの問題ではありませんが、指摘してください)
テンプレートファイル(テーマ内の *.php)
以下をスキャンします:
- ループ内の
get_template_part→ 警告:出力のキャッシュを検討してください - ループ内のデータベースクエリ(N+1) → 重大:クエリ数が増加します
- テンプレート内の
wp_remote_get→ 警告:レンダリングをブロックします
JavaScript ファイル
以下をスキャンします:
- 読み取り操作での
$.post(→ 警告:キャッシュ可能にするため GET を使用してください setInterval.*fetch\|ajax→ 重大:ポーリングパターンですimport _ from 'lodash'→ 警告:ライブラリ全体のインポートによりバンドルが肥大化します- ページ読み込み時に AJAX 呼び出しを行うインライン
<script>→ 必要性を確認します
ブロックエディター/Gutenberg ファイル(block.json、blocks/ 内の *.js)
以下をスキャンします:
- 多数の
registerBlockStyle()呼び出し → 警告:呼び出しごとにプレビュー用 iframe が作成されます - レンダーコールバック内の
wp_kses_post($content)→ 警告:InnerBlocks が壊れます render_callbackのない静的ブロック → 情報:保守性のために動的ブロックを検討してください
アセットの登録(functions.php、*.php)
以下をスキャンします:
- バージョン指定のない
wp_enqueue_script→ 情報:キャッシュの無効化に問題が生じます defer/asyncの戦略を指定していないwp_enqueue_script→ 情報:レンダリングをブロックしますTHEME_VERSION定数の欠落 → 情報:バージョン管理に関する問題です- 条件チェックのない
wp_enqueue_script→ 警告:特定のページでのみ必要なアセットが全ページで読み込まれます
トランジェントとオプション
以下をスキャンします:
- 動的なキー(例:
user_{$id})を使用するset_transient→ 警告:オブジェクトキャッシュがない場合、テーブルが肥大化します - 頻繁に変化するデータに対する
set_transient→ 警告:キャッシュの目的を損ないます - 共有ホスティングでトランジェントに大きなデータを保存する → 警告:オブジェクトキャッシュがない場合、DB が肥大化します
WP-Cron
次の項目を確認します。
DISABLE_WP_CRON定数が未設定 → INFO:Cron はページリクエスト時に実行されます- 実行時間の長い Cron コールバック(全ユーザー/投稿をループ処理)→ CRITICAL:Cron キューをブロックします
- スケジュール済みかどうかを確認せずに
wp_schedule_eventを呼び出している → WARNING:スケジュールが重複します
すばやく検出するための検索パターン
# Critical issues - scan these first
grep -rn "posts_per_page.*-1\|numberposts.*-1" .
grep -rn "query_posts\s*(" .
grep -rn "session_start\s*(" .
grep -rn "setInterval.*fetch\|setInterval.*ajax\|setInterval.*\\\$\." .
# Database writes on frontend
grep -rn "update_option\|add_option" . | grep -v "admin\|activate\|install"
# Uncached expensive functions
grep -rn "url_to_postid\|attachment_url_to_postid\|count_user_posts" .
# External HTTP without caching
grep -rn "wp_remote_get\|wp_remote_post\|file_get_contents.*http" .
# Cache bypass risks
grep -rn "setcookie\|session_start" .
# PHP code anti-patterns
grep -rn "in_array\s*(" . | grep -v "true\s*)" # Missing strict comparison
grep -rn "<<<" . # Heredoc/nowdoc syntax
grep -rn "cache_results.*false" .
# JavaScript bundle issues
grep -rn "import.*from.*lodash['\"]" . # Full lodash import
grep -rn "registerBlockStyle" . # Many block styles = performance issue
# Asset loading issues
grep -rn "wp_enqueue_script\|wp_enqueue_style" . | grep -v "is_page\|is_singular\|is_admin"
# Transient misuse
grep -rn "set_transient.*\\\$" . # Dynamic transient keys
grep -rn "set_transient" . | grep -v "get_transient" # Set without checking first
# WP-Cron issues
grep -rn "wp_schedule_event" . | grep -v "wp_next_scheduled" # Missing schedule check
プラットフォームのコンテキスト
ホスティング環境によって、必要なアプローチは異なります。
マネージド 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 );
}
} );
// ❌ CRITICAL: Missing WHERE clause (falsy ID becomes 0).
$query = new WP_Query( array( 'p' => intval( $maybe_false_id ) ) );
// ✅ GOOD: Validate ID before querying.
if ( ! empty( $maybe_false_id ) ) {
$query = new WP_Query( array( 'p' => intval( $maybe_false_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: NOT IN queries (filter in PHP instead).
'post__not_in' => $excluded_ids
// ✅ GOOD: Fetch all, filter in PHP (faster for large exclusion lists).
$posts = get_posts( array( 'posts_per_page' => 100 ) );
$posts = array_filter( $posts, function( $post ) use ( $excluded_ids ) {
return ! in_array( $post->ID, $excluded_ids, true );
} );
フックとアクション
// ❌ 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
// ❌ WARNING: WP Cron runs on page requests.
// Add 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 を無視する | 小さな最適化ですが、積み重なります | WARNING ではなく INFO として指摘してください |
| 共有ホスティングでオブジェクトキャッシュを推奨する | 多くの共有ホストには永続キャッシュがありません | まずホスティング環境を確認してください |
| 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 に含まれています。他のリファレンスは、クエリの最適化、キャッシュ戦略の実装、またはトラフィックイベントへの準備に特化して取り組む場合に、より詳しい情報を提供します。