WordPress Multisite + GlotPress Translation Platform: Real-World Optimization Reducing TTFB from 45 Seconds to 60 Milliseconds

Problem

A WordPress Multisite translation platform (GlotPress 4.0.3 + WordPress 6.8.3) running 67 subsites exhibits severe performance degradation:

  • Homepage TTFB: 5.7 seconds
  • Traditional Chinese pages (/zh-tw/, /zh-hk/): TTFB up to 45 seconds
  • Server load consistently exceeds 30

Server hardware is robust: 80 CPU cores, 62 GB RAM, PHP 8.3, and Redis already deployed (via Object Cache Pro). Yet PHP-FPM workers (22 total) all run at >80% CPU utilization.

Root Cause Analysis

Three compounding factors were identified:

1. wp-chinese-converter’s OpenCC Dictionary Sorting

The primary performance bottleneck. The plugin uses the OpenCC PHP library for simplified–traditional Chinese conversion. Its core inefficiency lies in the get_prepared_dictionaries() method in class-wpcc-opencc-converter.php:

// 53,650 dictionary entries — sorted on *every* request
uksort($flat, function ($a, $b) {
    return mb_strlen($b) <=> mb_strlen($a);
});

Although $this->prepared[] caches results within a single request, PHP-FPM spawns isolated processes per request — meaning this expensive sort runs afresh every time. Sorting 53,650 entries with uksort + mb_strlen comparisons consumes several seconds per request.

Subsequently, strtr($output, $dict) performs replacements across >50,000 entries — itself highly inefficient.

2. Missing robots.txt → Crawler Storm on Translation Pages

No robots.txt file existed, allowing aggressive crawlers to flood translation endpoints:

  • MJ12bot crawled /zh-hk/tag/right-sidebar/page/1105 (yes — page 1,105)
  • GPTBot, OAI-SearchBot, Bytespider, SemrushBot, and ClaudeBot concurrently scanned similar paths

Each crawler request triggers full PHP execution, OpenCC dictionary sorting, and large-scale strtr replacement. Among the last 10,000 requests, ~10% were from such bots — yet they exclusively targeted CPU-intensive translation pages.

3. Cavalcade Cron Task Explosion

Cavalcade (a WordPress cron replacement) had 63 zombie action_scheduler_run_queue tasks stuck since November 2025. Five Cavalcade workers each consumed ~95% CPU. Compounding this, WP-Rocket’s activation triggered simultaneous RUCSS generation and preload tasks across all 67 subsites — overwhelming system resources.

4. wp-chinese-converter’s Set-Cookie Headers Break Caching

The plugin sets Set-Cookie: wpcc_variant_xxx on every request, causing nginx fastcgi_cache to skip caching those responses by default — even if cache rules are correctly configured.

Optimization Strategy

Step 1: Block Crawlers (Immediate Impact)

Create robots.txt to disallow low-value crawlers:

User-agent: MJ12bot
Disallow: /

User-agent: Bytespider
Disallow: /

# Repeat for GPTBot, OAI-SearchBot, SemrushBot, DotBot, AhrefsBot, PetalBot, ClaudeBot

User-agent: *
Disallow: /wp-admin/
Disallow: /glotpress/
Disallow: /translate/
Disallow: /zh-tw/tag/
Disallow: /zh-hk/tag/

Also enforce hard blocking at the nginx layer (since robots.txt is merely advisory):

# /www/server/panel/vhost/nginx/extension/wpfanyi.com/bot_block.conf
if ($http_user_agent ~* "(MJ12bot|Bytespider|GPTBot|OAI-SearchBot|SemrushBot|DotBot|AhrefsBot|PetalBot|ClaudeBot)") {
    return 403;
}

Step 2: Enable nginx fastcgi_cache (Fundamental Fix)

Define a shared cache zone in the nginx http block:

# 0.wpfanyi_fcgi_cache.conf (in http block)
fastcgi_cache_path /path/to/nginx-cache levels=1:2
  keys_zone=wpfanyi_cache:100m max_size=2g inactive=60m use_temp_path=off;

Enable caching in the server block — critically ignoring Set-Cookie headers from wp-chinese-converter:

set $skip_cache 0;

if ($request_method = POST) { set $skip_cache 1; }
if ($arg_s) { set $skip_cache 1; }
if ($http_cookie ~* "wordpress_logged_in_") { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/wp-login.php|/wp-json/") { set $skip_cache 1; }

fastcgi_cache wpfanyi_cache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 60m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_lock on;

# Critical: Ignore Set-Cookie to allow caching despite wp-chinese-converter
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
fastcgi_hide_header Set-Cookie;

add_header X-FastCGI-Cache $upstream_cache_status;

Step 3: OPcache-Powered File Caching for OpenCC Dictionaries

Modify get_prepared_dictionaries() to persist sorted dictionaries as PHP files — loaded via include and accelerated by OPcache:

private function get_prepared_dictionaries(string $strategy): array {
    if (isset($this->prepared[$strategy])) {
        return $this->prepared[$strategy];
    }

    // Try loading pre-sorted dictionary from cached PHP file (OPcache-accelerated)
    $cache_file = WP_CONTENT_DIR . "/cache/wpcc-dictionaries/dict_" . md5($strategy) . ".php";
    if (file_exists($cache_file)) {
        $prepared = include $cache_file;
        if (is_array($prepared) && !empty($prepared)) {
            $this->prepared[$strategy] = $prepared;
            return $prepared;
        }
    }

    // First-time: sort and write to cache file
    $sets = Dictionary::get($strategy);
    // ... unchanged uksort logic ...

    // Write sorted array as executable PHP file; OPcache auto-caches bytecode
    $export = "<?php\nreturn " . var_export($prepared, true) . ";\n";
    $tmp = $cache_file . ".tmp." . getmypid();
    if (@file_put_contents($tmp, $export, LOCK_EX) !== false) {
        @rename($tmp, $cache_file);
        if (function_exists("opcache_compile_file")) {
            @opcache_compile_file($cache_file);
        }
    }
    return $prepared;
}

Step 4: Restrict Cavalcade CPU Usage

Limit Cavalcade’s resource consumption via systemd:

# /etc/systemd/system/cavalcade.service
[Service]
Nice=19
CPUQuota=200%
CPUWeight=50

Results After Optimization

Metric Before After
Server Load 34.87 3.14
Homepage TTFB 5.7 s 60 ms
/zh-tw/ TTFB 45 s 66 ms
/zh-hk/ TTFB 6 s 65 ms
PHP-FPM Workers 22 @ >80% CPU 10 @ 19% CPU

Key Lessons Learned

  1. wp-chinese-converter / OpenCC is a CPU killer — Sorting and replacing across 50,000+ dictionary entries on every traditional-Chinese page request is prohibitively expensive. Persistent caching (e.g., OPcache-backed file cache) is mandatory.

  2. Set-Cookie silently breaks HTTP caching — Plugins that unconditionally set cookies prevent fastcgi_cache from storing responses. Explicitly ignore Set-Cookie via fastcgi_ignore_headers.

  3. Translation sites without robots.txt attract relentless crawlers — Predictable URL patterns like /zh-tw/tag/xxx/page/N generate infinite crawlable pages. Always deploy strict robots.txt and nginx-level bot blocking.

  4. Cavalcade + Action Scheduler can spiral out of control — Zombie background tasks + concurrent heavy operations (e.g., WP-Rocket’s RUCSS/preload across 67 sites) cause CPU saturation. Enforce CPU quotas and monitor task queues.

  5. fastcgi_cache is the most effective frontend optimization for multisite WordPress — One well-configured cache layer resolves >99% of visitor-facing performance issues. Simple to implement, immediate impact.

  6. Object Cache Pro’s metrics snapshots can bloat — The objectcache_snapshots entry in wp_sitemeta may grow to hundreds of KB, causing multi-second write latency. Schedule regular cleanup.

核心优化策略分析

你的优化方案非常全面,特别是对 wp-chinese-converter 插件性能瓶颈的定位很准确。让我补充几个 WordPress 层面的优化建议:

1. 替代 wp-chinese-converter 的方案

既然这是翻译平台,可以考虑更轻量的繁简转换方案:

// 在主题的 functions.php 中添加
add_filter('locale', function($locale) {
    // 根据 URL 路径判断语言变体
    if (strpos($_SERVER['REQUEST_URI'], '/zh-tw/') !== false) {
        return 'zh_TW';
    }
    if (strpos($_SERVER['REQUEST_URI'], '/zh-hk/') !== false) {
        return 'zh_HK';
    }
    return $locale;
});

// 使用 WordPress 内置的翻译文件,避免实时转换
load_textdomain('default', WP_LANG_DIR . '/zh_TW.mo');

2. 优化 Cavalcade 和 Action Scheduler

对于多站点环境,Action Scheduler 任务需要更精细的控制:

// 在 wp-config.php 中添加
define('ACTION_SCHEDULER_LOCK_TIMEOUT', 60); // 减少锁超时时间
define('ACTION_SCHEDULER_QUEUE_RUNNER_CONCURRENT_BATCHES', 1); // 限制并发批次
define('ACTION_SCHEDULER_GROUP_CONCURRENCY', 1); // 限制组并发

// 清理僵尸任务
add_action('init', function() {
    global $wpdb;
    // 清理超过24小时的失败任务
    $wpdb->query("
        DELETE FROM {$wpdb->prefix}actionscheduler_actions 
        WHERE status = 'failed' 
        AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 24 HOUR)
    ");
});

3. 优化 Object Cache Pro 配置

// wp-config.php 中的 Redis 配置优化
define('WP_REDIS_CONFIG', [
    'token' => 'your_token',
    'host' => '127.0.0.1',
    'port' => 6379,
    'database' => 0,
    'timeout' => 1.0,
    'read_timeout' => 1.0,
    'retry_interval' => 100,
    'compression' => 'zstd', // 使用 zstd 压缩
    'serializer' => 'igbinary', // 更快的序列化
    'async_flush' => true, // 异步刷新
    'split_alloptions' => true, // 分割 alloptions
    'prefetch' => false, // 多站点关闭预取
    'debug' => false,
    'save_commands' => false, // 关闭命令记录
    'prefix' => 'wpfanyi:', // 自定义前缀
]);

4. WordPress 多站点查询优化

// 减少跨站点查询
add_filter('ms_site_check', '__return_false'); // 禁用站点检查
add_filter('site_option__site_transient_timeout_available_translations', '__return_zero'); // 禁用翻译更新检查

// 优化 GlotPress 查询
add_filter('gp_before_get_translation_sets', function($query) {
    $query['no_found_rows'] = true; // 禁用分页计数
    $query['update_post_meta_cache'] = false;
    $query['update_post_term_cache'] = false;
    return $query;
});

5. 数据库索引优化

检查并添加必要的索引:

-- 为 wp_posts 表添加索引
ALTER TABLE wp_posts ADD INDEX idx_post_type_status_date (post_type, post_status, post_date);
ALTER TABLE wp_posts ADD INDEX idx_post_parent (post_parent);

-- 为 wp_term_relationships 表添加索引
ALTER TABLE wp_term_relationships ADD INDEX idx_term_taxonomy_id (term_taxonomy_id);

-- 清理 objectcache_snapshots
DELETE FROM wp_sitemeta WHERE meta_key LIKE 'objectcache_snapshots%';

6. PHP-FPM 配置优化

; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000 ; 防止内存泄漏
request_terminate_timeout = 30s ; 超时时间

7. 监控和告警

建议添加性能监控:

// 在 mu-plugins 中添加性能监控
add_action('shutdown', function() {
    if (defined('SAVEQUERIES') && SAVEQUERIES) {
        $queries = get_num_queries();
        $time = timer_stop(0, 3);
        if ($queries > 100 || $time > 1.0) {
            error_log(sprintf(
                'Slow request: %s - %d queries in %s seconds',
                $_SERVER['REQUEST_URI'],
                $queries,
                $time
            ));
        }
    }
});

你的优化方案已经覆盖了主要瓶颈,这些补充建议可以帮助进一步提升 WordPress 层面的性能。特别是对于多站点环境,减少跨站点查询和优化数据库索引会有明显效果。