How to Speed Up a WordPress Website
Updated by Linode Written by Nathan Melehan
Goals For This Guide
This guide outlines two primary steps to lowering the response time of a WordPress website:
Analyze and identify performance bottlenecks.
Implement best practices for site optimization.
How This Guide is Structured
This guide provides a testing environment which will be used to illustrate the process of optimizing WordPress. The environment has two components:
A Docker Compose file is provided which sets up a WordPress installation. This installation is intentionally pre-configured with poorly performing customizations. When the site is initially loaded in a web browser, it will take upwards of 15-20 seconds to appear.
This test environment was designed to highlight the kinds of performance bottlenecks that can appear in some plugins or themes: high CPU usage, high memory usage, slow SQL queries, and slow JavaScript. This guide will focus on troubleshooting the test site by removing those customizations until the response time for requests is minimized.
The Docker Compose file also installs a PHP profiling tool which collects performance data for each request to the website. This data is pushed to a Mongo database that the second Docker Compose file provisions.
A second Docker Compose file is responsible for the Mongo database that collects profiling data from the site. This second Compose file also runs a visualization app for viewing that profiling data. This tool will be used to identify the slow customizations that were installed.
These two Compose files are decoupled. This decoupling allows you to separately install the second Compose file on a server running your own WordPress site instead of the test site after you finish reading the guide.
Setting Up the Test Environment
You can use this guide without installing the environment, but working through the steps presented may help you better understand the process. If you would prefer to not perform this work, skip to the Profiling the Application section.
To install the test environment, you will need a Linode which does not have any running processes already bound to ports 80
, 8080
, 3306
, and 27017
.
Install Docker
These steps install Docker Community Edition (CE) using the official Ubuntu repositories. To install on another distribution, or to install on Mac or Windows, see the official installation page.
Remove any older installations of Docker that may be on your system:
sudo apt remove docker docker-engine docker.io
Make sure you have the necessary packages to allow the use of Docker’s repository:
sudo apt install apt-transport-https ca-certificates curl software-properties-common gnupg
Add Docker’s GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
Verify the fingerprint of the GPG key:
sudo apt-key fingerprint 0EBFCD88
You should see output similar to the following:
pub rsa4096 2017-02-22 [SCEA] 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88 uid [ unknown] Docker Release (CE deb)
sub rsa4096 2017-02-22 [S] Add the
stable
Docker repository:sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
Note
For Ubuntu 19.04, if you get an
E: Package 'docker-ce' has no installation candidate
error, this is because the stable version of docker is not yet available. Therefore, you will need to use the edge / test repository.sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable edge test"
Update your package index and install Docker CE:
sudo apt update sudo apt install docker-ce
Add your limited Linux user account to the
docker
group:sudo usermod -aG docker $USER
Note
After entering theusermod
command, you will need to close your SSH session and open a new one for this change to take effect.Check that the installation was successful by running the built-in “Hello World” program:
docker run hello-world
Install Docker Compose
Download the latest version of Docker Compose. Check the releases page and replace
1.25.4
in the command below with the version tagged as Latest release:sudo curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
Set file permissions:
sudo chmod +x /usr/local/bin/docker-compose
Download the Test Environment
Run these commands from your Linode:
Download the test environment Tar file:
wget https://raw.githubusercontent.com/linode/docs-scripts/master/hosted_scripts/how-to-speed-up-a-wordpress-website/speed-up-wp-test-env.tar
Unpack the file:
tar -xvf speed-up-wp-test-env.tar
Start the WordPress Test Site
Change to the directory containing the Compose file:
cd speed-up-wp-test-env/xhprof_host_net_wordpress/
Run the Compose file in a detached state:
docker-compose up -d
Verify that you can view the site by loading
http://<your-Linode-IP-address>
in a web browser. This should take 15-20 seconds to load.Open a Bash shell on the WordPress Docker container:
docker exec -it xhprof_host_net_wordpress_wordpress_1 bash
Set the password for the WordPress admin user via the WordPress CLI:
wp --path='/var/www/html' --allow-root user update wpuser --user_pass='your_new_wordpress_login_password'
Verify that you can login to the WordPress admin site at
http://<your-Linode-IP-address>/wp-admin
with the password you’ve set. Enterwpuser
for the admin user name.Exit the Bash shell in the WordPress container:
exit
Start the PHP Profile Visualization Application
Change to the directory containing the Compose file:
cd ../xhgui_app/
Run the Compose file in a detached state:
docker-compose up -d
Verify that you can view the visualization app by loading
http://<your-Linode-IP-address>:8080
in a web browser:
Profiling the Application
The profiling visualization app used in this guide is named XHGUI. This app provides bar charts, line plots, flamegraphs, and other representations of the raw profiling data collected for the test WordPress site.
The profiling data is collected by a tool called XHProf. The original XHProf distribution does not support PHP 7 (which the test WordPress site runs on), so this guide uses a PHP 7-compatible fork of XHProf called Tideways. The data generated by Tideways will be stored in a Mongo database, and XHGUI will read from this database.
Test Response Time
Run this curl
command from your home computer to test the site speed prior to troubleshooting:
time curl http://<your-Linode-IP-address> -s 1>/dev/null
12.79 real
0.01 user
0.02 sys
The time displayed can vary between requests. If you do not have curl
installed on your computer, you can use an in-browser speed test like Google PageSpeed Insights.
View the Profiling Data
Visit XHGUI in your browser on port
8080
:http://<your-Linode-IP-address>:8080
. A page which lists entries for your recent requests should appear:Click the timestamp link for the most recent
GET
request on/
. A page will appear which shows detailed information for that request, including bar charts of the highest CPU usage (referred to as Wall Time) and memory usage ordered by function. This page also has links to other useful visualizations for the request, like the Flamegraph:
Investigate CPU Usage: Pi_Widget::calculatePi
Under the bar chart for CPU usage, the first item listed is labeled Pi_Widget::calculatePi
. This function had a wall time of 5,693,944 µs
, or about 5.7 seconds. To find the code responsible for this function call, execute these commands from the Linode:
Open a Bash shell on the WordPress Docker container:
docker exec -it xhprof_host_net_wordpress_wordpress_1 bash
From the document root of the container, search for the function name:
root@localhost:/var/www/html# grep -R calculatePi .
./wp-content/plugins/pi_widget/pi_widget.php: public function calculatePi() { ./wp-content/plugins/pi_widget/pi_widget.php: echo $this->calculatePi();
We can see that a plugin (named Pi Widget) is responsible for this call, though we may have guessed that from the XHGUI view. Further examination of the plugin shows that it calculates a value for Pi using 100 billion iterations of an approximation algorithm.
Deactivate this plugin from the Plugins section of your site’s WordPress admin page.
Re-test the site response time from your home computer:
time curl http://<your-Linode-IP-address> -s 1>/dev/null
5.96 real 0.01 user 0.02 sys
Investigate CPU Usage: mysqli_query
The next highest CPU usage function call displayed by XHGUI was labeled mysqli_query
. This is the PHP-MySQL interface that WordPress uses to run database queries.
This name is too generic for us to search the WordPress code base for the cause of the query. To continue troubleshooting:
Install the Query Monitor WordPress plugin, which will show the individual queries that WordPress runs. Go to the Plugins section of your site’s WordPress admin page, click the
Add New
button at the top, and then search forQuery Monitor
. Be sure to activate the plugin once you install it.Re-load the WordPress site in your browser. In the admin menu bar at the top you will see an orange-highlighted collection of site statistics. Hovering over this highlight will show a drop-down menu, and clicking on the Slow Queries option will bring up a list of slow queries:
The slow query that was found is the following statement:
SELECT SLEEP(5)
. This simply sleeps the database for 5 seconds without taking any other action.The Queries by Component section of Query Monitor will sometimes show the name of a plugin that was responsible for the query. That is not the case for this query, so you will search the code base for where it appears. Run this command from the Bash shell you already opened on the WordPress Docker container:
root@localhost:/var/www/html# grep -R 'SELECT SLEEP' .
./wp-content/plugins/slow_query_test/slow_query_test.php: $wpdb->query($wpdb->prepare ( "SELECT SLEEP(%d)", array(5) ));
This reveals that the Slow Query Test plugin is responsible. Deactivate this plugin from the WordPress admin page.
Re-test the site response time from your home computer:
time curl http://<your-Linode-IP-address> -s 1>/dev/null
0.96 real 0.00 user 0.02 sys
Investigate Memory Usage: openssl_random_pseudo_bytes
XHGUI showed that a function named openssl_random_pseudo_bytes was responsible for allocating 30 MB of memory. Searching the code base reveals that the High Memory Test plugin is responsible:
root@localhost:/var/www/html# grep -R openssl_random_pseudo_bytes .
./wp-content/plugins/high_memory_test/high_memory_test.php: $data = openssl_random_pseudo_bytes(1000000);
./wp-includes/random_compat/random_bytes_openssl.php: * Since openssl_random_pseudo_bytes() uses openssl's
./wp-includes/random_compat/random_bytes_openssl.php: $buf = openssl_random_pseudo_bytes($bytes, $secure);
./wp-includes/random_compat/random_bytes_com_dotnet.php: * openssl_random_pseudo_bytes() available, so let's use
./wp-includes/random_compat/random.php: * 5. openssl_random_pseudo_bytes() (absolute last resort)
./wp-includes/random_compat/random.php: * openssl_random_pseudo_bytes()
Other files in the wp-includes
folder call this function, but they are part of the WordPress core. Deactivate the High Memory Test plugin and then re-test speed:
time curl http://<your-Linode-IP-address> -s 1>/dev/null
0.20 real
0.00 user
0.02 sys
Investigate Slow Loading Time: Render-Blocking JavaScript
The load time reported by cURL is now low, but if you load the page in a web browser, it can still take 5 seconds to show content. Because of this, we can deduce that the slowness is likely the result of a client-side bottleneck. We will use the developer tools of the Firefox Developer Edition browser to investigate this:
Visit the WordPress site in Firefox and then open the Performance tab of the developer tools.
Click the Start Recording Performance button and then reload the page in the browser.
When the page has finished loading, click the Stop Recording Performance button.
A waterfall graph will appear, and each row in it represents a browser rendering event. Scroll down in this graph until you see the following longer event, and then click on that event:
In the panel that appears on the right side, click on the numbered blue link. A view of the HTML document tree will appear and will highlight the responsible script. The responsible script is a sleep function:
In the Bash shell on your WordPress Docker container, search for this code:
root@localhost:/var/www/html# grep -R 'function sleep( timeInMilliseconds ){' .
./wp-content/plugins/blocking_js_test/blocking_js_test.php: function sleep( timeInMilliseconds ){
The Blocking JS Test plugin is responsible for this code. Deactivate this plugin. The page should now load in less than a second in the browser.
A Note About Plugins and Themes
The slow code examples used all correlated with specific plugins, and the solution was to deactivate them. If you need the functionality of a plugin but it is slow to load, try searching for other plugins that do the same thing and test them to see if they are more efficient.
Slow code can also be found in WordPress themes, and so if you can’t find bottlenecks in your plugins, it’s also a good idea to try different themes.
Best Practices
In addition to identifying bottlenecks in your code, you can implement general best practices for speeding up your site. Many of these practices can be easily set up by publicly-available WordPress plugins.
Asset Optimization
High-resolution images can slow down the speed of a site. Reduce the resolution of your images and optimize them for the web. Plugins like WP Smush can handle this task.
Minify CSS and JavaScript loaded by your site. Minification is the process of compressing code so that it is harder for a human to read, but faster for a computer to process. Scripts are often distributed in minified and non-minified versions, so you can look up the minified style for each of your scripts and upload them to your server. Some WordPress plugins can also automatically minify your scripts.
Browser Caching
By default, all page assets (images, scripts, styles) are downloaded from your site’s web server each time a user visits it, even if they have recently visited it and already downloaded those items. Your web server can be set to flag assets so that the browser instead caches those items on disk. The mod_expires module controls this behavior for Apache.
Web Server File Compression
Your web server can be configured to compress files on the server before they are sent to your web browser, reducing download sizes:
WordPress Caching Plugins
When you visit a WordPress page, the page is dynamically generated on each request by PHP and your database process. This is more taxing on your server than serving a static HTML page. WordPress caching plugins pre-compile your pages into static downloads. Two example plugins that do this are WP Rocket and W3 Total Cache. Some of these plugins also bundle in other best practices.
Web Server and Database Tuning
After you fix code performance bottlenecks and install other best practice measures, you can fine-tune the basic settings for your web server and database. This involves estimating the average memory and CPU usage of a request, comparing it with the total level of resources for your server, and then adjusting your software configuration to make the most of those resources. Linode has guides on optimizing Apache and MySQL:
Optional: Profile Your Own WordPress Site
You can reuse the XHGUI Docker Compose file provided by this guide to profile your own WordPress site. There are two halves to setting this up:
Run the provided XHGUI Docker Compose file to store and view profiling data from your WordPress site.
Insert the XHProf code into your WordPress app so that the data is actually generated on each request.
Perform the steps in the Setting Up the Test Environment section, and stop after you Download the Test Environment.
Run the XHGUI app
Change into the directory that corresponds with this Docker Compose file:
cd speed-up-wp-test-env/xhgui_app/
Start the app:
docker-compose up -d
Insert the XHProf code into your WordPress site
These instructions will only succeed if you are running PHP 7, and have unzip
and the php-dev
packages installed. For earlier versions of PHP, you will need to replace Tideways with the original distribution of XHProf.
These instructions walk through downloading the source code for XHGUI. This may seem strange, as the Docker Compose file is already responsible for running the XHGUI app. The reason the code is downloaded again is because XHGUI also provides helper tools for injecting the XHProf/Tideways profiling code into your app. Without these helper functions, you would need to manually add calls to XHProf/Tideways to your WordPress code, and also set up the connection to the Mongo database running inside Docker Compose.
Install Tideways:
wget -O tideways-xhprof.zip https://github.com/tideways/php-xhprof-extension/archive/master.zip unzip tideways-xhprof.zip cd php-xhprof-extension-master/ phpize ./configure make sudo make install
Add a line with the value
extension=tideways_xhprof.so
to yourphp.ini
configuration file.Note
There may be multiplephp.ini
files in different locations, like/etc/php/7.0/apache2/php.ini
and/etc/php/7.0/cli/php.ini
. Add this value in eachphp.ini
file both in this step and in Step 4 that follows.Install the MongoDB PHP Driver:
sudo pecl install mongodb
Add a line with the value
extension=mongodb.so
to yourphp.ini
configuration file.Download the source for XHGUI, install its dependencies (via the provided
install.php
), and copy the source to your document root. Replace instances of/var/www/html
with the document root for your web server:wget -O xhgui.zip https://github.com/perftools/xhgui/archive/master.zip unzip xhgui.zip sudo cp -r xhgui-master /var/www/html/xhgui cd /var/www/html/xhgui/xhgui-master sudo chown www-data:www-data -R . sudo php install.php sudo cp config/config.default.php config/config.php
Install XHGUI’s helper injection function.
For Apache servers, insert this line into your Virtual Host–remember to substitute in your document root– and then reload Apache with
sudo systemctl reload apache2
:php_admin_value auto_prepend_file "/var/www/html/xhgui/external/header.php"
This line will call XHGUI’s
header.php
at the beginning of every PHP file that is served.For NGINX, add this line to your server block, then reload the configuration file with
sudo nginx -s reload
:fastcgi_param PHP_VALUE "auto_prepend_file=/var/www/html/xhgui/external/header.php";
Configure which web requests are profiled by updating the
profiler.enable
function inxhgui/config/config.php
. The default configuration will profile 1 in 100 requests:'profiler.enable' => function() { return rand(1, 100) === 42; }
If you want to profile every request, change this to always return true:
'profiler.enable' => function() { return true; }
Your XHGUI app should now be collecting profiling data for requests on your site. The XHGUI app can be reached on port
8080
at your Linode’s IP address.
More Information
You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.
- Finding Bottlenecks in WordPress Code
- Profiling WordPress Performance
- Profiling with XHProf and XHGUI
- Tideways XHProf Extension
- XHGUI
- How to Use Docker Compose
Join our Community
Find answers, ask questions, and help others.
This guide is published under a CC BY-ND 4.0 license.