Storefront Production Optimization Tips

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.

Note: 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 Medusa-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 Type

Recommended Strategy

Homepage

SSG 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)

ISR or CSR.

Product Detail Page (PDP)

SSG for product content that doesn't change often, such as descriptions and images. Use CSR for dynamic content like stock availability and prices.

Cart and Checkout Pages

CSR.

Blog or Content Pages (About Us, Privacy Policy, etc...)

SSG or ISR.

User Account Pages

CSR.

How you implement these strategies depends on the frontend framework you use. Refer to the documentation of your framework for guidance.


Use TanStack Query#

When fetching data from your Medusa 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:

Code
1// queryClient.ts2import { QueryClient } from "@tanstack/react-query"3
4const queryClient = new QueryClient({5  defaultOptions: {6    queries: {7      staleTime: 1000 * 60 * 5, // 5 minutes8    },9  },10})11
12export default queryClient13
14// product.ts15// Used in components that are children of QueryClientProvider16import { useQuery } from "@tanstack/react-query"17import { sdk } from "../lib/sdk"18
19export const useProduct = (id: string) => {20  return useQuery(["product", id], async () => {21    const response = await sdk.store.products.retrieve(id)22    return response.data23  })24}25
26export const useProductPrice = (id: string) => {27  return useQuery(28    ["product-price", id],29    async () => {30      const response = await sdk.store.products.retrieve(id, {31        fields: "*variants.calculated_price",32      })33      return response.data.variants[0].price34    },35    {36      staleTime: 0, // Always fetch fresh data37    }38  )39}

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:

Code
1import { useMutation, useQueryClient } from "@tanstack/react-query"2import { sdk } from "../lib/sdk"3
4export const useAddToCart = () => {5  const queryClient = useQueryClient()6
7  return useMutation(8    async (item) => {9      await sdk.store.cart.createLineItem("cart_id", item)10    },11    {12      onSuccess: () => {13        queryClient.invalidateQueries(["cart"])14      },15    }16  )17}

Optimize Fetched Data#

When fetching data from your Medusa 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.

Medusa'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:

Code
1const response = await sdk.store.products.retrieve("product_id", {2  fields: "*variants.calculated_price, id, title",3})

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:

Code
1const useProduct = (id: string, fields?: string) => {2  return useQuery({3    queryKey: ["product", id, fields],4    queryFn: async () => {5      const response = await sdk.store.products.retrieve(id, { fields })6      return response.data7    },8  })9}

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


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:

Note: 

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 Medusa 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.

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.

Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Medusa MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break