Blog

How to cache queries to admin-ajax.php in WordPress to improve performance

Hello!

Working with wordpress for a while now, we noticed that many actions, whether administrative in nature or building a WordPress query on the front end, are dependent on the built-in admin-ajax.php or WordPress AJAX API.

Since many 3rd party plugins depend on this Ajax API to dynamically push and pull data, it is unfortunately a common occurrence to have the performance of a site impacted when many AJAX API calls are happening.

One of the tell tale signs of admin-ajax.php performance issues can be seen when inspecting the network connections of rendering a particular page on your WordPress site. If you filter “admin-ajax.php” in your network tab of the developer console in your browser, you should see clearly the admin-ajax.php POST that may be taking too long. In our experience, some post grid plugins that make it easy to render a grid of posts on your page heavily rely on admin-ajax. In these scenarios we’ve seen the admin-ajax.php query take over 10 seconds to complete!

Many factors contribute to this, but this will continue to be a problem until better understanding between plugin developers and the WordPress core developers can be had. This could simply be an understanding of best practice guidelines for plugin development or perhaps some marginal improvements to the WordPress AJAX API to better accommodate heavy usage.

There are some caching plugins that may help reduce this strain on your WordPress site. However the purpose of this post is to give a proof of concept demonstration to actually caching an admin-ajax.php query response.

This means we will develop the code necessary to interrupt a request to admin-ajax.php before it arrives and is processed. If the request was processed and cached, serve the response from the cached response (in this case a static file). If the request is new, pass it to WordPress’ AJAX API and save the response as a file for future queries. I’ll break down this process in further detail below.

Interrupt a regular request to admin-ajax.php

First things first, we want to catch a request to admin-ajax.php. This is because we want to serve the response from a statically cached flat file instead of actually bothering the Ajax API. This will greatly speed up the response time and make your site run lightning fast.

// Initialize only if enabled
if (shift8_ajax_cache_check_options()) {
    add_action('init', 'shift8_ajax_cache_init', 1);
}
function shift8_ajax_cache_init() {
    $current_url = rtrim($_SERVER['REQUEST_URI'], '/');
    if ( strpos($current_url, '/admin-ajax.php') !== false ) {
        echo "Ajax API request caught";
    }
}

The above snippet is very simple. We are adding an action to init to trigger our function. We are checking the request URI to see if it includes admin-ajax.php. If it does, echo a simple response. Now we have successfully caught an admin-ajax.php POST.

Check the admin-ajax.php POST data

This is where the magic happens. In our scenario we wanted to cache the POST data that was requesting we build a grid of WordPress posts (i.e. a post grid).

Since we are designing this proof of concept with a specific use case, we are going to be checking if the admin-ajax.php query matches what we are trying to cache. We definitely dont want to cache every admin-ajax.php query! This would include administrative actions like auto-saving revisions and too many WordPress admin actions to count. It should be highlighted that this could potentially put the security of your site in jeopardy if you are producing static (and publicly accessible) cache files for admin actions. Thats why this is a proof of concept!

In our case, we wanted to cache the WPBakery Page builder built in post grid, which at the time of this writing, can be horribly slow.

        if (isset($_POST['data']['visible_pages']) && isset($_POST['data']['page_id']) && isset($_POST['data']['style']) && isset($_POST['data']['action']) && isset($_POST['data']['shortcode_id']) && isset($_POST['data']['tag']) && !isset($_POST['shift8-ajax'])) {
            $vc_id = $_POST['visible_pages'] . $_POST['page_id'] . $_POST['style'] . $_POST['action'] .  $_POST['shortcode_id'] . $_POST['tag'];
            $post_vars = $_POST;
            $cache_file = plugin_dir_path(__FILE__)."cache/".hash('md5', $vc_id).".html";

In our shift8_ajax_cache_init function, after we match the URI for admin-ajax.php, we are checking the _POST data. Dont worry too much about what we’re checking, but in your case if you wanted to do this for one specific admin-ajax.php post, you would simply need to look at the POST data that your sending and check for it.

The one thing to note is a check in that if-condition for a _POST that wouldnt normally be sent : !isset($_POST[‘shift8-ajax’]). This is a new _POST data variable that we are sending. We need to avoid an infinite AJAX POST loop where we check for the admin-ajax.php query, then re-submit it.

When we re-submit the POST if the cache file doesn’t exist (illustrated below), then we need to bypass this whole function outright. This is why we are passing the extra shift8-ajax POST variable – if it exist it means we really need WordPress to process the admin-ajax.php query.

Process the admin-ajax.php POST data, cache it or serve from cache if it exists

In this part , we are going to check if the cache file exists first and foremost. If it does then it means we’ve done the query before and should serve from the static cache file which dramatically improves speed.

// If the file exists and was cached in the last 24 hours...
            if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
                $file = file_get_contents($cache_file); // Get the file from the cache.
                echo $file; // echo the file out to the browser.
            } else {
                $post_vars['shift8-ajax'] = true;
                $result = wp_remote_post( 'http://shift8.local/wp-admin/admin-ajax.php',
                    array(
                        'method' => 'POST',
                        'timeout' => 45,
                        'redirection' => 5,
                        'httpversion' => '1.0',
                        'blocking' => true,
                        'headers' => array(),
                        'body' => $post_vars,
                        'cookies' => array()
                    )
                );
                if ( is_wp_error( $result ) ) {
                    $error_message = $response->get_error_message();
                    echo "Something went wrong: $error_message";
                } else {
                    $result_body = wp_remote_retrieve_body($result);
                    file_put_contents($cache_file, $result_body, LOCK_EX);
                    echo $result_body;
                }

In the first if-condition above you can see we are checking if the file exists and if its younger than 24 hours old. You can change the 24 hour (86400 seconds) to any time that makes sense. If that condition matches, then we are simply grabbing the contents of the file and returning it as a response from the admin-ajax.php POST request, simulating the exact response WordPress would give but way faster.

If it doesnt exist? Then we need to know what WordPress would return. We make use of the wonderful wp_remote_post function thats built into WordPress to re-submit the exact same request, save for the additional shift8-ajax POST variable. That additional variable will bypass this entire function and force WordPress to respond.

The response from WordPress is then (finally) stored in a cache file and returned back as a response.

Security Considerations

Again this is a proof of concept. There are definite and serious considerations that should be made if one were to put this proof of concept into practice. Namely, this caching implementation should be restricted to specific admin-ajax.php queries, not all. There should also be more checks in place to ensure that administrative functions, actions or sensitive information isn’t being leaked into the static cache files.

Hopefully someone finds this useful. If implemented properly, you can definitely see major performance gains!