Optimizing Remix.js Performance: A Cache-Driven Approach
My Remix.js site had a Largest Contentful Paint (LCP) of 5.68 seconds. For a content site, that's terrible. The culprit was obvious: every page load trigg...
26 Dec 2023

My Remix.js site had a Largest Contentful Paint (LCP) of 5.68 seconds. For a content site, that's terrible. The culprit was obvious: every page load triggered multiple API calls to my Strapi.js backend. The data rarely changed, but it was fetched fresh every time.
The fix was caching. Specifically, HTTP caching.

The approach
In Remix, loaders run on every request. If your loader fetches from an external API, that request adds latency to every page load. HTTP caching tells the browser (and CDN) to reuse a previous response instead of making a new request.
I added Cache-Control headers to my loader responses:
"Cache-Control": "public, s-maxage=3600"
And configured the server response headers:
responseHeaders.set("Cache-Control", "public, max-age=3600")
max-age=3600 tells the browser to cache the response for one hour. s-maxage=3600 tells CDN edge servers to do the same. public means the response can be cached by shared caches, not just the user's browser.
The result
After adding caching headers, the LCP dropped from 5.68 seconds to under 2 seconds on cached requests. The first request to a page is still slow (it hits the backend), but every subsequent request within the cache window is instant.
The trade-offs
Caching means users might see stale data. If I publish a new article, it won't appear for up to an hour on cached pages. For a blog, that's acceptable. For an e-commerce site with live pricing, it's not.
You can mitigate this with shorter cache durations, cache invalidation (purging the CDN cache when content changes), or stale-while-revalidate, which serves the cached version immediately while fetching a fresh one in the background.
"Cache-Control": "public, max-age=60, stale-while-revalidate=3600"
This serves cached content for 60 seconds, then serves stale content for up to an hour while revalidating in the background. The user always gets an instant response. The data is at most 60 seconds old during normal operation.
When to cache
Cache aggressively for content that changes infrequently: blog posts, documentation, product listings. Cache sparingly (or not at all) for user-specific data: dashboards, carts, account pages.
The fastest network request is the one you never make.