WordPress itself generates lean HTML, but every uncached page load triggers PHP, MySQL, and file I/O. The web server layer — Apache or Nginx — can eliminate significant latency before PHP even starts, by serving compressed responses and instructing browsers to cache static assets locally. For Apache-based hosting (the vast majority of shared and managed WordPress hosts), these optimisations live in .htaccess. Enabling gzip or Brotli compression for text assets, setting long-lived Expires and Cache-Control headers for versioned static files, and connecting asset versioning to WordPress’s built-in cache busters are three independent performance improvements that each reduce page weight and round-trip count. Together they typically cut time-to-interactive by 30–50 % on a production site with no caching plugin installed.
Problem: Google PageSpeed Insights flags "Enable text compression" and "Serve static assets with an efficient cache policy" on a shared Apache host. The site scores 45 on mobile. Plugins for caching are not an option due to hosting restrictions.
Solution: Add gzip compression via mod_deflate, long-lived Expires headers via mod_expires, and cache-control headers via mod_headers to .htaccess. Use WordPress's wp_enqueue_style() / wp_enqueue_script() version parameter to bust browser caches on asset updates.
# .htaccess — add AFTER the WordPress rewrite block
# ── 1. Gzip compression (mod_deflate) ────────────────────────────────
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml
AddOutputFilterByType DEFLATE image/svg+xml font/woff2 font/woff
# Don't compress already-compressed formats
SetEnvIfNoCase Request_URI \.(gz|br|zip|png|jpg|webp|mp4)$ no-gzip
</IfModule>
# ── 2. Browser caching — Expires headers (mod_expires) ───────────────
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 month"
# HTML — short TTL so changes propagate quickly
ExpiresByType text/html "access plus 0 seconds"
# CSS + JS — long TTL; use versioned query strings in WordPress
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Fonts
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
# Images
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/webp "access plus 6 months"
ExpiresByType image/svg+xml "access plus 6 months"
ExpiresByType image/x-icon "access plus 1 year"
</IfModule>
# ── 3. Cache-Control headers (mod_headers) ───────────────────────────
<IfModule mod_headers.c>
# Prevent caching of dynamic WordPress pages
<FilesMatch "\.(php)$">
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header set Pragma "no-cache"
</FilesMatch>
# Long cache for versioned static assets
<FilesMatch "\.(css|js|woff2?|ttf|eot|svg|ico|png|jpe?g|webp|gif)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# Security headers (bonus)
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
<?php
// ── WordPress side: version your assets so caches bust on update ──────
// Use plugin/theme version or filemtime() as the version argument
add_action( 'wp_enqueue_scripts', function () {
// File modification time — updates automatically when file changes
$css_ver = filemtime( get_stylesheet_directory() . '/assets/style.css' );
$js_ver = filemtime( get_stylesheet_directory() . '/assets/app.js' );
wp_enqueue_style(
'my-theme-style',
get_stylesheet_directory_uri() . '/assets/style.css',
[],
$css_ver // browser sees ?ver=1710000000 and re-fetches on change
);
wp_enqueue_script(
'my-theme-app',
get_stylesheet_directory_uri() . '/assets/app.js',
[ 'jquery' ],
$js_ver,
true
);
} );
NOTE: The Expires and Cache-Control: max-age=31536000 headers only work safely for static assets because WordPress appends a ?ver= query string to every enqueued script and style. When the version changes, the browser treats it as a new URL and ignores the cached copy. Never set long-lived cache headers on .php files or WordPress's main URL — doing so causes users to see stale HTML after you publish new posts. Test your configuration with curl -I https://example.com/wp-content/themes/my-theme/style.css and verify the response headers before deploying.