Production Tuning
Four switches. Measurable differences. No guesswork.
PerfLocale is light out of the box. On a site with no object cache and no tuning at all, its own work on a page is a handful of queries — 16 on the home page of our WooCommerce test site, and 2 once an object cache is in front of it. The settings below reduce what the whole page costs, your theme, WooCommerce and every other plugin included, in order of impact.
What to expect
Measured on a copy of our WooCommerce test site — a block theme, three languages, PerfLocale 1.0.6 on PHP 8.4 and MySQL 8.0. Each page was requested five times per column and the count was the same every time. The second column adds Redis as the object cache; any persistent object cache does the same job. Your theme and your other plugins bring queries of their own, so read this as the shape of the numbers rather than as targets for your site.
The figures below are PerfLocale’s own queries, with the whole page’s total in brackets. Out of the box means exactly that: the plugin installed, three languages active, nothing else tuned.
| Request | Out of the box PerfLocale (whole page) | With an object cache PerfLocale (whole page) |
|---|---|---|
| Home page | 16 (93) | 2 (5) |
Localised home (/de/) | 19 (82) | 4 (7) |
Category archive (/de/) | 20 (92) | 4 (6) |
Product page (/de/) | 19 (95) | 4 (7) |
Bold is PerfLocale’s own work; the bracketed figure is every query on the page, WordPress, theme and all other plugins included. On this site PerfLocale accounts for about a fifth of an uncached page, and the rest of the page is what an object cache mostly removes.
Paginated archives get an extra optimization for free. On language-filtered front-end archive queries PerfLocale suppresses WordPress core’s deprecated SQL_CALC_FOUND_ROWS — which, combined with the language JOIN, forces MySQL to scan every matching row (ignoring LIMIT) just to count them — and supplies the pagination count from a generationally-cached COUNT instead. It’s on by default and needs no configuration; to restore core’s behaviour, add_filter( 'perflocale/query/optimize_found_rows', '__return_false' ).
1. Persistent object cache
The single biggest win. In the table above it takes a page from roughly 80–100 queries to under 10. Every WordPress site benefits; multilingual sites benefit most because PerfLocale reads small data structures (language list, translation links, slug translations) on every request — those reads are mostly served from the object cache instead of the database.
Redis (recommended)
# Install Redis + the PHP extension
sudo apt install php8.3-redis redis-server
sudo systemctl enable --now redis-server
# Install & activate the WordPress plugin
wp plugin install redis-cache --activate
wp redis enableVerify:
wp eval 'echo wp_using_ext_object_cache() ? "YES" : "NO";'
# → YESAlternatives
- Memcached - simpler, functionally equivalent for this workload. Install the Memcached drop-in.
- Object Cache Pro - commercial, tighter WooCommerce integration. Often bundled with managed hosts (Kinsta, Pressable, Cloudways).
2. PHP OPcache
Caches compiled PHP bytecode. Bundled with PHP 5.5+, almost always available, but defaults are too small for WordPress - hot files get evicted. Give it room:
# /etc/php/8.3/fpm/php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.interned_strings_buffer=32
opcache.validate_timestamps=0 ; production only - requires fpm reload on deploy
opcache.save_comments=1 ; required for WordPressVerify:
wp eval 'print_r( opcache_get_status( false )["opcache_statistics"] );'
# Look for "opcache_hit_rate" — should be > 99% on a warm pool.validate_timestamps=0 means OPcache won’t notice file edits. systemctl reload php-fpm after every deploy. Don’t set this on a dev machine.
3. CDN edge caching
The fastest request is the one PHP never sees. With a CDN in front, most visitors get HTML from the edge. PerfLocale ships two features that make edge caching work correctly on multilingual sites:
Cache-Tag headers
Enable at Settings → Advanced → CDN Cache-Tag Headers. PerfLocale emits a tag on every response:
Cache-Tag: perflocale,lang:fr_FR,lang-slug:fr,post:42,post-type:postYour CDN (Cloudflare Enterprise, Bunny, Fastly, KeyCDN…) can then purge surgically - flush everything tagged lang:fr_FR, or just post:42, without nuking the whole cache. Full reference: Cache-Tag Headers.
Edge language detection
On Cloudflare Workers, Vercel Edge, or Netlify Edge, you can resolve the visitor’s language at the edge and include it in the cache key. Result: / caches separately per language, zero round-trips to WordPress for language detection. Full guide: Edge Integration.
One thing to avoid: Vary: Accept-Language. Shreds hit rate - every distinct browser Accept-Language header produces a separate cache entry. PerfLocale deliberately does not emit this header. Use URL-based routing (/de/, subdomain, or per-domain) or edge-hint headers instead.
4. String-translation mode
Under Settings → Performance, choose how UI string translations are stored:
| Mode | Speed | When to use |
|---|---|---|
| Files (default) | No database read for UI strings | Default. Translations compiled into .l10n.php files - zero DB cost per string lookup. |
| Database | Slightly slower, zero filesystem writes | Read-only filesystems (some container / serverless WP deployments), or permission issues with the translations directory. |
For normal production servers, keep the default.
5. Deny web access to the export directory
Not a speed setting, but it belongs in the same pass. Exports land in wp-content/uploads/perflocale/exports/, which is inside the web root. PerfLocale writes a Deny from all .htaccess there, and nginx and Caddy ignore .htaccess, so on those servers you need a real rule:
location ~* /wp-content/uploads/perflocale/exports/ {
deny all;
return 404;
}Tools → Site Health checks this for you by writing a temporary random file and requesting it over HTTP. If it comes back, you get a critical result with the snippet. See Security for the full explanation.
Measuring your own numbers
Drop this into wp-content/mu-plugins/perf-log.php for a tuning session. Remove it when done.
<?php
if ( ! defined( 'SAVEQUERIES' ) ) define( 'SAVEQUERIES', true );
add_action( 'shutdown', function () {
global $wpdb;
$total = 0;
foreach ( $wpdb->queries ?? [] as $q ) $total += (float) ( $q[1] ?? 0 );
file_put_contents(
WP_CONTENT_DIR . '/perf.log',
sprintf( "%-40s queries=%3d time=%6.2fms\n",
$_SERVER['REQUEST_URI'] ?? '?',
count( $wpdb->queries ?? [] ),
$total * 1000 ),
FILE_APPEND
);
}, 9999 );curl -s -o /dev/null https://example.com/
curl -s -o /dev/null https://example.com/de/
tail /wp-content/perf.logRun each URL twice - the second run is the warm-cache number that matters. If your query counts are far above the table above, one of the four layers isn’t doing its job. Check them in order: object cache, OPcache, CDN.
Related
- CDN Cache-Tag Headers - per-language CDN purging
- Edge Integration - Cloudflare Workers / Vercel / Netlify
- Performance feature page - the architectural choices that make PerfLocale fast by default