cimplify
TypeScript SDK

Performance & Optimization

How Cimplify storefronts cache and render fast, and the page-level knobs you control to keep them that way.

Cimplify storefronts are fast by default. Cimplify runs a multi-layer cache in front of every storefront so most requests never touch a render at all. This page covers the small set of things you control in your page code that keep that cache working: the rendering model, the canonical page shape, TTLs, static asset caching, and the anti-patterns that quietly defeat caching.

Use the ISR Previous Model, not Cache Components

Cimplify templates use Next 16's Previous Model:

  • export const revalidate = N at the page level
  • cacheOptions: { revalidate, tags } on SDK reads (forwarded as fetch().next.{revalidate,tags})
  • revalidateTag for invalidation
  • generateStaticParams on every [slug] route

Keep cacheComponents off in next.config.ts. The 'use cache' directive (Cache Components) is still experimental and its runtime constraints don't suit hosted storefronts. The Previous Model is stable, fully supported, and what every Cimplify template ships with.

app/products/[slug]/page.tsx (canonical PDP)
import { notFound } from "next/navigation";
import { getServerClient, tags } from "@cimplify/sdk/server";

// generateStaticParams: enumerate known slugs at build so the page is
// edge-cacheable. Placeholder fallback keeps the build alive when the
// catalogue API isn't reachable at build time.
export async function generateStaticParams() {
  const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });
  if (!r.ok || r.value.items.length === 0) return [{ slug: "__placeholder__" }];
  return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));
}

export const revalidate = 3600;

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const r = await getServerClient().catalogue.getProductBySlug(slug, {
    cacheOptions: { revalidate: 3600, tags: [tags.product(slug), tags.products()] },
  });

  // Distinguish a real 404 from a transient SDK error. Calling notFound()
  // on a transient error caches the not-found page for an hour, visitor
  // sees "This page couldn't load" until something else invalidates.
  if (!r.ok) {
    if (r.error.code === "NOT_FOUND") notFound();
    // Soft state: caller renders a skeleton; the next request retries.
    return <ProductDetailSkeleton />;
  }

  return <ProductDetail product={r.value} />;
}

Every storefront page should look like this. Five non-negotiable elements:

ElementWhy
generateStaticParams returning real slugsWithout it Next emits Cache-Control: no-store on [slug] routes and the response can't be cached
Placeholder fallback in generateStaticParamsIf the catalogue API is unreachable at build time, an empty array can fail the build. The placeholder keeps it alive
export const revalidate = 3600 (or your TTL)Without it Next treats the page as fully dynamic
cacheOptions: { revalidate, tags } on SDK readsTags are what revalidateTag and Cimplify's automatic invalidation target
Soft-render on transient SDK errorCalling notFound() on every !r.ok makes a brief upstream timeout look like a 404 for the entire revalidate window

TTL recommendations

Page typerevalidate valueWhy
Home, shop, search, sitemap, llms.txt3600 (1h)Browsed often, content changes ~daily
PDP (/products/[slug])3600Same; automatic invalidation handles real-time edits
Category, collection list pages3600Same
Cart, checkout, account(no revalidate)Per-user, never cache. A Cookie header bypasses the edge cache anyway
/api/v1/* (proxied)(no revalidate)The API handles its own caching

You can set revalidate as high as you like. Cimplify's automatic invalidation flips a cached page fresh within ~1–3 seconds of a merchant edit regardless of the TTL, so a 1-hour revalidate is not a 1-hour staleness window. The TTL is just the upper bound for content that isn't edit-driven.

Verifying the cache is working

Every storefront response carries an X-Cimplify-Edge-Cache: HIT | MISS | BYPASS header so you can confirm caching without guessing:

$ for i in 1 2 3; do
    curl -s -o /dev/null -w "ttfb %{time_starttransfer}s | " "$URL"
    curl -sI "$URL" | grep -i 'x-cimplify-edge-cache'
  done
ttfb 1.77s | X-Cimplify-Edge-Cache: MISS cold render, populates cache
ttfb 0.05s | X-Cimplify-Edge-Cache: HIT warm
ttfb 0.05s | X-Cimplify-Edge-Cache: HIT
  • BYPASS on a page that should be cached → the response is missing Cache-Control: s-maxage (usually a missing revalidate), or the request carried a Cookie/Authorization header.
  • MISS repeatedly on warm requests → the cache warms independently per region, so testing from rotating locations always shows a first MISS. Also check the page isn't emitting Cache-Control: max-age=0 or private.

Static asset caching

Everything above tunes the page (HTML) cache. Static assets (JS/CSS bundles, fonts, files in public/, uploaded brand assets) sit on a separate HTTP layer. Most of it is automatic, but a few habits quietly defeat it. The rule of thumb: content-hashed URLs are cached forever; stable URLs are revalidated.

AssetHow it's servedWhat you do
/_next/static/** (JS/CSS chunks)Content-hashed by next build, served Cache-Control: public, max-age=31536000, immutable. A new deploy mints new hashes, so there's no staleness riskNothing; automatic
Fonts via next/fontSelf-hosted, hashed into /_next/static/media, same immutable cachingPrefer over linking a font CDN: no extra origin, no handshake on a render-critical resource
Files in public/Served from the storefront origin but not hashed, so the URL is stable across edits. Served Cache-Control: public, max-age=0, must-revalidate (edge-cached, but the browser revalidates)Don't expect immutable caching here. For forever-cacheable assets, import through the bundler (gets a hashed /_next/static URL) or upload via cimplify assets
Uploaded brand assets via assetUrl()Long-lived CDN cache on storefrontassetscdn.cimplify.io, with on-the-fly transforms. Immutable per storage keyRe-uploading to the same key needs a new path (or a changed transform query) to bust
Catalogue images (Cloudinary)Long-lived immutable on the image CDNSize them at the source; see Responsive images

Don't cache-bust hashed assets

// ✗ wrong: the hash already versions the file; a changing query string
// forces a refetch on every load and poisons both browser and edge cache
<script src={`/_next/static/chunks/main.js?v=${Date.now()}`} />
<link rel="stylesheet" href={`${asset}?t=${buildId}`} />

Manual cache-busting on a content-hashed URL is always wrong. The filename hash is the version. Reference the asset as-is and let the immutable cache do its job. A new deploy changes the hash, so users get the new bytes the moment they land on the new HTML.

Don't import large media into the JS bundle

import hero from "./hero.png" for a multi-MB image gets a hashed URL and caches fine, but it bloats the build graph and ships the original at full resolution. Upload it via cimplify assets and reference it with assetUrl("hero/main.jpg", { w: 1600 }) so it streams from the CDN at the size you ask for.

Verifying asset caching

URL="https://<your-handle>.mycimplify.com"

# A hashed chunk should be immutable
asset=$(curl -s "$URL/" | grep -oE '/_next/static/[^"]+\.(js|css)' | head -1)
curl -sI "$URL$asset" | grep -i 'cache-control'
# → cache-control: public, max-age=31536000, immutable

If a hashed /_next/static asset comes back max-age=0 or no-store, something is rewriting headers in front of the storefront (a custom proxy, a headers() override in next.config.ts). Remove it; the platform sets the right header.

Client-side prefetching

Everything above tunes server rendering and caching. The React SDK also prefetches in the browser so the next interaction is usually a cache hit:

WhatWhen
Product data + primary imageA product card comes within 300px of the viewport, or on hover/focus
Next pagination pageAfter the current CataloguePage page renders
Neighboring gallery imagesWhile a product gallery is on screen
preconnect to the API and asset CDNOn provider mount; emitted in the SSR stream on React 19
First grid row imagesloading="eager" + fetchPriority="high"; everything below the fold is lazy

Speculative work is skipped for visitors with data-saver enabled or on 2g connections, capped to a few concurrent requests, deduplicated against the hook cache, and excluded from product-view analytics.

You control it from the provider: prefetch={{ mode }} with "aggressive" (default), "hover", or "off", plus rootMargin and images. Per-card override: <ProductCard prefetch="off" />. Pass linkComponent (e.g. next/link) to add route-level prefetch to SDK-rendered links. See React SDK: Prefetching.

SDK timeout

The SDK's default per-call timeout is 5 seconds, short on purpose, so one slow upstream call can't stall a whole page render. When a call exceeds it, Result.ok becomes false, the page renders with empty data (soft-render), and that result is cached so subsequent hits are instant.

Override only for genuinely-large bulk reads, never on a storefront request path:

import { createCimplifyClient } from "@cimplify/sdk";

// Admin tools, batch exports, etc. Never for storefront request paths.
const adminClient = createCimplifyClient({
  secretKey: process.env.CIMPLIFY_SECRET_KEY,
  timeout: 30_000,
});

Common anti-patterns

Calling notFound() on transient SDK errors

// ✗ wrong: caches the not-found page for an hour on any upstream blip
const r = await getServerClient().catalogue.getProduct(slug, {...});
if (!r.ok) notFound();
// ✓ right: distinguish 404 from transient
if (!r.ok) {
  if (r.error.code === "NOT_FOUND") notFound();
  return <Skeleton />;
}

Skipping generateStaticParams on [slug] routes

Without it Next emits Cache-Control: no-store on the route, defeating the cache entirely. next build will show the route as ƒ Dynamic instead of ● SSG.

Enabling cacheComponents: true "just to try it"

It's a global flag (you can't enable it for one page) and storefronts 5xx under it. The 'use cache' directive only works with the flag on. Don't.

Importing @cimplify/sdk/server/evict from a page

That subpath is for Cimplify's deploy pipeline only and has build-time dependencies a storefront doesn't install. From your page code, only ever import from @cimplify/sdk/server.

Don't try to build cart-aware page caching. Any request with a Cookie header bypasses the cache by design. Cart contents are per-user and can never be shared, and a stale cart in someone else's cache is a data-leak class of bug. Pull the cart into a client island and hydrate from the SDK on the browser.

What to do when something breaks

  1. TTFB suddenly slow on every page → check the X-Cimplify-Edge-Cache header is present and reads HIT on warm requests. Persistent MISS/BYPASS means the page lost its s-maxage (check revalidate is still declared).
  2. HIT but stale data → the page's cacheOptions.tags don't match the resource being edited. See Revalidation: tags must be keyed by database ID, not slug.
  3. Cache-Control: no-store on a [slug] pagegenerateStaticParams not declared, or returning empty without the placeholder fallback.
  4. React #419 on category/collection navigation → the notFound()-on-transient race. See Revalidation for the soft-render pattern.

Where next

On this page