Performance Best Practices for Writing PWA Kit-Based Storefronts

When designing your PWA kit app, take into account these performance best practices to make sure your storefront site provides an optimal user experience, loads quickly, and is responsive to user interactions.

Efficient data fetching is vital for a rapid LCP and a responsive user experience.

  • For new projects (PWA Kit v3+): Leverage withReactQuery. This tool streamlines data fetching on both the server and client, cleverly preventing redundant client-side requests for already-retrieved server data. This means cleaner code and better performance.
  • For older projects (optimizing getProps):
    • Be selective: Only return the precise data your component needs for its initial display. Avoid sending entire API responses to keep your HTML payload lean.
    • Fetch in parallel: If a page requires data from multiple APIs, use Promise.all to initiate these requests simultaneously, which is much faster than sequential fetching.
    • Graceful error handling: For critical issues (for example, product not found), throw an HTTPError to show a proper error page. For minor issues, pass an error flag via props for component-level handling, preventing crashes.
    • Client-side non-essentials: Any data not crucial for the initial, above-the-fold view (like reviews or related items) should be fetched on the client using a useEffect hook. This accelerates initial page load, boosting TTFB and LCP.
  • Server-side data fetching: Fetch critical data during SSR to ensure the initial HTML contains all necessary content.
  • API caching: Implement robust caching for API responses, both on the server and client-side (for example using stale-while-revalidate strategies).
  • Batching/debouncing: Batch multiple small API requests into one where possible, or debounce rapid requests.
  • Small payload size from API calls or queries: For APIs and queries, such as GraphQL queries, make sure sure you only get the data you need and the payload is as small as possible. For example, for B2C Commerce API (SCAPI), check out these tips in Shopper API Performance and Best Practices to reduce the response size.

Your client-side React code heavily influences INP. Check out these tips to fine-tune it.

  • Analyze bundle size: Use tools like Webpack Bundle Analyzer to identify large modules and dependencies.
  • Split your code.
    • The PWA Kit is preconfigured for code splitting with Webpack. Use the loadable utility to dynamically load page-level components. For example, product detail page code only downloads when visited, reducing initial bundle size.
    • Implement dynamic imports (React.lazy() and Suspense) to split your application into smaller chunks that are loaded on demand.
  • Lazy-load hidden content: For substantial components located "below the fold" or within modals, implement lazy loading.
  • Eliminate redundant renders: Unnecessary rerenders are a primary cause of poor INP. Master React's memoization hooks:
    • React.memo: Wrap components to prevent rerendering if their props haven't changed, ideal for simple display components.
    • useCallback: When passing functions to memoized child components, wrap them in useCallback. This maintains function reference stability, preventing needless child re-renders.
    • useMemo: Use useMemo for expensive computations, caching results to avoid recalculation on every render.
  • Smart state management: While the Context API is useful, be cautious; any update to a context re-renders all components using it. For complex states, divide your contexts into smaller, logical units (e.g., UserContext, CartContext) to limit rerenders.
  • Tree shaking: Ensure your build process (Webpack/Rollup) effectively removes unused code.
  • Minification and compression: Minify JavaScript (JS) files. To further help with compression, the Managed Runtime CDN uses Gzip/Brotli compression for serving files if the browser supports it.
  • Reduce the JavaScript payload: All JS scripts, including third-party scripts, use compression to reduce the size of the JS files. We recommend that the server use compression as well behind the CDN, or behind your own CMS or another system. We also recommend that third-party systems also use compression.
  • Progressive hydration: Prioritize hydration of critical components first.
  • Optimize hydration: Use the <Island/> component in PWA Kit 3.11 to control which components to hydrate on the client. By deferring hydration to only essential components, you benefit from server-rendering at scale without the overhead of hydrating the entire page at once. See the Island component release notes and the Islands readme on GitHub.
  • Minimize initial interactive components: Reduce the number of complex, interactive components loaded on the initial page view.

Maximizing your CDN cache hit ratio is the most effective way to enhance LCP for most users.

  • Set precise cache-control headers.

    • Per-page control: Within a page's getProps function, define custom cache durations. A static "About Us" page might cache for days, while a product page could refresh every 15-30 minutes.
    • stale-while-revalidate Magic: This header (Cache-Control: s-maxage=600, stale-while-revalidate=3600) instructs the CDN to serve cached content for 10 minutes. If a request arrives afterward, it instantly delivers the older content, ensuring a fast user response, while fetching a fresh version in the background. It's the perfect balance of speed and content freshness.
  • Create cache-friendly components: For server-rendered HTML to be cached broadly, it must be generic. Personalized content (like a user's name or cart quantity) must only render on the client. A simple method is to wrap it in a conditional check: typeof window !== 'undefined' && <MyPersonalizedComponent />, ensuring it renders solely in the browser.

  • Clean up query parameters: Marketing URLs often contain "unnecessary" parameters (e.g., gclid, utm_tags) that create unique URLs, hindering caching effectiveness. Modify the processRequest function in app/request-processor.js to strip these parameters before the cache check, allowing many different URLs to access the same cached page.

  • Cache your APIs: By default, proxied requests aren't CDN-cached. If you want a proxied request to be cached, simply change its path prefix from proxy to caching.

    Caching proxies isn't suitable for the B2C Commerce API; use its Server-Side Web-Tier Caching feature instead.

  • Perform browser caching: Leverage HTTP caching headers, for example, Cache-Control, and Expires, for static assets (JS, CSS, images).

Third-party scripts are notorious performance drains. Manage them effectively.

  • Audit and justify: Inspect your Network panel in Chrome DevTools. List all external scripts. For each, ask: "Is this essential?" Eliminate any whose value doesn't outweigh their performance cost.
  • Load asynchronously: Never load external scripts synchronously. Always use the async or defer attribute. async allows downloads without blocking the page, while defer ensures execution only after the page has fully parsed.
  • Lazy-load widgets: For elements like chat widgets or social media buttons, avoid initial loading. Use JavaScript to load the script only when the user scrolls nearby or clicks a placeholder.
  • Utilize a Consent Management Platform (CMP): A robust CMP integrated with Google Tag Manager (GTM) is crucial. It prevents marketing and ad tags from loading until user consent is given, benefiting both privacy and performance.
  • Check your service worker: Your PWA's service worker might block requests to unapproved domains. When adding new external scripts, ensure their domains are correctly configured in your service worker to prevent blocking.
  • Analyze unused JavaScript and CSS: Use Chrome development tools to find JavaScript code and CSS that's not being used. Removing unused code can improve page load performance and helps reduce mobile data usage for users. For more information, see Coverage: Find unused JavaScript and CSS in the Chrome documentation.

To achieve optimal performance during page load, use system fonts, or minimize the size of web fonts and improve their discovery. Large web font files can slow a page's performance during load time and rendering. Large web font files take longer to download in the browser and negatively affect First Contentful Paint (FCP). The incorrect font-display value could cause undesirable layout shifts that contribute to a page's Cumulative Layout Shift (CLS).

  • Font Discovery: Browsers use @font-face to find fonts. You can help them discover fonts earlier with the preload directive or by inlining the @font-face declaration.
  • System Fonts: Using system fonts improves the page performance by avoiding the need to download fonts and reducing render-blocking. For examples of system fonts, see Fonts for Apple platforms and Windows 11 font list.
  • Font Download: If you use custom web fonts, it's recommended to self-host your web fonts. Use the WOFF2 format for its superior compression and subset fonts to reduce file size by including only the necessary characters.
  • Font Rendering: The font-display CSS property controls how text is shown while a font loads. The swap value is often recommended because it immediately shows a fallback font and then swaps in the web font once it's loaded, which helps prevent a flash of unstyled text.

For more details, see Optimize web fonts in web.dev.

Images typically form the largest part of a page. Optimizing them is mandatory.

  • Use the Dynamic Imaging Service: Use the Dynamic Imaging Service for image optimization, resizing, and format conversion. Optimizing images with the Dynamic Imaging Service, together with using responsive image formats, is one of the main ways you can gain the largest performance boost for your web pages.

  • Serve responsive images: Use the <picture> element or the srcset and sizes attributes on your <img> tags. This enables the browser to select the ideal image size for the device based on screen size and device pixel ratio, preventing phones from downloading massive desktop images.

  • Use the <DynamicImage/> Component: Create responsive images with the <DynamicImage/> component, which enables responsive preloading for images. Also, the response image sizes on Product Listing Pages (PLPs) consider the impact of the search refinements panel, reducing unnecessary image fetches. See the DynamicImage release notes.

  • Use Modern Formats:

    • Prioritize modern image formats like the WebP format for images, which offers significantly better compression than JPEG or PNG, often reducing file size by 25-35% with no perceptible quality loss.

    Cloudflare only supports WebP. If using a third-party image provider, check for more modern options like AVIF.

    • Provide fallbacks for older browsers.
  • Compress aggressively: Use an image optimization service or build tools to compress your images. A JPEG quality of 85 usually strikes a great balance.

  • Prevent layout shifts with dimensions: This is a simple yet effective fix for CLS. Always include width and height attributes on your <img> and <video> tags. This allows the browser to reserve the correct space before the media loads, preventing annoying content jumps.

  • Lazy-load offscreen images: For any image not within the initial view, add the native loading="lazy" attribute or Intersection Observer API. This instructs the browser to defer loading these images until the user scrolls into their vicinity, substantially enhancing performance. This tip applies to videos as well.