Performance
The habits that keep a theme fast, from narrow imports and lazy loading to image rules and skeleton timing.
Read this before a theme goes live, or when a page feels slow. Most speed problems in a theme come from loading too much code up front or from images that shift the page. The fixes are small rules you can apply as you write.
Import narrowly
The engine has many subpaths on purpose. A narrow import loads only what you use; a broad one pulls in everything behind it.
// Good: one subpath per area
import { useStore } from '@salla.sa/twilight-theme-engine/hooks/useStore';
import { Modal } from '@salla.sa/twilight-theme-engine/modal';
import { CartSummary } from '@salla.sa/twilight-theme-engine/cart';
// Avoid: the broad barrels
import { Cart } from '@salla.sa/twilight-theme-engine/routes';
import { Modal } from '@salla.sa/twilight-theme-engine/components';The lint config flags the two broad ones. See Lint your theme.
Load late what is not needed first
| Situation | Do this |
|---|---|
| A modal, drawer or panel opened by a click | Load it with React.lazy() inside <Suspense>, and render it only when open. |
| An expensive block far down the page | Wrap it in RenderWhenVisible from @salla.sa/twilight-theme-engine/common. Its children are not mounted, and do no work, until the customer scrolls near. |
| Data a page can show later | Return a promise from the loader instead of awaiting it, so the page renders first. See Loading data. |
RenderWhenVisible takes placeholder and estimatedHeight so the space is reserved, rootMargin to start before it is on screen, and renderOnMount to skip waiting. Once shown, the content stays mounted.
Images
Use Image for store images. It lazy-loads and asks the CDN for a suitable size. Two exceptions and one habit:
- The main image above the fold (the first banner, the product photo) should load at once. Give
Imagethepriorityprop so it does not wait. - Give every image a size or an
aspectRatioso the page does not jump when it arrives. - Everything below the fold stays lazy.
Paint the first screen first
Framework: TanStack Start
This section is written for TanStack Start, the default and currently supported framework. See what depends on the framework.
On a phone, the stylesheet and scripts the <head> asks for share the network with the page's main image, and delay its paint (the page's largest paint, or LCP). The starter theme uses three engine pieces so its home and product pages paint sooner:
| Piece | What it does |
|---|---|
ThemeStylesheet (/tanstack) | On the routes you list, puts that page's critical CSS inline in the HTML instead of a blocking link to the full stylesheet. Every other page links the full sheet as usual. |
bootClient (/boot) | Your client entry. On a page that paints from critical CSS, it waits until the first screen's priority images (fetchpriority="high") have loaded and painted, then loads your app and the full stylesheet and hydrates. Other pages hydrate at once. |
ThemeHeadContent (/tanstack) | The route's head tags without TanStack's modulepreload links, which compete with the main image. The entry script still loads every chunk it needs. |
In the root route, ThemeStylesheet takes the full sheet's URL and a map of route id to critical CSS. The route id is the path string you pass to createFileRoute in that route's file:
import { ThemeHeadContent, ThemeStylesheet } from '@salla.sa/twilight-theme-engine/tanstack';
import appCssUrl from '../styles/app.css?url';
import homeCriticalCss from '../styles/critical.scss?inline';
// The CSS ships with the server render only; the client keeps the key so hydration matches.
const CRITICAL_CSS = { '/{-$locale}/': import.meta.env.SSR ? homeCriticalCss : '' };
// inside <head>, in place of TanStack's <HeadContent />:
<ThemeStylesheet href={appCssUrl} critical={CRITICAL_CSS} />
<ThemeHeadContent />app/client.tsx is the first script the browser runs, and bootClient is all it calls. load imports your hydration module, which exports a hydrate() that calls hydrateRoot (and anything that must run first, such as removeEdgeInjections from /dom):
import { bootClient } from '@salla.sa/twilight-theme-engine/boot';
import appCssUrl from './styles/app.css?url';
bootClient({ stylesheet: appCssUrl, load: () => import('./hydrate') });The starter's critical CSS is app/styles/critical.scss (home) and critical-product.scss (product). Each imports the part of the theme's styles its first screen needs, in app.css's order, with a Tailwind config that scans only the first-screen components.
- Keep the route ids on the client, with empty strings as above. Dropping them makes the browser render the full-sheet link where the server inlined the CSS: a hydration mismatch.
- The critical CSS must also be in the full sheet, in the same order. The full sheet takes over when it arrives.
- The route is read once, from the server render. A client-side navigation never adds or removes the inline CSS; by then the full sheet is loaded. Visitors without JavaScript get the full sheet from a
<noscript>link. - Give the main image
priority.bootClientwaits for the images withfetchpriority="high", which is whatImagesets forpriority. It waits at most 2.5 s for them and 3 s for the full stylesheet, so a slow image never blocks hydration. - A failed load reloads once. If the app's JavaScript fails to load (usually a chunk from a replaced deploy),
bootClientreloads the page once per session; a second failure is logged, not looped.
Navigation feel
The engine preloads a page when the customer hovers or focuses a link (Link does this by default) and shows a skeleton only if a load takes more than 100 ms. Keep it that way: use Link, not <a>, for internal links, so a click is a quick in-browser move and not a full reload.
In your own code
- Wrap pure display components in
memo(). - Pass stable values to hooks. Do not create new objects or arrays inside render and use them as dependencies.
- Prefer CSS animations to a JavaScript animation library.
- Do not call
window.location.reload()to refresh. Update state, or change a component'skeyto remount it.
Gotchas
- Lazy loading hides work, it does not remove it. Do not lazy-load something visible on first paint: it shows up late.
- A skeleton or placeholder with the wrong height causes layout shift, which hurts the page score more than a slow load does.
Next
- Lint your theme turns these rules into warnings while you code.
Image, Skeletons, Loading data