把 `get_posts` 换成 `WP_Query` 再裸奔:我整理了一份插件查询层"性能急救包"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 51 浏览 3 回复

上周帮一个兄弟看插件,后台加载 8 秒,top 一看 CPU 飙到 90%。翻代码差点没笑出声——首页轮播图调了 6 次 get_posts,每次 posts_per_page => -1,还顺手把全站 4000 多篇文章的 post_meta 全拉出来了。今天不聊架构,就聊三个能立刻见效的脏活:查询剪枝、缓存埋点、静态资源瘦身。

一、查询层:别让 MySQL 替你"兜底"

WordPress 的查询优化有个反直觉的点:get_posts 默认 suppress_filters => true,看起来快,但它绕过了缓存插件的 SQL 拦截;WP_Query 慢在钩子多,可你真正该怕的是 posts_per_page => -1 和没设 fields 的查询。

看个真实改造。某插件要拉"最近更新的 20 个分类",原代码:

// 原罪:-1 + 没限字段 + 嵌套查询
$terms = get_terms([
    'taxonomy'   => 'category',
    'orderby'    => 'term_id',
    'hide_empty' => false,
    'number'     => 20,  // 看起来设了,但下面又全量查
]);

foreach ($terms as $term) {
    $posts = get_posts([
        'cat'            => $term->term_id,
        'posts_per_page' => -1,  // 💥
        'post_status'    => 'publish',
    ]);
    // ... 只取每分类最新 1 篇的 ID
}

改后:

// 一次性拿关系,再按需补数据
$term_ids = get_terms([
    'taxonomy'   => 'category',
    'orderby'    => 'term_id',
    'order'      => 'DESC',
    'hide_empty' => false,
    'fields'     => 'ids',        // 只拿 ID,内存省 80%
    'number'     => 20,
]);

// 单条 SQL 取最新文章映射,不用 N+1
global $wpdb;
$placeholders = implode(',', array_fill(0, count($term_ids), '%d'));
$latest_map = $wpdb->get_results($wpdb->prepare("
    SELECT term_id, MAX(object_id) as post_id
    FROM {$wpdb->term_relationships} tr
    JOIN {$wpdb->posts} p ON tr.object_id = p.ID
    WHERE tr.term_taxonomy_id IN (
        SELECT term_taxonomy_id 
        FROM {$wpdb->term_taxonomy} 
        WHERE term_id IN ($placeholders)
    )
    AND p.post_status = 'publish'
    GROUP BY term_id
", ...$term_ids), OBJECT_K);

// 最后按需 get_post(),走对象缓存

关键不是写原生 SQL 炫技,是先想数据长什么样,再决定查几次fields => idsno_found_rows => true(不分页时)、update_post_meta_cache => false(不读 meta 时)这三个参数,插件里能救半条命。

二、缓存层:别只会在 transient 上堆时间

很多人缓存就一招:set_transient('my_data', $data, HOUR_IN_SECONDS)。问题是你数据变了,缓存还在,用户骂你"保存不生效";或者 10 个并发同时击穿,MySQL 又扛一波。

我现在的习惯是按"失效事件"埋缓存键,而不是按时间:

class MyPlugin_Cache {
    private static $group = 'myplugin_v2';
    
    // 缓存键带"版本号",数据变时只递增版本,不用清具体键
    public static function get_version($type) {
        $ver = wp_cache_get("ver:{$type}", self::$group);
        if (false === $ver) {
            $ver = get_option("myplugin_cache_ver_{$type}", 1);
            wp_cache_set("ver:{$type}", $ver, self::$group);
        }
        return $ver;
    }
    
    public static function bump_version($type) {
        $ver = self::get_version($type) + 1;
        update_option("myplugin_cache_ver_{$type}", $ver);
        wp_cache_set("ver:{$type}", $ver, self::$group);
    }
    
    public static function get($key, $type) {
        $ver = self::get_version($type);
        return wp_cache_get("{$key}:v{$ver}", self::$group);
    }
    
    public static function set($key, $data, $type) {
        $ver = self::get_version($type);
        wp_cache_set("{$key}:v{$ver}", $data, self::$group, HOUR_IN_SECONDS * 6);
    }
}

// 用的时候
$stats = MyPlugin_Cache::get('dashboard_stats', 'stats');
if (false === $stats) {
    $stats = heavy_calculation();
    MyPlugin_Cache::set('dashboard_stats', $stats, 'stats');
}

// 某处数据变了,一行失效全部相关缓存
MyPlugin_Cache::bump_version('stats');

这套偷师自 object cache 的 cache invalidation 模式,适合读多写少、但写必须即时生效的场景。如果用的是 Redis/Memcached,wp_cache_* 直接落内存;没装 object cache 的话,这套会退化到 wp_options,但至少逻辑统一。

三、静态资源:插件前端别当"打包侠"

插件里塞个 Vue/React 构建产物,vendor.js 2MB,自己代码 30KB,这种事我见多了。WordPress 插件不是 SPA,用户可能只用到你 1/10 的功能,却加载 100% 的脚本。

我的拆分原则:

  • 按路由/功能块拆 entry:后台设置页一个 JS,前台短码一个 JS,公共逻辑打 runtime + common
  • wp_enqueue_script$deps 做依赖声明,别自己 document.write 塞脚本
  • 动态 import 给重型组件:图表库、编辑器这些,点开了再拉

贴个实际 enqueue 结构:

// 公共基础,所有页面都可能用到(很小,< 10KB)
wp_register_script(
    'myplugin-base',
    plugin_dir_url(__FILE__) . 'dist/base.js',
    ['jquery'],
    '2.1.0',
    true
);

// 后台专用,依赖 base
if (is_admin()) {
    wp_enqueue_script(
        'myplugin-admin',
        plugin_dir_url(__FILE__) . 'dist/admin.js',
        ['myplugin-base', 'wp-api-fetch'],  // 复用 base,挂官方 api-fetch
        '2.1.0',
        true
    );
    
    // 只在设置页加载的重量级组件
    $screen = get_current_screen();
    if ($screen && $screen->id === 'toplevel_page_myplugin') {
        wp_enqueue_script(
            'myplugin-settings-chunk',
            plugin_dir_url(__FILE__) . 'dist/settings.js',
            ['myplugin-admin'],
            '2.1.0',
            true
        );
    }
}

// 前台只在短码存在时加载
add_filter('the_content', function($content) {
    if (has_shortcode($content, 'myplugin_form')) {
        wp_enqueue_script('myplugin-frontend');
    }
    return $content;
});

还有个冷门技巧:script_loader_tag 过滤器给特定脚本加 asyncdefer,但别全局加——jQuery 依赖链会炸。只给你自己确定无依赖的 chunk 加:

add_filter('script_loader_tag', function($tag, $handle) {
    if ($handle === 'myplugin-analytics') {
        return str_replace(' src=', ' async src=', $tag);
    }
    return $tag;
}, 10, 2);

最后

性能优化不是一次性的,是每次加功能时多问一句:这查询能限字段吗?这缓存失效时机对吗?这脚本必须现在加载吗?

你们插件里有没有那种"当时图省事,现在想

评论3
回复 · 3
chenshao
chenshao 新手 · #3 ·
mommy
chenshao
chenshao 新手 · #2 ·
jjff
chenshao
chenshao 新手 · #1 ·
hh
微信客服 微信客服