Reducing Wordpress, PHP, MySQL Memory Usage

BACKGROUND


I hate WordPress, but I also love WordPress. It is still the most user-friendly open source blogging platform in existence. I have tried many alternatives, I've even written my own, but the feature set has always felt lacking.

My main problem with WordPress used to be the codebase. It was a mess, but the devs have managed to clean it up quite a bit. The current version seems quite tidy as far as PHP projects go, and hacking on it isn't nearly as frustrating as it had been in earlier versions.

So now my big beef is with performance. PHP and MySQL are slow, gobble up tons of resources even when sitting idle, and WordPress' extensive feature set dumps a ton of bloat on top.

At the end of the day, I just want users to load my dumb blog as fast as possible and have WordPress leave as small of a footprint as possible so I can save my server resources for more interesting projects.


HOW TO REDUCE MYSQL MEMORY USAGE


The first thing I notice when checking my system memory is MySQL. Why are you eating half of my memory, MySQL? You're not doing anything. Just give it back.

MySQL loves to cache. It increases query speed tremendously to store your entire database in memory, but that's not my goal. Once I'm done writing my post, I won't need MySQL anymore, because I will be generating a static version of my blog for public viewing. Nothing serves faster than static files, and I intend my blog to be read only for the general public.

My end goal now is to dial down MySQL's memory usage as far as possible while still being able to use the Wordpress admin panel without noticeable lag.

So open up your MySQL config (mine was at /etc/mysql/mysql.conf.d/mysqld.cnf) and start tweaking some settings.

The biggest single improvement that I was able to make was to turn off MySQL's Performance Schema by adding this line:

performance_schema = 0

I also found a huge list of variables here and then proceeded to completely ignore the "minimum" values:

key_buffer_size = 8
max_allowed_packet = 1K
max_connections = 10
query_cache_limit = 0
query_cache_size = 0
expire_logs_days = 1
max_binlog_size = 1K
thread_stack = 1
thread_cache_size = 0
host_cache_size = 0
innodb_buffer_pool_size = 0
innodb_log_buffer_size = 0
innodb_ft_cache_size = 0
innodb_ft_total_cache_size = 0
sort_buffer_size = 0
read_buffer_size = 0
read_rnd_buffer_size = 0
max_heap_table_size = 0
tmp_table_size = 0
bulk_insert_buffer_size = 0
join_buffer_size = 0
net_buffer_length = 0
innodb_sort_buffer_size = 0
binlog_cache_size = 0
binlog_stmt_cache_size = 0

Now just save and restart MySQL.

I'm amazed that WordPress can still function at all with these settings, but honestly I don't even notice the difference when clicking around the admin panel. But what I did notice what that I had slashed MySQL's memory usage by 90%.


HOW TO REDUCE PHP MEMORY USAGE


Just like MySQL, I will only need PHP to write posts, not to serve them. Again, I will be minimizing PHP's memory usage by modifying configuration files.

PHP's biggest issue is that, by default, it spawns multiple processes to handle incoming requests, and then refuses to kill them once the requests have been handled. These processes, even when sitting idle, take up way too much of my precious memory.

So most of the work can be done in the pool config. For my system, that file is at /etc/php/7.0/fpm/pool.d/www.conf

pm = ondemand
pm.process_idle_timeout = 1s

This configuration tells PHP to not create any listener processes until a request is actually made. If a process does nothing for more than 1 second, it will be killed. Humanely. (PETA plz no bully.)

So PHP can now eat all the memory it wants when I'm actually using it (almost never) and will give it back immediately after. If you are worried about peak memory usage, you can set 

pm.max_children = 1

Other than than you can't do much. Limiting a PHP thread's max memory size will result in errors. You are essentially at the mercy of WordPress' dev team.


HOW TO REDUCE WORDPRESS MEMORY USAGE


All of this is has been a lead up to the main objective, building WordPress into a static website. Static websites are faster, more stable, and more secure. They also require very little memory to serve.

If you don't mind manually generating your public-facing WordPress site, I would recommend installing the Simply Static WordPress Plugin, and just use that.

If you're lazy, I'll share how I was able to trigger a static site generation every time a post is published or updated.

The first step is to create a shell script to clone your WordPress site into a public directory. I called mine static.sh

# Download your dynamic wordpress site into /tmp
wget --trust-server-names --no-clobber --mirror --convert-links --adjust-extension -e robots=off -P /tmp http://privateblog.com

# Search for any mention of your private domain and replace it with your public domain
find /tmp/privateblog.com -type f -print0 | xargs -0 sed -i 's/privateblog\.com/publicblog\.com/g'

# Manually copy your uploads directory to make sure you have all the images sizes that wget ignored
cp -r /var/www/privateblog.com/wp-content/uploads/* /tmp/privateblog.com/wp-content/uploads/

# Move the result into your public www directory

rm -rf /var/www/publicblog/*
mv /tmp/privateblog.com/* /var/www/publicblog/

Now we have a bash script that needs to run whenever we update our blog. Luckily WordPress makes this easy by shipping with XML-RPC functionality.

Since XML-RPC uses HTTP as a transport protocol, we don't need any extra technology to detect a change; we can just use PHP. Of course, if you wanted to analyze the XML payload, you would need to set up an actual XML-RPC server, but that's not necessary here.

So now we just need a PHP script added to our WordPress directory which calls static.sh. I called mine rpc.php

<?php passthru('./static.sh'); ?>


Nice. The last piece of this is to add the URL of rpc.php to our WordPress Update Services. In the dashboard, go to Settings > Writing and find Update Services down at the bottom. Add your URL to the box, and hit Save Change.


That should do the trick. If you run into problems, double check your file permissions and ownership. Keep in mind that the www-data user needs to be able to execute both scripts and have write access to your public directory.