Caching Inertia's SSR responses
Inertia has been widespread among Laravel developers for a few years due to its seamless integration of front-end frameworks like Vue.js and React. However, the recent addition of SSR support in Inertia opens up new possibilities for improving performance and SEO. To further enhance the performance of SSR-rendered responses, we can leverage our very own spatie/laravel-responsecache package, a powerful HTTP caching solution for Laravel applications. In this post, we will explore how to integrate and configure the package to cache SSR responses generated by Inertia. We'll also look at some interesting edge cases we discovered when integrating this in Flare 2.0.
SSR in Inertia: A Quick Introduction
Server-side rendering (SSR) is a technique that allows the server to generate and deliver fully rendered HTML pages to the browser instead of relying on the browser to execute megabytes of JavaScript to render the content.
In the context of Inertia, this is achieved by passing the Inertia props from Laravel to a Node.js SSR process. This way, we can utilize the server's full performance to quickly pre-render the component's HTML. On the client side, React knows how to handle this SSR response and "re-hydrate" it to add the required interactivity for the React components. While this may sound complicated, Inertia makes integrating SSR easier than ever, even in existing applications (like Flare!).
This entire SSR flow results in faster load times, improved performance, and the ability to use React components in an SEO-friendly way (as the fully rendered HTML content is always readily available for indexing by search engines).
(Re)-introducing spatie/laravel-responsecache
spatie/laravel-responsecache
is a versatile HTTP caching package. While it may be nothing new in 2023, combining it with SSR could be this package's most exciting use case since its release. Inertia with SSR is already pretty fast, but in-memory caching will always be faster.
Apart from easy response caching, the package provides flexible caching strategies, cache invalidation mechanisms, and integration with various parts of your Laravel application. We can now efficiently cache Inertia's SSR responses using some of the above features initially designed for regular HTTP requests, resulting in lower server load and faster subsequent page loads for our users.
Let's take a quick look at how we configured spatie/laravel-responsecache
to work with Inertia's SSR features:
Installation and Configuration
Installing the response cache package is as easy as installing any other package in Laravel:
# use composer to install the package
composer require spatie/laravel-responsecache
# publish the config file
php artisan vendor:publish --provider="Spatie\ResponseCache\ResponseCacheServiceProvider"
Finally, add the CacheResponse::class
middleware to the (Inertia) routes you want cached:
use \Spatie\ResponseCache\Middlewares\CacheResponse::class;
Route::get('my-awesome-homepage', HomeController::class)->middleware(CacheResponse::class);
And just like that, you've got basic HTTP response caching in Laravel. However, looking at the laravel-responsecache
header in your browser's developer tools, you might notice that Inertia responses are not being cached. That's because Inertia requests are made using Axios (AJAX), and the response cache package avoids caching any AJAX requests.
A custom cache profile for Inertia requests
By default, spatie/laravel-responsecache
comes configured with the CacheAllSuccessfulGetRequests
cache profile. This profile caches (you guessed it!) all successful GET requests. However, less evident from reading the class name, it avoids caching AJAX requests. Because Inertia uses AJAX to request new pages, we need to customize the cache profile to ensure caching of these requests.
We'll do this by creating a new InertiaResponseCacheProfile
class and configuring that in the cache_profile
option of the config/responsecache.php
config:
'cache_profile' => \App\Support\ResponseCache\ResponseCacheProfile::class,
The custom cache profile will work almost identically to the original CacheAllSuccessfulGetRequests
profile. Here's what it looks like:
use Illuminate\Http\Request;
use Spatie\ResponseCache\CacheProfiles\CacheAllSuccessfulGetRequests;
class InertiaResponseCacheProfile extends CacheAllSuccessfulGetRequests
{
public function shouldCacheRequest(Request $request): bool
{
if ($request->ajax() && $request->isMethod('get')) {
// Cache Inertia (= AJAX) GET requests!
return true;
}
if ($this->isRunningInConsole()) {
return false;
}
return $request->isMethod('get');
}
}
Testing this new cache profile with Inertia routes will quickly surface its biggest flaw: Inertia will return a normal HTML response on the first request to a particular endpoint and JSON responses on subsequent requests to the same endpoint. The important thing to note is that the response cache will not consider the response's content type. This means that whatever HTML or JSON response is returned will be cached, resulting in situations where HTML is returned when JSON is expected or vice versa.
Making the response cache aware of content-type
To deal with Inertia's HTML and JSON responses on the same endpoint, we must make the response cache aware of the content type. We can do this by using a custom hasher class. You can think of the hasher class as a cache key generator: it should generate a unique hash for unique requests (e.g., different content types) but the same hash for similar requests (e.g., GET /?a=1&b=2
and GET /?b=2&a=1
).
Let's create a custom InertiaResponseCacheHasher
hasher class for generating a unique hash that is not only based on the request URL and method but also the request's content-type
header:
use Illuminate\Http\Request;
use Illuminate\Hashing\DefaultHasher;
use App\Actions\ResolveCurrencyForCustomerAction;
class InertiaResponseCacheHasher extends DefaultHasher
{
public function getHashFor(Request $request): string
{
$baseHash = parent::getHashFor($request);
$contentType = $request->getContentTypeFormat();
return "{$baseHash}-{$contentType}";
}
}
In this custom response cache hasher, we extend the DefaultHasher
provided by the package and override the getHashFor
method. We append the content-type
to the base hash to maintain a different response cache for each content type.
If you provide different responses based on request parameters, this would be the ideal location to configure it. For instance, Flare's pricing page displays prices in either euros or dollars, depending on the IP location. By incorporating the resolved currency in the response cache key, we can prevent mistakenly serving the incorrect currency to the incorrect location.
Conclusion
Combining spatie/laravel-responsecache
with Inertia can significantly enhance performance and reduce initial load times. Additionally, it enables us to fully utilize Inertia's SSR support with minimal server load and no SEO penalty. In Flare, we can combine the above to serve our marketing site and documentation as quickly as possible and ensure that it gets indexed by search engines.