This episode covers caching and performance in Remix: cache headers with cache control, data revalidation and stale-while-revalidate, image optimization and asset caching, and performance monitoring with Lighthouse.

With configuration and environment ready, it's time to talk about speed. Caching is one of the most effective ways to make an application feel fast — and in Remix, control over caching is very explicit because every route can set its own HTTP headers.
The key point: in Remix, you control HTTP headers directly. The right Cache-Control lets CDNs and browsers store pages so requests don't always reach the server. This differs from frameworks that hide HTTP behind abstractions.
Episode 9 covers cache headers, stale-while-revalidate, image and asset optimization, and performance monitoring with Lighthouse.
Every route can export a headers function to set response headers, including Cache-Control. This function receives the loader result and parent headers, then returns the final headers.
export function headers() {
return {
"Cache-Control": "public, max-age=60, s-maxage=3600",
};
}max-age controls caching in the browser; s-maxage controls caching at the CDN and proxies. With the combination above, the browser stores for 60 seconds and the CDN for 1 hour — a balance between speed and data freshness.
The general rules:
Warning
Be careful with public caching on pages that display private data. Cache-Control "public" means the CDN is allowed to store copies of the page — if its content is specific to a particular user, use "private" or don't cache it at all.
Stale-while-revalidate keeps a page considered fresh even when its cache age passes the limit. When the limit is exceeded, users still get the old version while the server loads the new version in the background. Remix and modern CDNs support this pattern through the Cache-Control header.
export function headers() {
return {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
};
}stale-while-revalidate=300 means the old version may be served for up to 300 extra seconds. Users never wait, and the data is never staler than 5 minutes.
For data read by loaders but changed through actions, Remix already handles automatic revalidation: after an action finishes, all loaders of the involved routes are called again. You don't need to write manual synchronization code — just make sure the loader always reads from the source of truth.
For applications that don't use an image optimization service, place images in the public folder or import them through the bundler. Importing through the bundler has an advantage: file names get a content hash, so browsers can cache images forever — a new file automatically gets a new name.
public/logo.a1b2c3.png <- generated by Vite, unique name per contentThe hash on the file name allows a very long Cache-Control without stale content risk. This is the standard way to cache assets: immutable because the name changes when the content changes.
Besides caching, pay attention to how images load: use the right dimensions so they don't exceed the required size, and consider modern formats like WebP. Large images are the biggest contributor to page weight after JavaScript.
The same rules apply to fonts. Avoid loading several font variants at once; use subsets and proper loading techniques. Every unnecessary byte means longer render time, especially on mobile networks.
Lighthouse measures various metrics: time until content is visible (FCP), time until ready for interaction (TTI), and the accessibility score. Run an audit from the browser devtools:
npx lighthouse http://localhost:3000 --preset=desktopThe Lighthouse result gives scores and a prioritized list of fixes. Run it against a production build, not the dev server, because the results are more representative.
Beyond Lighthouse, use the Network tab in devtools to inspect response headers directly. Check whether Cache-Control is set correctly, the size of each asset, and whether any assets are loaded repeatedly without need. Combining Lighthouse and devtools gives a complete picture of your page.
Focus on metrics with real impact: reduce the JavaScript sent, shrink images, and use caching. A score of 100 doesn't automatically mean the application feels fast — combine audits with real experience. For real-user monitoring, episode 22 covers observability in more depth.
Episode 9 gives you a performance toolkit: cache headers through the headers function, the stale-while-revalidate pattern for semi-dynamic data, image and asset optimization with hashes, and continuous auditing with Lighthouse. Speed can now be measured, not just felt.
The key takeaways:
In the next episode, episode 10, we'll discuss database and persistence — integrating ORMs like Prisma, setting up connection pooling and environment-based configuration, querying data in loaders and actions, and transaction handling for safe persistence. Caching makes pages fast; the database makes them meaningful.