Storefront Production Optimization Tips

Rendering, caching, and query tips for production.

On this page

In this guide, you’ll find tips useful when optimizing a storefront for production.

Summary

When building a storefront, you want to ensure that it loads quickly and efficiently for your users. You can achieve this by implementing many optimizations, including:

  1. Implement appropriate rendering strategies.
  2. Use fetching libraries optimized for performance and caching like TanStack Query.
  3. Optimize queries to fetch only the necessary data.
  4. Implement optimistic UI updates for cart operations.

This guide explains how to implement these optimizations. You can follow it regardless of the frontend framework you use.

There are other important optimizations like lazy-loading images, code-splitting, and using CDNs. These optimizations depend on the frontend framework you use and your setup. This guide focuses on Aiminaabee-specific optimizations in your storefront.


Choose the Right Rendering Strategy

A rendering strategy defines how your frontend framework renders pages. The most common strategies are:

  1. Server-Side Rendering (SSR): Pages are rendered on the server for each request. Ideal for dynamic content that changes frequently.
  2. Static Site Generation (SSG): Pages are pre-rendered at build time. Best suited for content that doesn't change often.
  3. Incremental Static Regeneration (ISR): A hybrid approach where pages are pre-rendered at build time but can be updated at runtime. Perfect for content that changes occasionally.
  4. Client-Side Rendering (CSR): Pages are rendered in the browser using JavaScript. Optimal for highly interactive applications.

For your storefront, we recommend using different strategies for different pages:

Page TypeRecommended Strategy
HomepageSSG above the fold, CSR below the fold. For example, render the hero section at build time and load product recommendations client-side.
Product Listing Page (PLP)SSR or ISR. Fetch products on the server with region_id.
Product Detail Page (PDP)SSR or ISR for catalog fields. Pass region_id for prices; inventory can stay dynamic.
Cart and Checkout PagesServer components + server actions. Persist cart ID and JWT in httpOnly cookies.
Blog or Content Pages (About Us, Privacy Policy, etc...)SSG or ISR.
User Account PagesSSR with _aiminaabee_jwt.

On Next.js App Router, follow Build a Next.js Storefront: SDK calls in src/lib/data/*.ts, cache tags from _aiminaabee_cache_id, revalidate carts, customers, and fulfillment after mutations. TanStack Query below is optional for client islands, not a replacement for the server data layer.


Use TanStack Query

When fetching data from your Aiminaabee backend, consider using a fetching library optimized for performance and caching. There are many options available, but one of the most popular is TanStack Query.

TanStack Query is a powerful data-fetching library that provides features like caching, background updates, and optimistic updates.

By using TanStack Query, you can significantly improve your storefront's performance by reducing the number of network requests and ensuring that your UI is always up-to-date.

Learn how to get started with TanStack Query in their official documentation. Their documentation also has guidance on advanced usage and best practices.

Stale Time Configuration

When configuring TanStack Query, you can set the staleTime option to control how long data is considered fresh. However, avoid setting staleTime for highly dynamic data like product prices and stock levels.

Set the staleTime globally, then override it to 0 for specific queries that fetch dynamic data.

For example:

ts

tsCopy
// queryClient.ts
import { QueryClient } from "@tanstack/react-query"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
})
export default queryClient
// product.ts
// Used in components that are children of QueryClientProvider
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
export const useProduct = (id: string) => {
return useQuery(["product", id], async () => {
const response = await sdk.store.products.retrieve("us", id)
return response.data
})
}
export const useProductPrice = (id: string) => {
return useQuery(
["product-price", id],
async () => {
const response = await sdk.store.products.retrieve("us", id, {
fields: "*variants.calculated_price",
})
return response.data.variants[0].price
},
{
staleTime: 0, // Always fetch fresh data
}
)
}

Invalidate Queries

When performing mutations that change cached data, invalidate the relevant queries to ensure that the UI reflects the latest data.

For example, when adding an item to the cart, you should invalidate the cart query:

ts

tsCopy
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
export const useAddToCart = () => {
const queryClient = useQueryClient()
return useMutation(
async (item) => {
await sdk.store.cart.createLineItem("us", "cart_id", item)
},
{
onSuccess: () => {
queryClient.invalidateQueries(["cart"])
},
}
)
}

Optimize Fetched Data

When fetching data from your Aiminaabee backend, optimize your queries to fetch only the necessary data in the context of a component or page. This reduces the response size and time, improving your storefront's performance.

Aiminaabee's API routes accept a fields parameter that allows you to specify which fields and relations to include in the response.

For example, if you only need the product's id, title, and variants.calculated_price, you can specify this in the fields parameter:

ts

tsCopy
const response = await sdk.store.products.retrieve("us", "product_id", {
fields: "*variants.calculated_price, id, title",
})

This query will return only the specified fields, reducing the response size and improving performance.

If you're using TanStack Query, make sure to include the fields parameter in the query key to ensure that different field selections are cached separately:

ts

tsCopy
const useProduct = (id: string, fields?: string) => {
return useQuery({
queryKey: ["product", id, fields],
queryFn: async () => {
const response = await sdk.store.products.retrieve("us", id, { fields })
return response.data
},
})
}

Learn more about the fields parameter in the Store API reference.

Each Store API route restricts the fields that you can request through an allowed-fields list. If you request a field that the route doesn't allow, Aiminaabee omits it from the response without returning an error.

To allow extra fields, such as a custom relation, override the route's allowed fields as explained in the Allowed Fields documentation.


Implement Optimistic UI Updates

When performing mutations that may take some time, you can implement optimistic UI updates to provide a better user experience. This means updating the UI immediately, assuming the mutation will succeed, then rolling back if it fails.

For example, if you're using TanStack Query, you can implement the following optimistic cart-update utilities and then use them in cart mutations:

The following code snippets are not complete implementations. They are simplified for clarity. They assume that:

  • You defined separate cart data functions that use the JS SDK to call the Aiminaabee backend.
  • You have a query-keys.ts utility that defines consistent query keys for TanStack Query.

You can make changes as needed based on your implementation.

optimistic-cart.ts

ts

tsCopy
import { HttpTypes } from "@aiminaabeejs/types"
import { QueryClient } from "@tanstack/react-query"
import { queryKeys } from "@/lib/utils/common/query-keys"
/**
* Utility functions for optimistic cart updates
*/
export interface OptimisticCartItem {
id: string;
variant_id: string;
quantity: number;
title: string;
thumbnail?: string | null;
product_title?: string;
variant_title?: string;
product?: {
id: string;
title: string;
};
variant?: {
id: string;
title: string;
};
unit_price: number;
total: number;
isOptimistic?: boolean;
}
export interface OptimisticCart extends HttpTypes.StoreCart {
isOptimistic?: boolean;
}
/**
* Creates an optimistic cart item for immediate UI updates during add to cart operations.
* Generates a temporary item with calculated pricing before the server response.
*
* @param variant - The product variant being added to cart
* @param product - The product object containing title and thumbnail
* @param quantity - The quantity to add (defaults to 1)
* @returns Optimistic cart item with temporary ID and calculated totals
*
* @example
*

use-cart.ts

ts

tsCopy
import {
addToCart,
applyPromoCode,
createCart,
deleteLineItem,
removePromoCode,
retrieveCart,
updateCart,
updateLineItem,
} from "@/lib/data/cart"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { queryKeys } from "@/lib/utils/common/query-keys"
import {
addItemOptimistically,
createOptimisticCartItem,
getCurrentCart,
rollbackOptimisticCart,
updateLineItemOptimistically,
removeLineItemOptimistically,
createOptimisticCart,
} from "@/lib/utils/cart/optimistic-cart"
import { HttpTypes } from "@aiminaabeejs/types"
/**
* React hook to fetch the current cart with optimistic updates and caching.
* Uses Tanstack Query with no stale time to ensure fresh data.
*
* @param fields - Optional fields to include in the cart response
* @returns Tanstack Query result object with cart data, loading, and error states
*
* @example
*

With this setup, your cart mutations will provide immediate UI feedback with optimistic updates, while ensuring that the cart data is always fresh and consistent with the backend.