Caching Responses

Caching responses is critical for keeping your site as fast as possible, both for pages as well as middleware.

A good default is to use stale-while-revalidate caching for all responses.

For instance, we can add an onGet export to our root layout (src/routes/layout.tsx) like so, to apply good caching defaults site-wide:

src/routes/layout.tsx
import { component$, Slot } from "@qwik.dev/core";
import type { RequestHandler } from "@qwik.dev/city";
 
export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    // Always serve a cached response by default, up to a week stale
    staleWhileRevalidate: 60 * 60 * 24 * 7,
    // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
    maxAge: 5,
  });
};
 
export default component$(() => {
  return (
    <main class="mx-auto max-w-[2200px] relative">
      <Slot />
    </main>
  );
});

With the above setup, you will not only have better performance (pages are always served instantly from cache), but you can also have significantly decreased hosting costs, as our server or edge functions only need to run at most once every 5 seconds per page.

cacheControl

Any method that takes a request event can call request.cacheControl to set the cache control headers for the response:

src/routes/layout.tsx
import type { RequestHandler } from "@qwik.dev/city";
 
export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    public: true,
    maxAge: 5,
    sMaxAge: 10,
    staleWhileRevalidate: 60 * 60 * 24 * 365,
  });
};

If you have default caching set at the root, but want to disable caching for a specific page, you can override this setting using nested layouts. The below example overrides caching for dashboard pages.

src/routes/dashboard/layout.tsx
import type { RequestHandler } from "@qwik.dev/city";
 
// Override caching for /dashboard pages to not cache as they are unique per visitor
export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    public: false,
    maxAge: 0,
    sMaxAge: 0,
    staleWhileRevalidate: 0,
  });
};
 

You can see the full API reference of options you can pass to request.cacheControl.

When not to cache

Caching is generally beneficial, but not right for every page all the time. If your site has URLs that will show different content to different people โ€” for example, pages exclusive to logged-in users or pages that show content based on a user's location โ€” you should avoid using cache-control headers to cache these pages. Instead, render the content of these pages on the server side on a per-visitor basis.

For high traffic pages that look the same to everyone, such as a homepage, caching is great for enhancing performance and reducing cost. For pages specifically for logged in users that may have less traffic, it may advisable to disable caching.

You can conditionally change cache behaviors with any logic you like:

src/routes/layout.tsx
import type { RequestHandler } from "@qwik.dev/city";
 
export const onGet: RequestHandler = async ({ cacheControl, url }) => {
  // Only our homepage is public and should be CDN cached. Other pages are unique per visitor
  if (url.pathname === '/') {
    cacheControl({
      public: true,
      maxAge: 5,
      staleWhileRevalidate: 60 * 60 * 24 * 365,
    });
  }
};

CDN Cache Controls

For even more control on your caching strategy, your CDN might have another layer of cache control headers.

The cacheControl convenience method can receive a second argument (set to "Cache-Control" by default). You can pass in any string value specific to your CDN such as "CDN-Cache-Control", "Cloudflare-CDN-Cache-Control", "Vercel-CDN-Cache-Control", etc.

cacheControl({
  maxAge: 5,
  staleWhileRevalidate: 60 * 60 * 24 * 365,
}, "CDN-Cache-Control");

Missing Controls

Some CDNs (such as Vercel Edge) may strip some of your "Cache-Control" headers.

On Vercel's documentation:

If you set Cache-Control without a CDN-Cache-Control, the Vercel Edge Network strips s-maxage and stale-while-revalidate from the response before sending it to the browser. To determine if the response was served from the cache, check the x-vercel-cache header in the response.

If your CDN, such as Vercel Edge, automatically removes certain cache control headers and you wish to implement caching strategies like "stale-while-revalidate" or "s-maxage" in the browser, you can specify an additional cacheControl:

src/routes/layout.tsx
import type { RequestHandler } from "@qwik.dev/city";
 
export const onGet: RequestHandler = async ({ cacheControl }) => {
    // If you want the browser to use "stale-while-revalidate" or "s-maxage" Cache Control headers, you have to add the second cacheControl with "CDN-Cache-Control" or "Vercel-CDN-Cache-Control" on Vercel Edge 
    cacheControl({
      staleWhileRevalidate: 60 * 60 * 24 * 365,
      maxAge: 5,
    });
    cacheControl({
      maxAge: 5,
      staleWhileRevalidate: 60 * 60 * 24 * 365,
    }, "CDN-Cache-Control");
};

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • steve8708
  • harishkrishnan24
  • maiieul
  • igorbabko
  • mrhoodz
  • mhevery
  • chsanch
  • hamatoyogi
  • Jemsco