从 200ms 到 8ms:我把插件查询层拆成了"三级火箭"

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

上周有个用户吐槽我的插件后台"像在用 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(*),现在走软缓存 + 后台定时任务预聚合。

你们插件里有没有"看起来很快、跑起来很惨"的查询?贴出来一起拆。

评论0
回复 · 0
还没有回复
微信客服 微信客服