What pages should I cache?
The pages that are good candidates for caching are those that are typically accessible as external links, such as the homepage, product pages, category pages, and CMS pages. People may decide to share these pages with their friends or save them in their bookmarks. Search engines will also index these pages and display them in search results.
Any page that users access via an external link will be served via SSR (Server-Side Rendering), so caching them will improve the site's performance.
Some of the worst candidates for caching are checkout or profile edit pages. These pages are usually accessed by users only as part of a specific session and contain a lot of personalized data, making them inefficient for caching.
Making SSR Cacheable
There are two primary ways in which a page may be generated: by the browser or by the server.
When a user navigates to a URL, the server generates the HTML and transfers it to the browser, where it is immediately displayed. Although the website won't be interactive yet, the perceived performance will be better, as the user can look around even before it becomes interactive. This process is called server-side rendering (SSR). Subsequently, the server sends JavaScript and the state of the UI framework to the browser, and through a mechanism known as hydration, the website becomes interactive.
For a page to be cached, the HTML must reach the viewer's computer via the internet. This is not what happens when users navigate through pages via the website's navigation. In such cases, the server doesn't render the HTML anymore; instead, the browser does. This is called client-side rendering (CSR). The browser sends a request to the server, the server sends the data back, and it is the browser that renders the HTML.
Introducing a CDN to your storefront requires changes to how your application is built. One of the most important changes is removing time-sensitive and session-specific content that should not be cached.
Time-sensitive data
Some data, such as stock levels or prices, may change too frequently to be cached for long periods. To decide whether this data should be cached, consider how often it changes and the importance of having the most current information. This decision also depends on the TTL (time-to-live) cache settings, which determine how long data should be cached. A longer TTL setting means the data can be outdated for a longer period.
Personalized and session-specific data
Personalized data is another type of data that should not be cached. This includes data specific to a user or a group of users, such as their shopping cart, wishlist, promotions, or recently viewed products.
A CDN works by storing the outputs of server-side rendering (SSR) and associating them with a specific key.
[key-a]: html-for-page-a
[key-b]: html-for-page-b
[key-c]: html-for-page-c
...
When a request arrives, the CDN generates a key based on the request URL and some cookies. The CDN then checks if that key already contains a cached response. If it does, the cached content assigned to this key is retrieved and sent. Otherwise, the CDN relays the request to the Storefront’s server and caches the response under the key generated from the request.
This means that every time the CDN interprets an incoming request as a certain key, the HTML returned during SSR has to be the same. Otherwise, users will receive cached content that may differ from what they are expecting.
Now, let's imagine what would happen if this isn't the case and the same request can generate different HTML. A common example of this is personalizing a user's page based on their session. The SSR will generate different HTML for the same request, meaning that caching will not work properly.
Consider a user visiting an e-commerce site. When the user is logged in, the homepage might display personalized recommendations, a welcome message with their name, and their shopping cart contents. If the cookie identifying the user is being taken into the a count when generating the key, then CDN will be able to only create user-specific cache and most of the visiting users won't benefit from the CDN. If the cookies is ignored then users might see the previous user's recommendations, welcome message, and shopping cart. This occurs because the CDN returns the cached HTML based on the request URL, not taking into account the personalized session data.
Issues with Caching Personalized Content
When a request comes from customer A for an endpoint /products/skateboard, their session data is stored as a cookie. Since the CDN is unaware of how the storefront handles cookies, it creates a key called /products/skateboard.
Then, our application generates a response during SSR. The response includes some personalized content for customer A. For example, each page includes a slider with recommended products specific to the customer.
So the current cache of the CDN looks as follows:
/products/skateboard: Product page for skateboard, with recommended products for customer A
Now, let's consider what happens when a request comes from customer B for the same endpoint /products/skateboard. Since the CDN already has a cached response for this URL, it will return the cached content, which includes the personalized recommendations for customer A. This results in customer B seeing customer A's personalized content, which is not only confusing but also a potential breach of privacy.
To address the issue of caching personalized content while using a CDN, here are two approaches:
- Configure the CDN to take into a count the cookies:
- This approach allows the CDN to differentiate cached responses based on customer group IDs.
- Drawback: This method reduces cache efficiency because each customer group will have a different cache, making cache hits less likely.
- Ensure SSR responses are as consistent as possible and avoid caching personalized content:
- Design the SSR response to be uniform across all users, avoiding personalized content in the SSR output.
- Any personalized content should be fetched and rendered on the client side after the initial page load.
- This approach maintains cache efficiency because the CDN can serve the same cached content to all users.
- Personalization is handled dynamically on the client side, ensuring that each user sees the correct content without compromising cache effectiveness.
Examples of Personalized Content
- Prices Specific to a Customer or Customer Group:
- Different customers or customer groups might receive special pricing or discounts.
- Example: Regular customers get a 10% discount on all products.
- Recommendations Based on User Behavior:
- Product recommendations tailored to individual users based on their browsing and purchase history.
- Example: A slider showing "Products You May Like" based on previous purchases or viewed items.
- User-Specific Data:
- Shopping Cart: Items added to the cart by a specific user.
- Wishlist: Items a user has saved to their wishlist.
- Recently Viewed Products: A list of products a user has recently viewed on the site.
Skeletons
Every piece of the UI that we defer to the client side should have a placeholder, known as a skeleton. A skeleton is a simplified version of the UI element that will be displayed while the data is being fetched. It should have the exact dimensions of the content that will be displayed to avoid impacting the arrangement of the page. Layout shifts, which occur when elements on a webpage move around unexpectedly as new content loads, are performance-heavy and can confuse users, so it's best to avoid them.
How to remove the session-specific and time-sensitive data?
We can remove elements from the SSR by explicitly rendering them only on the client side. This way, the SSR response will only contain content that is consistent for all users.
All the session-specific content should be wrapped within the useEffect
hook. The hook will be executed only on the client side, so the data will be fetched only when the page is loaded in the browser.
While the data is being fetched, we should display a skeleton to indicate to the user that content is still loading.
For our earlier example of recommended products, this would look like this:
Let’s create a skeleton for a product tile.
// apps/storefront-unified-nextjs/components/ui/product-skeleton.tsx
interface ProductSkeletonProps {
count: number;
}
export default function ProductSkeleton({ count }: ProductSkeletonProps) {
return (
<div className="flex gap-4">
{Array.from({ length: count }, (_, itemIndex) => (
<div
className="animate-pulse border border-neutral-200 rounded-md w-[190px]"
key={itemIndex}
data-testid="skeleton-item"
>
<div className="w-full">
<div className="p-3 border-b border-neutral-200">
<div
className="h-4 bg-gray-300 rounded-md"
data-testid="skeleton-title-bar"
/>
</div>
<ul className="p-4 space-y-3">
{Array.from({ length: 4 }, (__, contentIndex) => (
<li
className="w-full h-4 bg-gray-200 rounded-full"
key={contentIndex}
data-testid="skeleton-detail-line"
/>
))}
</ul>
</div>
</div>
))}
</div>
);
}
Now, let’s defer loading to the client side and use our newly created skeleton.
// apps/storefront-unified-nextjs/components/recommended-products.tsx
import { SfScrollable } from '@storefront-ui/react'; import ProductSkeleton from '@/components/ui/product-skeleton'; import { useSdk } from '@/sdk/alokai-context';
import { getSdk } from '@/sdk'; ...
export default async function RecommendedProducts() { export default function RecommendedProducts() { const sdk = getSdk();
const sdk = useSdk();
const [recommendations, setRecommendations] = useState();
useEffect(() => {
// your recommendations pulling method
sdk.recommendations.getRecommendations.then((data) => setRecommendations(data));
}, []);
...
return (
...
{!recommendations.data && (
<SfScrollable>
<ProductSkeleton count={8} />
</SfScrollable>
)}
{!!recommendations.data?.length && <ProductSlider navigation="floating" products={recommendations.data} />}
...
);
}