Use optimization strategies to build fast storefronts. Follow performance best practices for web fonts, resource hints, bundle optimization, and third-party scripts.
Note
This document focuses on areas not covered elsewhere. For in-depth documentation about other performance-related topics, check out these links.
- Data Fetching: Server-load everything, data classification, loaders, actions, and fetchers.
- Loading States: Suspense boundary granularity, lazy loading overlays, and skeleton vs. spinner tradeoffs.
- State Management: URL state, optimistic UI, and avoiding derived state.
- Images: DIS integration,
<DynamicImage>component,DynamicImageProvider, image utilities, performance checklist, and alt text strategy.
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 take longer to download and negatively affect First Contentful Paint (FCP). An incorrect font-display value can cause layout shifts that contribute to Cumulative Layout Shift (CLS).
Self-host web fonts instead of loading them from third-party CDNs like Google Fonts. Self-hosting eliminates cross-origin DNS lookups and connection setup, avoids browser cache partitioning (browsers isolate third-party CDN caches per site), and is required for GDPR compliance, since loading fonts from external CDNs transmits the visitor's IP address to that third party on every page load. A 2022 ruling by the Munich Regional Court established this as a GDPR violation applicable across the EU. The alternative of gating external font loading behind a consent manager preserves CDN delivery but degrades the experience for users who haven't consented and adds implementation complexity. For details, see Google Fonts and GDPR.
Browsers use @font-face to find fonts. Help the browser discover fonts earlier by inlining the @font-face declaration in the <head> and adding a <link rel="preload"> directive. Without preload, the browser doesn't request the font until it computes a style that references it, adding a waterfall delay.
Use the WOFF2 format for its superior compression. Prefer variable fonts because a single file covers multiple weights, reducing the number of requests and preload hints. Subset fonts to include only necessary characters when the full Unicode range isn't needed.
The font-display CSS property controls how text is shown while a font loads. Use swap to immediately show a system fallback font and swap in the web font once loaded, avoiding Flash of Invisible Text (FOIT). Use optional to eliminate the swap-induced layout shift entirely. The web font is used only if it arrives before first render, otherwise the system font persists.
Using system fonts avoids the font download entirely and eliminates render-blocking. For examples of system fonts, see Fonts for Apple platforms and Windows 11 font list.
For more detail, see Optimize web fonts on web.dev.
The template ships with Sen, a self-hosted variable font that applies the recommendations above:
| Aspect | Implementation | File |
|---|---|---|
| Font file | public/fonts/sen-variable.woff2 (~22 KB, variable, weight 400–800) |
— |
| Preload | <link rel="preload" as="font" type="font/woff2" crossorigin="anonymous"> |
src/root.tsx |
@font-face |
Inline <style> in <head> with font-display: swap |
src/root.tsx |
| Fallback stack | 'Sen', -apple-system, 'system-ui', 'Helvetica Neue', Arial, sans-serif |
src/theme/tailwind.css |
When replacing Sen with a different font, update the font file in public/fonts/, the preload hint and inline @font-face in src/root.tsx, and the --font-sans / --font-serif / --font-mono variables in src/theme/tailwind.css. Choose a system fallback with similar metrics to minimize the visual shift during swap.
Resource hints tell the browser to start DNS lookups, TCP connections, or resource downloads before they're needed, reducing latency when those resources are eventually requested.
The template renders resource hints in the <head> based on configuration values in config.server.ts, so they can be tuned per environment without code changes (src/root.tsx).
appConfig.links.preconnect: Origins the browser should open early connections to (DNS + TCP + TLS). Use for services that will definitely be contacted on every page, such as the image CDN. The template preconnects to the DIS host by default.appConfig.links.prefetchDns: Origins for DNS-only prefetching. Lighter thanpreconnect, appropriate for services that may or may not be contacted (for example, analytics, optional third-party APIs).appConfig.links.prefetch: Specific resources to fetch and cache in the background. Use sparingly, as prefetched resources consume bandwidth regardless of whether the user navigates to them.
# Override via environment variables
PUBLIC__app__links__preconnect='["https://edge.dis.commercecloud.salesforce.com"]'
PUBLIC__app__links__prefetchDns='["https://analytics.example.com"]'Warning
Only preconnect to origins that are actually used on every page. Each preconnect opens a TCP and TLS connection eagerly, so unused preconnects waste the browser's connection budget and can delay more important requests. Performance audits such as Lighthouse's "Avoid unnecessary preconnects" will flag this. If an origin is only used on some pages (for example, a payment provider on checkout), prefer dns-prefetch instead. DNS lookups are cheaper and don't trigger warnings when unused.
Vite handles tree-shaking, minification, and chunk splitting automatically. Follow these practices to help keep bundles small.
- Split your code. Vite automatically splits each route into its own chunk, so users only download the JavaScript for the page they're visiting. For components that aren't needed on initial render, for example modals, drawers, rich editors, or heavy below-the-fold content, use
React.lazy()with deferred mounting to split them into separate chunks that are loaded on demand. See Lazy Loading for Overlays for the pattern. For large route-specific component groups, usemanualChunksinvite.config.tsto control how Rollup groups modules. The template uses this to split checkout components and per-locale translation files into dedicated chunks that are only loaded when needed. - Analyze and monitor bundle size. Run
pnpm bundlesize:analyzeto generate an interactive visualization of client and server bundles (opensbuild/client-bundle-size.htmlandbuild/ssr-bundle-size.html). Runpnpm bundlesize:testto verify against configured size limits — CI enforces these checks on every PR. - Avoid large dependencies for small tasks. Before adding a library, check its bundle size (for example, via bundlephobia). A 50 KB utility library for a function you could write in 10 lines is not a good tradeoff.
- Compression is handled automatically. Managed Runtime (CloudFront) applies Gzip/Brotli compression to responses at the edge — no application-level configuration is needed.
Unnecessary re-renders inflate Interaction to Next Paint (INP) and degrade responsiveness. Here are the most impactful optimizations.
- Split React Contexts by concern. One context per domain (theme, locale, and user). A single large context re-renders all consumers on every value change — even consumers that don't use the changed value. See State Management.
- Memoize expensive computations. Use
useMemofor derivations that are genuinely expensive. Don't memoize everything, since the overhead of memoization exceeds the cost of cheap computations. - Stabilize callback references. When passing callbacks to memoized child components, wrap them in
useCallbackto prevent the child from re-rendering on every parent render. - Use
React.memoselectively. Wrap components that re-render often with unchanged props. Don't apply it broadly because it adds comparison overhead and obscures the component tree.
Third-party scripts, such as analytics, tag managers, A/B testing, chat widgets, and consent banners, are a common source of performance degradation. Each script adds to Total Blocking Time (TBT) and can delay Interaction to Next Paint (INP).
Keep these tips in mind for best performance results.
- Audit regularly. Every external script must justify its performance cost. Remove scripts that aren't actively used.
- Never load synchronously. Always use
asyncordefer. A synchronous<script>blocks HTML parsing entirely. - Lazy-load interaction-driven widgets. Chat widgets, social buttons, and similar components should load only when the user scrolls near them or clicks a placeholder — not on page load. See Lazy Loading for Overlays for the deferred mounting pattern.
- Use a Consent Management Platform (CMP). Integrate with a tag manager to prevent marketing and analytics tags from loading before user consent. This method satisfies privacy regulations and improves performance for users who haven't consented.
- Measure impact. Use the Chrome DevTools Coverage tab to identify unused JavaScript and CSS from third-party scripts.