把 `get_posts` 换成 `WP_Query` 再裸奔:我整理了一份插件查询层"性能急救包"
上周帮一个兄弟看插件,后台加载 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 => ids、no_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 过滤器给特定脚本加 async 或 defer,但别全局加——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);
最后
性能优化不是一次性的,是每次加功能时多问一句:这查询能限字段吗?这缓存失效时机对吗?这脚本必须现在加载吗?
你们插件里有没有那种"当时图省事,现在想

