Introducing WPGraphQL Smart Cache
- Jason Bahl
Developers are choosing WPGraphQL for decoupling their projects because of the flexibility it provides, the control it gives the client developer, the type safety, the tooling and more. The developer experience of using WPGraphQL is pretty nice.
One pain point, however, is the lack of caching.
In this post, we will go over some of the performance problems developers have ran into when using WPGraphQL, and the solutions.
The Problem: WordPress Server Timeouts
The most common way to interact with a GraphQL API is via HTTP POST requests. Most servers do not cache HTTP POST requests, meaning a POST request to WordPress will make its way to the WordPress server to be processed and returned. This requires the WordPress server to process the request and return a response.
This requires WordPress to load, execute the WordPress “bootstrap” process, load plugins, run actions and filters, load options, check for authentication and authorization, then execute the resolvers of the GraphQL request. This can cause scaling issues.
When building decoupled sites with WPGraphQL, using HTTP POST requests as the primary method of fetching data can cause your WordPress server to be overwhelmed. WebDevStudios wrote about this problem and a lot of Gatsby users have experienced this problem as well.
When a WordPress server is overwhelmed with HTTP POST requests, the server can become exhausted and run into performance issues and server timeouts.
Solution: Fetch Less
While this article is announcing WPGraphQL Smart Cache v1.0, I want to point out another solution that can work well, too.
I’ve worked with many WPGraphQL users to improve the performance of their decoupled site build times.
One of the easiest solutions to implement, is to fetch less data from WPGraphQL.
Several NextJS WordPress starter projects have popularized patterns of fetching everything from WPGraphQL in order to build static pages. This is one of the main culprits of performance issues, including the one WebDevStudios wrote about.
I’ve explained at length why this pattern of fetching data should be corrected.
The idea is that instead of fetching EVERY post and page and statically generating each page during every build of your decoupled application, you would instead only fetch what you need to build the most important pages at build time, and let the rest of the pages be built at run time, incrementally regenerated when possible.
So, instead of fetching every post and page and generating static pages for every single url at build time, we would use the WPGraphQL nodeByUri query to fetch the node for the uri at run time and incrementally build static pages at run-time, then re-generate them using ISR.
I also paired with Colby Fayock to refactor his Headless WordPress Starter project to use NextJS Incremental Static Regeneration (pull request here), leading to far fewer requests to the WordPress server.
This leads to faster build times, much fewer requests to the WordPress server, and a better experience for content creators publishing in WordPress as their changes will be live on the decoupled front-end much faster. (see a demo of publishing content on WPGraphQL.com)
By implementing Incremental Static Regeneration with the nodeByUri query, I’ve helped customers on WP Engine Atlas get their NextJS build times from more than 5 minutes, to less than 20 seconds!
Solution: WPGraphQL Smart Cache
WPGraphQL Smart Cache is a free open-source WordPress plugin that brings support for Network Cache, Object Cache, Cache Invalidation and Persisted Queries to WPGraphQL.
Below we’ll look at how WPGraphQL Smart Cache can greatly reduce response times from your WPGraphQL API and reduce build times of your decoupled sites.
You can use WPGraphQL Smart Cache and benefit greatly even if you don’t / can’t implement Incremental Static Regeneration as discussed in the section above.
Below we will look at how WPGraphQL Smart Cache helps reduce build times and alleviates pressure on your WordPress server.
When I refer to “Network Cache” I’m referring to caching when using HTTP GET requests.
WPGraphQL has supported making GraphQL queries via HTTP GET requests for several years. And many WordPress hosts already support HTTP caching of WPGraphQL requests made over GET requests.
The way Network cache works, is that there’s a caching layer between the client application requesting data, and the WordPress server itself.
The client application will send the GraphQL Query over HTTP GET. The first time the query is sent, this will be a “cache miss” and the request will make its way to the WordPress server for execution. WordPress will load up and process the request, executing all of the resolvers for the GraphQL query and a response will be returned.
The response will then be cached in a network cache layer (typically Varnish or similar). Whatever the layer is, if your host supports network caching, the request will be cached there.
Then, the next time the same query (and same variables) is executed, the response will be served from the cache instead of sending the request all the way to WordPress for execution.
Let’s take a look at this in action.
HTTP GET: Cache Miss
Below is a screenshot of an HTTP GET request, with the Chrome Developer tools open, highlighting the request in the “Network” tab. The first time the query is executed, it will be a Cache MISS. We can see this in the the response headers. The request took 304ms to respond.
HTTP GET: Cache Hit
Now, if we refresh the browser, the request will be returned from the network cache, and my WordPress server won’t be hit at all.
Here we can see that after refreshing multiple times, we’re getting the response in 53ms, and the response headers indicate the response is a cache hit.
We can keep refreshing, and keep getting responses from the cache.
If you had a client-side component on a page that was executing this query with HTTP GET, and the page received 10,000 visitors at the same time, 9,999 of those visitors would get a response from the network cache, instead of the WordPress server.
By switching your GraphQL client from using POST requests to using GET requests, you can start taking advantage of HTTP caching and your server will immediately have a reduced burden.
Caching is great, but that’s only one part of the picture. The other part is Cache Invalidation, and this is where WPGraphQL Smart Cache really shines.
If all you do is switch from HTTP POST requests to HTTP GET requests, if your host supports network caching for GraphQL queries, you will benefit from faster responses and a reduced load on your WordPress server, but the cached responses will remain cached for a period of time, determined by the max-age headers of the response.
In this screenshot we see the “cache-control” header is set to 600 seconds. That means that for 10 minutes the response will be cached. Any requests for this query in the next 10 minutes will get a cached response, even if these posts were edited or new posts were published in those 10 minutes.
This isn’t ideal.
When building decoupled applications, we want the GraphQL response times to be fast, but we also want the data to be accurate.
Auto purging caches when data changes
WPGraphQL Smart Cache listens for publish, update and delete events in WordPress, and then emits a “graphql_purge” action to purge caches related to the object that was modified.
The eviction strategy is as follows:
- publish: purge( ‘list:$type_being_published )
- update: purge( $id_of_node_being_updated )
- delete: purge( $id_of_node_being_deleted )
This is more nuanced than simply calling purge in response to actions like “save_post”.
We’ll discuss that nuance below.
There’s a lot of nuance around what is considered “Public” vs “Private” and WPGraphQL Smart Cache takes this into consideration.
Creating a new post isn’t automatically considered a “publish” event. New posts are created in many states, such as “draft” or “auto draft”, and draft posts aren’t considered public, and therefore wouldn’t be in responses of public queries that would be cached.
Transitioning a draft post to become a published post, however, would convert a post from a non-public state to a public state, and this would trigger WPGraphQL Smart Cache to call “purge( “list:post” )“.
If the post being published was a page, it would call “purge( “list:page” )” and if it were a custom post type it would call “purge( “list:$graphql_type_name_for_the_cpt” )“.
When it comes to other data, such as users and menus, there’s even more nuance.
Users in WordPress are not considered public entities by default.
If you create a user in WordPress, you must login to WordPress to know that user exists. However, once content is published with that user as the author, that user becomes a public entity, with an archive page, a REST API endpoint, etc.
WPGraphQL Smart Cache treats this scenario as a publish event. So, when a User becomes the author of their first publicly visible post, this would trigger “purge( “list:user” )“, evicting any queries that asked for a list of users.
Menus are similar, in that Menus that are not assigned to a Menu Location are considered non-public entities in WordPress. Once a Menu is assigned to a location, that would trigger “purge( “list:menu” )” and any queries that asked for a list of menus should be evicted.
Update events are a bit more straightforward than publish events.
If an object is considered public (published post, user with published content, menu assigned to a location, etc), and that object is updated, it will trigger “purge( $id_of_object )“. This would evict any cached queries that had that “node” in its results.
Delete events are as nuanced as Publish events.
Simply listening to the “delete_post” hook isn’t enough. We need to know when posts transition from a public state to a non-public state to be considered a “delete” event.
If a GraphQL query returned a post or a list of posts, and one of those posts was converted from “publish” back to “draft”, that would be considered a “delete” event and any caches with that post would be evicted. This would trigger “purge( $post_id )“. Of course, if a published post was moved to the trash or force deleted, this would also trigger.
If a “draft” post is moved to the trash, this event is not triggered because it would be deleting something that’s not considered public, and therefore not in cached responses that would need purging.
Supported Hosts & Hosting Guide
In order for Network Cache invalidation to work, this requires some implementation details supported by the WordPress host.
When WPGraphQL returns a response, it contains an “X-GraphQL-Keys”.
This is a list of keys relevant to the query and the response. The list of keys contains information about what types of nodes were querying as a list as well as the individual node IDs.
When the host caches the response, it needs to be able to use these keys to “tag” the cached document.
Then, the host needs to be able to purge cached documents when that tag is purged.
WP Engine is the first host to fully support WPGraphQL Smart Cache. You can give it a try by signing up for a free sandbox account.
If you are a WordPress host that wants to support WPGraphQL Smart Cache, or you want to tell your host to support it, we have a hosting guide.
WPGraphQL provides support for object cache of GraphQL queries. This works similar to network cache, but instead of being cached at a level above WordPress, its cached within WordPress object cache layers.
This does require WordPress to load and retrieve the cache, so there’s more overhead than network cache, but it still reduces the overall processing required to return results for a GraphQL query.
If your WordPress host supports persistent object cache, such as memcached or similar, the cache will be stored there. If not, it will fall back to transients.
WPGraphQL Smart Cache does not enable Object Cache by default. To enable it, you need to visit the settings page under “GraphQL > Settings > Cache” and check the option to “Use Object Cache”.
With this enabled, GraphQL queries made using HTTP POST requests can benefit from caching. This can come in handy if your client application can’t use GET. For example, if you had developed a native iOS application, even if you wanted to switch to GET, the application can’t be changed until the user downloads an update.
From this settings page, you can also purge the GraphQL Object cache.
The invalidation of the object cache works the same as the invalidation for the network cache, but doesn’t require specific implementation details from your host.
When possible, we recommend using GET requests to take advantage of HTTP / network cache as it will lead to faster responses and greater processing reduction on your WordPress server.
Persisted Queries is a feature provided by WPGraphQL Smart Cache that is often used together with Network cache.
GraphQL documents can get quite long, so using GET requests for some queries isn’t realistic as there are query string length limitations.
Persisted Queries help avoid this problem.
With persisted queries, you can store GraphQL Query documents on the WordPress server, then make HTTP GET requests referencing the query by ID, instead of passing the full query string in the URL.
The Admin User Interface for Persisted Queries is hidden by default (we will likely be iterating how the UI in the admin looks and behaves, so don’t get married to it’s current state).
To show the UI, you can visit “GraphQL > Settings > Saved Queries” and check the option to “Display saved query documents in admin editor”.
This adds a “GraphQL Documents” menu item in your WordPress Dashboard.
From this User Interface, you can manage GraphQL Documents.
There’s a list view of the documents:
And an individual document editor where you an add the document, provide an optional description, assign aliases (ids), set the allow/deny rule, and customize the max-age header.
Once a document is saved, it can by queried by id.
For example, in the screenshot above, the ID of the document is: “b5f0fe422d78ffdfc7a43206693a8268d2fe368ec0f7d737e778efd5a3a7a08d”. That can be used in a GET request as the value of the “queryId” parameter:
In the screenshot we can see an HTTP GET request to the GraphQL endpoint using the “alias” as the value of the “queryId” parameter, and the results of the referenced Query Document being returned.
Automatic Persisted Queries (APQ)
Automatic Persisted Queries is a technique made popular by Apollo and is supported by WPGraphQL Smart Cache.
The way this works, is that the client application will try and use HTTP GET requests to fetch queries by ID (a hash of the query). The server responds with the results of the query if it can. If the ID is not recognized, the server returns a specific error, and the client follows-up with an HTTP POST request with the full query document and the query ID. The server then stores the GraphQL document and associates it with the ID, and future HTTP GET requests referencing the ID will execute the document.
This technique is implemented and active on WPGraphQL.com.
Allow / Deny Lists
One benefit of having GraphQL Documents saved on the server, is the ability to define allow and deny lists.
By default the WPGraphQL API allows arbitrary execution of queries. Sometimes you might want to disable that and only allow queries you’ve pre-defined.
To restrict the GraphQL endpoint to only execute allowed queries, you can visit “GraphQL > Settings ?> Saved Queries” and select the “Allow only specific queries” value for the “Allow/Deny Mode” setting.
With “allow” mode enabled, arbitrary queries will not be executed. Instead, an error will be returned.
One workflow teams can implement is as follows:
Front-end teams work on the application, writing all the queries they need for the application. Once the front-end team is ready, they generate a list of queries needed for the application and save them in the production environment as GraphQL Documents marked as “allowed”.
Then the production app can execute the queries against the server, while preventing arbitrary queries from being executed.
WPGraphQL Smart Cache is a new and exciting piece of the WPGraphQL ecosystem. We believe that the features provided by WPGraphQL Smart Cache will help teams scale their use of decoupled WordPress and unlock new possibilities for what can be built with this stack.
We’d love to hear what you build with WPGraphQL and WPGraphQL Smart Cache!
You can get the plugin here: https://wordpress.org/plugins/wpgraphql-smart-cache.
If you enjoy the plugin, please leave a review.