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. 05使用 rg 搜索常见反模式

适用场景

  1. 01审查 WordPress 代码变更
  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 或数据库操作
  • 调查内存耗尽或数据库锁

不适用于:

  • 仅进行安全审计(如有 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() → 严重:禁止使用,会破坏主查询
  • posts_per_page.*-1 或 numberposts.*-1 → 严重:无界查询
  • session_start() → 严重:绕过页面缓存
  • add_action.*init.* 或 add_action.*wp_loaded → 检查是否每次请求都会运行高开销代码
  • 非管理后台上下文中的 update_option 或 add_option → 警告:页面加载时写入数据库
  • 未使用缓存的 wp_remote_get 或 wp_remote_post → 警告:阻塞式 HTTP 请求
WP_Query / 数据库代码

扫描以下内容:

  • 缺少 posts_per_page 参数 → 警告:默认使用博客设置中的值
  • 使用 'value' 比较的 'meta_query' → 警告:扫描未建立索引的列
  • 用于分页/排序的大型 post__not_in 数组 → 警告:可能生成开销较大的 SQL;需逐案审查
  • LIKE '%term%'(前导通配符)→ 警告:全表扫描
  • 未进行分页时缺少 no_found_rows => true → 信息:执行了不必要的计数
AJAX 处理程序(wp_ajax_*、REST 端点)

扫描以下内容:

  • 使用 admin-ajax.php → 信息:考虑改用 REST API
  • 读取操作使用 POST 方法 → 警告:绕过缓存
  • setInterval 或轮询模式 → 严重:存在自我 DDoS 风险
  • 缺少 nonce 验证 → 安全问题(非性能问题,但应标记)
模板文件(主题中的 *.php)

扫描以下内容:

  • 循环中执行自定义查询、未缓存的 get_post_meta() 或远程调用 → 警告:重复执行 / 存在 N+1 风险
  • 循环中执行数据库查询(N+1)→ 严重:查询次数成倍增加
  • 模板中使用 wp_remote_get → 警告:阻塞渲染
JavaScript 文件

扫描以下内容:

  • 读取操作使用 $.post( → 警告:使用 GET 以便缓存
  • setInterval.*fetch\|ajax → 严重:轮询模式
  • import _ from 'lodash' → 警告:完整导入库会增大打包体积
  • 页面加载时执行 AJAX 调用的内联 <script> → 检查其必要性
区块编辑器 / Gutenberg 文件(block.json、blocks/ 中的 *.js)

扫描以下内容:

  • 编辑器端大量获取数据或预览逻辑 → 警告:拖慢编辑器加载
  • 渲染回调中的 wp_kses_post($content) → 警告:会破坏 InnerBlocks
  • 大型编辑器打包文件或范围过广的导入 → 警告:增大编辑器运行时开销
资源注册(functions.php、*.php)

扫描以下内容:

  • wp_enqueue_script 未设置版本 → 信息:缓存失效处理存在问题
  • wp_enqueue_script 未使用 defer/async 策略 → 信息:阻塞渲染
  • 缺少 THEME_VERSION 常量 → 信息:版本管理问题
  • wp_enqueue_script 未进行条件检查 → 警告:仅特定页面需要的资源却在全站加载
瞬态与选项

扫描以下内容:

  • 使用动态键(例如 user_{$id})的 set_transient → 警告:没有对象缓存时会导致表膨胀
  • 用于频繁变化数据的 set_transient → 警告:违背缓存目的
  • 共享主机上的大型瞬态数据 → 警告:没有对象缓存时会导致数据库膨胀
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.
滥用瞬态
// ❌ 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

严重程度定义

严重程度说明
严重会在规模扩大时导致故障(OOM、500 错误、数据库锁)
警告高负载下会降低性能
信息优化机会

输出格式

按以下结构整理发现:

## 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()缓存绕过会影响整个站点始终在所有代码中 grep session_start
忽略非分页查询中的 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 包含所需的所有模式。其他参考资料则在专门优化查询、实施缓存策略或为流量事件做准备时提供更深入的背景。

评论

支持 Markdown