The Ups and Downs of Creating a Decoupled WordPress


Ever since the REST API was included in the core of WordPress, there’s been a lot of talk about using it for decoupled WordPress (sometimes known as headless WordPress). In this article, we’ll delve a little deeper into this topic, explaining what decoupled WordPress actually is and how it should be used with (or without) WordPress REST API. We’ll also reveal some interesting surprises we’ve encountered while trying to implement it.

Decoupled WordPress introduction

In its original form, WordPress is a full-fledged content management system (CMS). It comes equipped with a working admin interface for content entering and editing the frontend that displays it. It also includes a backend system written in PHP with various APIs that handle database interaction, user authentication, routing, and more.

On the other hand, decoupled WordPress uses WordPress as a sort of a model and controller bundled together that handles data manipulation, and database interaction, while REST API Controller provides a way to interact with the view layer using various API endpoints. In addition to MVC separation, we can (for security reasons or speed improvements) place the JS App on a separate server like in the schema below:

REST API was bundled into the WordPress core since version 4.4. However, endpoints for posts, comments, terms, users, meta, and settings appeared later in version 4.7.

What WordPress REST API allows you to do is to interact with your WordPress installation remotely by sending and receiving JSON objects.

The ideal use case for this is a simple single-page application. Another advantage of REST API is that you can use any popular view framework or library to display your frontend. For example, React, Vue, Angular are just a few options in a sea of popular view libraries and frameworks.

Thanks to this feature, you can have the backend part on one server, which can be optimized for server-side tasks (faster disk, optimized database interactions, etc.). Then, your front is served from another server, giving you a good separation of concerns.

Creating a simple website using REST API

We wanted to determine how to use WordPress REST API for building a simple website with React on the frontend. We were testing whether we’d get better performance when decoupling the front from the backend. In our case, we didn’t really need all the REST API functionality, such as editing, creating and deleting posts or uploading media files. We wanted to retain the CMS ability so our colleagues in marketing, who usually add and edit content, wouldn’t have to ping developers to update the site.

We first encountered the strange WordPress lagging REST API on a client site, where we used the custom endpoints to get the list of locations on a Google map, alongside other meta information created using the Advanced Custom Fields Pro plugin. It turned out that the time to first byte (TTFB), which is used as an indication of the responsiveness of a web server or other network resource, was more than 3 seconds.

After investigating, we realized the default REST API calls were actually really slow, especially when we “burdened” the site with additional plugins.

As an initial test case, we created a default WordPress installation with version 4.9.4, using Varying Vagrant Vagrants running on PHP 7.0.27 and MySQL 15.1 (Distrib 10.1.31-MariaDB). Using Twenty Seventeen theme, without any pages and only sample posts, the postman app gave a loading time of 47 ms for a response size of 2.16 KB. This isn’t bad—but in the real world, you won’t have only one post on your site. So we added some more default posts, importing them from the Theme Review Teams repository.

In this case, the load times increased to 161 ms, and the response size was 41.63 KB. The timings in Chrome were a bit slower with 261 ms, and the TTFB was 173.76 ms. That was for just ten posts.

We were curious to see how adding additional plugins would influence the response times. The plugins we used, in no order of relevance, were:

  • Advanced Custom Fields
  • Akismet Anti-Spam
  • All In One SEO Pack
  • bbPress
  • Contact Form 7
  • Google XML Sitemaps
  • Hello Dolly
  • Jetpack by
  • Limit Login Attempts
  • Theme Check
  • TinyMCE Advanced
  • WooCommerce
  • Wordfence Security
  • WordPress Importer
  • WP Super Cache
  • Yoast

We encountered some interesting results. The postman app now gave the load time of 1970 ms for 41.9 KB of response size. Chrome’s load time was 1.25 s (TTFB was 1.25 s, content was downloaded in 3.96 ms). Just to retrieve a simple list of posts. No taxonomy, no user data, no additional meta fields.

Reasons for the long REST API response times

Why were response times so long? For one, accessing REST API on the default WordPress will load the entire WordPress to serve the endpoints, even though it’s not used—and the more plugins you add, the worse things get. The default REST controller WP_REST_Controller is a really big class that does a lot more than necessary when building a simple web page. It handles routes registering, permission checks, creating and deleting items, etc.

One workaround could be to create a custom REST controller, which will only serve the content and handle permissions and authorization. Unfortunately, we haven’t tested if this would have any impact on the performance.

Another workaround we implemented was to load only the bare minimum of WordPress and store our data in a transient, from which we’ll fetch using a custom page.


Transient API is used to store certain data in the options table in your database for a specific period of time. You can set an expiration time for a transient. After this date, the transient will be deleted from the database, or you can store it permanently and use post save or update hooks to manually delete and recreate it. It is a powerful tool WordPress uses to cache things, such as plugin versions, theme versions, and other data. Here is one of the major advantages of transients: When using a caching plugin, transients will be stored in memory, and when requested, data will be pulled from it instead of querying a database to fetch it—which is even faster. In addition to being really fast, it can hold up to 4GB of data in it; and 4GB JSON string is really, really a lot.

Minimal WordPress load

To load the bare minimum necessary for WordPress to work, you need to add just one line—either in your wp-config.php file or, better yet, in a custom file you’ll use in your plugin or in your theme:

	define( ’SHORTINIT’, true );

A quick look in the wp-settings.php reveals that if this constant is set to true, most of the WordPress won’t be loaded. Any additional WordPress functionality would have to be added manually, but it pays off.

What we created was a file called wp-config-simple.php that looks like this:


// This constant loads only the minimal version of WordPress.

define( ’SHORTINIT’, true );

// We need the wp-load.php to fetch the pages.

$parse_uri = explode( ’wp-content’, $_SERVER[’SCRIPT_FILENAME’] );

require_once( $parse_uri[0] . ’wp-load.php’ );

This in turn only calls the initial wp-load.php file necessary for pages and posts. Then we can include other files needed for other WordPress functionalities.

Caching pages

The complete solution to this is a plugin called Decoupled JSON content.

This plugin will cache the pages and posts in a transient, and its JSON content can then be fetched by accessing a file that will serve this JSON. For instance, the pagehttp://local.wordpress.test/wp-content/plugins/decoupled-json-content/page/rest-routes/page.php?slug=blog&type=page will give you the following JSON:

  "ID": 703,
  "post_author": "1",
  "post_date": "2011-05-20 18:51:43",
  "post_date_gmt": "2011-05-21 01:51:43",
  "post_content": "Use this static Page to test the Theme’s handling of the Blog Posts Index page. If the site is set to display a static Page on the Front Page, and this Page is set to display the Blog Posts Index, then this text should not appear.",
  "post_title": "Blog",
  "post_excerpt": "",
  "post_status": "publish",
  "comment_status": "open",
  "ping_status": "closed",
  "post_password": "",
  "post_name": "blog",
  "to_ping": "",
  "pinged": "",
  "post_modified": "2011-05-20 18:51:43",
  "post_modified_gmt": "2011-05-21 01:51:43",
  "post_content_filtered": "",
  "post_parent": 0,
  "guid": "",
  "menu_order": 0,
  "post_type": "page",
  "post_mime_type": "",
  "comment_count": "0",
  "filter": "raw",
  "custom_fields": {
    "_wp_page_template": [
  "template": ""

Aside from default post/pages routes, there are also custom menus routes and several hooks, which can be used to extend the functionality of the plugin: Add custom post types, override various post formats, change custom fields and more.

The way to use this in an app would be to fetch the data using the above URL, changing the slug and the type based on what you need to fetch, and store it in your application state (using Redux, or better yet MobX in case of React). Whenever data is needed, it can be pulled from the store. However, it’s important to take extra care to ensure new data is being fetched when the content changes on the WordPress side. This concerns the JS App, which we will cover in another article.

Testing it out

When we accessed the JSON data using the above method, we noticed a drastic decrease in load times. We conducted testing in Chrome, where we could see the total response time and the TTFB separately. We tested response times ten times in a row, first without plugins and then with the plugins added. Also, we tested the response for a list of posts and for a single post.

The results of the test are illustrated in the tables below:

alt text
alt text

As you can see, there is a drastic difference.

There are some limitations that should be taken into account. For now, you can only retrieve data. Storing and creating isn’t handled by the decoupled content plugin. Also, since the plugin directly calls wp-load.php, it won’t work on non-standard installations of WordPress such as bedrock.


Is WordPress REST ready? Yes and no.

REST API is great because it can be used to create fully-fledged apps—creating, retrieving, updating and deleting the data. The downside is its speed. You’ll either have to create a custom controller, which is something we need to test or sacrifice the speed of your app.

Obviously, creating an app is different than creating a classic website. You probably won’t need all the plugins we installed. But if you just need the data for presentational purposes, caching data and serving it in a custom file seems like the perfect solution at the moment.

You may be wondering, “What’s the point? Couldn’t we just create a regular WordPress theme using good old PHP?” Sure we can. Not only that, but we have an excellent boilerplate that uses object-oriented programming principle, namespacing and autoloading, along with some caching tricks to make a site as performant as possible. But with Gutenberg around the corner, front-end frameworks are getting really popular with the WordPress community, so this is something to keep in mind.

We hope you found this article helpful and interesting. Feel free to comment and share your thoughts.

For more details on the test data check this link.