RT Cunningham

WordPress Speed on Nginx – The Best Option is Not a Caching Plugin

WordPress Speed

For WordPress speed using Nginx as the web server, a caching plugin isn’t the best option. If you’re using Apache as your web server, Nginx gives you more than a couple of good reasons to switch over. The Nginx FastCGI module is just one of them.

I ignored the FastCGI option for a long time, a mistake I recently corrected. Setting it all up was incredibly easy.

WordPress Speed Tutorials

There are lots of tutorials out there if you search for them. Unfortunately, most of them are either outdated or just plain wrong. The rest of them give you details you don’t really care to know. I’m going to save you some trouble and give you the details you need to know and nothing more. I have the FastCGI routines set up on my web server and everything is running flawlessly.

Some tutorials offer cache purging routines. They aren’t necessary if you use sane options while configuring everything. The cache timeout, for example, shouldn’t be too long. Even a 10-minute cache will work in most cases. You can cache everything all at once with a PHP script I’ve written and refresh it every so often if you wish to go that route.

My Server Block

Just above my server block, I have this:

fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=www:100m inactive=65m;
fastcgi_cache_key "$scheme://$host$request_uri";

The zone is “www”. You can use almost anything as long as it’s alphanumeric. “100m” means make 100 megabytes of space available to the cache. “65m” means 65 minutes before any URL becomes inactive.

The relevant lines within my server block:

set $no_cache 0;
if ($request_method = POST) {
set $no_cache 1;
}
if ($request_uri ~ (^/wp-|\.php)) {
set $no_cache 1;
}
if ($cookie_wordpress_logged_in) {
set $no_cache 1;
}
if ($cookie_commment_) {
set $no_cache 1;
}
if ($host = 138.68.2.174) { # the web server IP address
set $no_cache 1;
}
location / {
try_files $uri $uri/ /index.php?$args;
. . .
}
location ~ \.php {
. . .
fastcgi_cache www;
fastcgi_cache_valid 200 65m;
fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;
}

While some tutorials say to use “/index.php” as the fallback for the main location block, the new WordPress block editor will not work properly unless you use “/index.php?$args”.

In the fastcgi_cache_valid line, “200” refers to the response code. Caching other response codes doesn’t make sense since no pages get retrieved with them. “65m” refers to how long to cache the response code.

You can find out everything you want to know by reading the ngx_http_fastcgi_module documentation if you want to invest the time.

Caching and Recaching Scripts

I’ve written two scripts, based on the scripts I wrote for my static site generator and my semi static WordPress routines. The first is for caching all the posts and the second is for caching newly published posts or edited posts. The second script goes hand in hand with two theme functions, which I’ll also provide.

I have the cache timeout and valid setting at 65 minutes. The main cache script runs once per hour, removing all posts before caching them. This makes sure that every valid page is always cached:

<?php
$path = '/path/to/wordpress/root';
$user_agent = 'RTCX';
$real_index_page = 'rt-cunningham';
$cache_path = '/etc/nginx/cache/';
#
Load WordPress and URLs
#
include $path . '/wp-load.php';
$site_url = home_url('/');
$queue[] = $site_url;
$posts = get_posts(array('numberposts' => -1, 'orderby' => 'modified', 'post_type' => array('post','page'), 'post_status' => 'publish'));
foreach ($posts as $post) {
if ($post->post_name == $real_index_page) continue;
$queue[] = $site_url . $post->post_name;
}
#
Load Pages
#
$context = stream_context_create(array('http'=>array('method'=>"GET", 'ignore_errors' => true, 'header'=>"User-Agent: " . $user_agent . "\r\n")));
foreach($queue as $url) {
$hash = md5($url);
$file = $cache_path . substr($hash, -1) . '/' . substr($hash, -3, 2) . '/' . $hash;
if (file_exists($file)) {
unlink ($file);
}
$a = file_get_contents($url, false, $context);
}

The user agent can be whatever you want it to be. It’s for your access log. The “real index page” is for the page you use as the front page, if that’s how you’re set up. I am.

The second script runs once per minute, checking for files in the /temp directory next to the WordPress root directory. If no files are there, it exits without doing anything at all:

<?php
$path = '/path/to/wordpress/root';
$user_agent = 'RTCX';
$cache_path = '/etc/nginx/cache/';
#
$test = glob($path . "/temp/*");
if (!isset($test[0])) exit;
#
Load WordPress and URLs
#
include $path . '/wp-load.php';
$site_url = home_url('/');
#
Queue File Names
#
foreach (glob($path . "/temp/*") as $file) {
$file = str_replace($path . '/temp/', '', $file);
if ($file == 'index.html') $file = '';
$queue[] = $site_url . $file;
}
#
Load Pages
#
$context = stream_context_create(array('http'=>array('method'=>"GET", 'ignore_errors' => true, 'header'=>"User-Agent: " . $user_agent . "\r\n")));
foreach($queue as $url) {
$hash = md5($url);
$file = $cache_path . substr($hash, -1) . '/' . substr($hash, -3, 2) . '/' . $hash;
if (file_exists($file)) {
unlink ($file);
}
$a = file_get_contents($url, false, $context);
}
#
Remove Files
#
foreach (glob($path . "/temp/*") as $file) {
unlink($file);
}

These are the two functions to be added to the functions.php file for your theme:

#
When a Post is Published
#
add_action('transition_post_status', 'on_publish', 10, 3);
function on_publish($new_status, $old_status, $post) {
if ($old_status != 'publish' && $new_status == 'publish') {
$path = '/path/to/wordpress/root/temp/';
touch($path . $post->post_name);
}
}
#
When a Post is Edited
#
add_action('edit_post', 'on_edit', 10, 2);
function on_edit($post_id, $post) {
$status = get_post_status($post_id);
if ($status == 'publish') {
$path = '/path/to/wordpress/root/temp/';
if ($post->post_name == 'rt-cunningham') {
$post->post_name = 'index.html';
}
touch($path . $post->post_name);
}
}

The “rt-cunningham” line is for the page you use for a static home page. It’s not needed otherwise. The “index.html” is merely a placeholder for your home page.

So… when I edit a page, or publish a new page, the second script sees the file name in the temp directory and caches or recaches the correct page in a minute. Once an hour, the main script recaches everything.

These take care of the first page load dilemma. No visitors, human or bot, will get a page that hasn’t been cached already. For WordPress speed, you can’t get any faster than that.

Share:    

RT Cunningham
May 6, 2019
Web Development