Building WPDecoupled: More Caching and WP Smart Search on Atlas

Alex Moon on

Well, forehead smacks happened today. I have been focussing on shipping fast and not on perfection or performance. Loading times on the site were particularly slow. Last week I talked about the work done on GraphQL caching to help load times. This did help, but I was still seeing server response times in the 700-800ms range on the HTML content alone from the server. This was cut in half from before but was slower than it should have been. I’ll share what I realized and how I fixed it.

Additionally, I finally finished implementing search! WPEngine has implemented great WPGraphQL and REST search functionality via WPEngine Smart Search. This is a beneficial upgrade from the default WP search and can be accessed via the same APIs in REST and WPGraphQL as the native WP search. I’ll share more on the implementation below.

Caching

Sometimes you intentionally don’t do something, and then months later you realize you never did that thing. For me, that something was HTML page caching. To my horror today I realized my SSR pages were not getting cached on Cloudflare. Cloudflare is provided by WPEngine Atlas but it was up to me to set my own Cache-Control headers.

Background

When it comes to caching I’m a fan of only purging things when needed. The WP standard of caching for 10 minutes and purging the entire cache when editing one post does not fly for me. It is better than nothing but Cache Keys or stale-while-revalidate(SWR) patterns.

Cache Keys tie a unique identifier to the cached data (JSON/HTML/etc) that is returned. Then when that specific data is edited the CDN is notified that a unique identifier has been made stale. The CDN or whatever cache then purges any number of cached data objects if they are tagged with that cache key. This is a very powerful cache purging pattern but also requires a lot of technology and integration between the CMS and the CDN.

If Cache Keys can purge a cache at the content type level and a full cache purge is done at the site level, then SWR is the sweet spot in the middle that is simple but allows you to purge cache at the route level. It requires no coordination with the backend as it is still fundamentally time-based. It requires adding a public, max-age=20, stale-while-revalidate=60 to your Cache-Control response headers for the CDN to handle caching.

This header means every 20 seconds the CDN will show the cached HTML as stale. For up to 60 seconds after that page goes stale, the CDN still responds with the stale HTML. But, upon receiving the first request after the cache had gone stale the CDN works in the background to revalidate the cache. The front-end server responds to that request with either the same content or new. Using the etag header from the server the CDN with either replace the data with new or remark the data as fresh for the next 20 seconds.

Implementation

My server had NO Cache-Control headers configured. Page loads were slow cause the front-end SvelteKit server was getting hit and having to re-render for nearly every page request. Not good. I would love to be able to use Cache Keys, but that’s going to be a longer project. SWR it is for now.

I added Cache-Control: 'public, max-age=20, stale-while-revalidate=60, stale-if-error=86400' to my base layout in SvelteKit which means this header applies to all pages. I could make this unique for various pages but for now this seems like it’ll work.

Screenshot of browser dev tools showing cache-control headers and cache hit.

The additional stale-if-error=86400 adds some fallback for when the server might go down. In the case that the CDN cache goes stale but the server is providing an HTTP 500 error the CDN will continue serving the stale page. In this case, it will do so for 24 hours.

Results

Cached responses are now coming in at 50-200ms. Initial responses with stale cache are still slow as this is done synchronously on Cloudflare, but any parallel requests coming in after that initial revalidation request will get the stale cached response. I might tweak my max-age to be longer, this would improve performance, but I don’t want it to be too long and not be able to quickly update content.

screen shot of dev tools showing page timings for HTML. Sub 100ms waiting on the server.

WP Engine has done all the hard work here. I had to write up a GraphQL query and render the results. Because of the Persisted Query work I did last week all queries are cached for fast search! Checkout their docs for specifics to getting started.

SvelteKit made this implementation super easy. I created a form that SvelteKit automatically enhances with JS so that It works with and without JS. I also used the new HTML <dialog> element that made the UI really easy to implement.

search results page

The one weird thing I noticed was WP default search query is different than what I built. I made my search page/results on /search?q=seo. WordPress is /?s=seo. This doesn’t really matter other than that I’m letting Rank Math handle structured data in my HTML for content. One of the items Rank Math adds the WP style search to that structured content. Google Search Console was picking this up and noting that this page wasn’t working. A quick redirect if the 's' query paramater exists fixes this issue.

Conclusion

I hope you enjoy the new speed and search ability of the site! Both are big steps forward made fairly straight forward by the WP Engine Atlas platform.

Alex Moon

About Alex Moon

Avid Open Source maintainer, a longtime developer, and vagabond.