Performance

A dish card lives in a scrolling feed, on a phone, on restaurant wifi. Everything below exists because of that.

Prefetch before the button appears

AR Quick Look cannot read a remote URL — it needs a local file. So the download happens either way; only its timing is yours to choose. Choosing early is the difference between an AR button that opens instantly and one that stalls for two seconds while the user wonders whether the tap registered.

ArmenusModel warms the cache as soon as it knows AR is offerable. Warm it yourself for rows about to scroll into view:

Grid.tsx
// The web needs no explicit prefetch — the browser streams the GLB,
// and <model-viewer> itself is imported lazily on mount.
// Do batch the metadata, though:
const { data } = useArmenusItems(merchantId, { withModel: true, limit: 20 });

What the native SDKs already do

  • Parsing runs off the main thread on both platforms. A textured dish is a few MB, and decoding it inline drops frames in whatever list the card is sitting in — the most visible performance mistake available on this path.
  • Renderers pause when off screen. Each SCNView owns a CADisplayLink; a feed with twenty cards all rendering off screen drains the battery and drops the scroll below 60fps for nothing visible.
  • Rotation is driven by Choreographer on Android, tied to actual frame delivery rather than a timer that keeps firing while the device is throttling.
  • An LRU disk cache, 256 MB, in the OS cache directory — so it is evictable under storage pressure and never counts against the user’s backup quota. Inspect it with cacheSize(); empty it with clearCache().
  • Concurrent requests for one dish coalesce into a single download, so a grid of cards showing the same item fetches once.
  • Native GPU resources are released when a card unmounts. Filament holds memory the garbage collector will not reclaim.

Rate limits

Quotas are counted per key, not per IP. A delivery app’s traffic arrives from every consumer IP in the country, so a per-IP limit would never bind on the caller worth bounding — while a carrier NAT would put thousands of unrelated diners in one bucket.

Responses carry x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset. The default is 600 requests per minute per key; ask if you need more.

Caching

Dish responses carry public, max-age=30, s-maxage=300, stale-while-revalidate=86400. The long stale window is deliberate: a slightly stale price is a far better failure than a spinner where a dish should be. Model files themselves are immutable and served from a CDN.