从 200ms 到 8ms:我把插件查询层拆成了"三级火箭"
上周有个用户吐槽我的插件后台"像在用 3G 上网",Profiler 一跑,get_posts 占了 87% 的请求时间。今天把重构过程摊开,不聊大道理,只贴能抄的代码。
第一级:把 "SELECT *" 的瘾戒了
WordPress 默认查询是贪婪的。我之前图省事:
// 作死写法,post_content 可能存了几 MB 的 base64
$posts = get_posts( [
'post_type' => 'my_custom_type',
'posts_per_page' => 50,
] );
foreach ( $posts as $post ) {
// 其实只用 ID 和 title
do_something( $post->ID, $post->post_title );
}
改完只取要用的字段,时间从 180ms 掉到 45ms:
$posts = get_posts( [
'post_type' => 'my_custom_type',
'posts_per_page' => 50,
'fields' => 'ids', // 或者 'id=>parent'
] );
// 真要 title?拆两次查询,或者用自定义 SQL
global $wpdb;
$results = $wpdb->get_results(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_type = 'my_custom_type'
LIMIT 50",
ARRAY_A
);
注意 'fields' => 'ids' 会跳过 WP_Post 对象构造,省的不只是数据库 IO,还有内存分配。
第二级:给"温数据"搭个廉价缓存层
用户列表、统计卡片这种"几分钟变一次就行"的数据,别每次都算。我搞了个"分层过期"策略:
class Lazy_Cache {
private string $group;
private int $soft_ttl; // 正常过期
private int $hard_ttl; // 兜底过期,防止缓存击穿后雪崩
public function get( string $key, callable $factory ) {
$full_key = "lazy_{$key}";
$cached = wp_cache_get( $full_key, $this->group );
// 软命中:直接返回
if ( false !== $cached && $cached['expires'] > time() ) {
return $cached['data'];
}
// 硬命中:返回旧数据,后台异步刷新(用 cron 或 shutdown hook)
if ( false !== $cached && $cached['expires'] > time() - $this->hard_ttl ) {
// 加个锁,防止并发重建
if ( wp_cache_add( "lock_{$key}", 1, $this->group, 30 ) ) {
add_action( 'shutdown', function() use ( $key, $factory ) {
$this->set( $key, $factory() );
} );
}
return $cached['data'];
}
// 彻底未命中,同步重建
$value = $factory();
$this->set( $key, $value );
return $value;
}
private function set( string $key, $value ): void {
wp_cache_set( "lazy_{$key}", [
'data' => $value,
'expires' => time() + $this->soft_ttl,
], $this->group, $this->soft_ttl + $this->hard_ttl );
}
}
用的时候:
$cache = new Lazy_Cache( 'my_plugin_stats', 60, 300 ); // 软60秒,硬300秒
$stats = $cache->get( 'dashboard_cards', function() {
// 这里可能是三个复杂查询的聚合
return [
'total' => count_expensive_query(),
'trend' => another_slow_thing(),
'heatmap' => yet_another(),
];
} );
关键技巧:shutdown hook 里刷新缓存,用户请求不阻塞;wp_cache_add 的原子性当分布式锁用,比 transient 的 _lock 后缀靠谱。
第三级:静态资源"按需诈尸"
插件后台 JS/CSS 以前是一锅炖,admin_enqueue_scripts 里不管哪个页面都加载 400KB。改成"路由感知"加载:
add_action( 'admin_enqueue_scripts', function( $hook ) {
// 只在自己的页面加载核心包
if ( $hook !== 'toplevel_page_my_plugin' ) {
return;
}
// 动态 chunk:编辑页才需要富文本编辑器
wp_enqueue_script(
'my-plugin-core',
plugins_url( 'dist/core.js', __FILE__ ),
[ 'jquery' ],
'1.2.3',
true
);
// 用 wp_add_inline_script 注入当前用户权限,减少一次 AJAX
wp_add_inline_script( 'my-plugin-core', sprintf(
'window.MY_PLUGIN_CAPS = %s;',
wp_json_encode( [
'can_edit' => current_user_can( 'edit_posts' ),
'can_export' => current_user_can( 'manage_options' ),
] )
), 'before' );
// 子模块:只有 ?tab=analytics 时才加载图表库
if ( ( $_GET['tab'] ?? '' ) === 'analytics' ) {
wp_enqueue_script(
'my-plugin-charts',
plugins_url( 'dist/charts.js', __FILE__ ),
[ 'my-plugin-core' ],
'1.2.3',
true
);
}
} );
额外收益:wp_json_encode 塞权限比前端再请求一次 /wp-json/ 快了 50-100ms,而且避免了未登录时的 403 往返。
一个反直觉的坑:对象缓存不是万能药
共享主机上有人开了 Memcached,但 wp_cache_get 比直接查 MySQL 还慢——因为外网连接 Memcached 有 20ms RTT。这种场景我加了环境探测:
if ( defined( 'WP_CACHE' ) && WP_CACHE ) {
$test = wp_cache_get( '_health_check' );
if ( false === $test ) {
wp_cache_set( '_health_check', 1, '', 1 );
$test = wp_cache_get( '_health_check' );
}
// 如果 set/get 往返超过 5ms,降级到 transient 或裸查询
}
最后数据:后台首屏从 1.8s 到 220ms,数据库查询从 47 次降到 6 次。最慢的那个统计卡片,原来每次请求都算全表 COUNT(*),现在走软缓存 + 后台定时任务预聚合。
你们插件里有没有"看起来很快、跑起来很惨"的查询?贴出来一起拆。

