Performance and validation

Updated: Sep 24, 2026
Keep frame work small and let components perform the measurement and animation they already own. Meta Ray-Ban Display applications run on low-power hardware, often on the device, where one forced layout per frame can drop animation below its target rate.
Run the validation checklist against the built application. A passing build establishes correctness checks, but does not verify rendered behavior.

Keep layout work off the frame

Reading layout and writing style in the same callback forces the browser to recompute layout synchronously, and doing it once per frame is the most common cause of a stuttering toolkit screen.
  • Layout reads such as getBoundingClientRect(), offsetWidth, scrollTop, and getComputedStyle() must stay out of scroll handlers, animation frames, and render because they can force synchronous layout.
  • Batch any measurement you genuinely need into one pass, then write in a separate pass.
  • Prefer ResizeObserver and IntersectionObserver over polling. Observers deliver measurements the browser has already computed.
  • Share one observer across many elements instead of creating one per row.

Let components own their own measurement

Toolkit components that need their own size measure themselves: they observe their border box, re-measure after web fonts finish loading, and regenerate the geometry their materials draw. That measurement drives the material path, the corner geometry, and the fading edges.
Do not duplicate that work from application code. If you find yourself measuring a component in order to size it, you are usually fighting intrinsic sizing — components size from the space you give them, so give them a container and let them fill it rather than computing a length. Never hardcode the viewport dimensions of a device.

Keep React off the animation frame

useSpringAnimation gives you an imperative controller precisely so a spring does not re-render React on every frame. Its onChange is a hot-path callback: write to the DOM or to a CSS custom property there. Calling a state setter puts a full React render back on the frame and defeats the hook.
import {useEffect, useRef} from 'react';
import {SpringConfigs, useSpringAnimation} from '@wearables-ui-toolkit/mrbd';
import styles from './ProgressFill.module.css';

export function ProgressFill({progress}: {progress: number}) {
  const fillRef = useRef<HTMLDivElement>(null);
  const spring = useSpringAnimation({
    config: SpringConfigs.PROGRESS_VALUE,
    initialValue: progress,
    onChange: value => {
      fillRef.current?.style.setProperty('--fill', String(value));
    },
  });

  useEffect(() => {
    spring.setTarget(progress);
  }, [progress, spring]);

  return <div ref={fillRef} className={styles.fill} />;
}
The controller is stable for the component’s lifetime, throttles to its frameRate (thirty updates per second by default), stops scheduling frames while the document is hidden, and snaps straight to its target when the user prefers reduced motion.

Animate transform and opacity

transform and opacity are the two properties a browser can animate without recomputing layout. Everything else — width, height, top, left, margin, padding — relayouts the subtree on every frame, and on a toolkit surface it also invalidates the measured size the material geometry was generated from.
Express timing with the exported motion constants rather than raw numbers:
import {
  AnimationDurations,
  Interpolators,
  createTransition,
} from '@wearables-ui-toolkit/mrbd';

const style = {
  transition: createTransition(
    ['transform', 'opacity'],
    AnimationDurations.CONTAINER_STATE_CHANGE,
    Interpolators.CONTAINER_SCALE,
  ),
};
If a size really has to change, animate a scale transform over a fixed layout box, or let the component’s own state transition handle it.

Reduced motion

Honor prefers-reduced-motion: reduce on both sides of the stack: pass JavaScript durations through reducedMotionDuration, and guard CSS transitions with the media query. useSpringAnimation already snaps instead of tweening. See Accessibility for the full pattern.

One same-axis scroller per route

Use ScrollView, or VerticalList for a list, and give a route exactly one scroller per axis.
Nested same-axis scrollers are not just a performance problem. Directional navigation hands unclaimed movement to the nearest scroll container, so two vertical scrollers inside one another compete for the same arrow key: focus sticks, the wrong region scrolls, or the outer region never reaches its end.
Crossing axes is fine — a horizontal rail inside a vertical list is a supported composition, because each axis has exactly one owner.
Long collections belong in the toolkit scroll infrastructure rather than a custom virtualized list. The scroller already coordinates scroll-to-focused behavior, fading edges, and directional handoff; a custom one has to reimplement all three.

Keep props stable

Every new object identity invalidates a memoized subtree. On a screen full of interactables, that is the difference between one component re-rendering and all of them.
  • Hoist icon data, style objects, and static arrays to module scope.
  • Wrap callbacks passed into list rows in useCallback, and derived objects in useMemo.
  • Create each ContainerMaterial and shape provider once with useMemo — one instance per mounted host. A material tracks the visual state it is animating between, and a shape provider caches the geometry it generated, so replacing either on every render throws both away.
  • Give list rows stable keys derived from the data, not the index, so a data update re-renders a row instead of remounting it. Remounting a focused row drops focus and restarts its state transition.
  • Do not build a new inline object for a prop that a component compares by identity.

Clean up everything you start

Anything created in an effect must be released by that effect’s cleanup, or it keeps running after the route is gone.
import {useEffect} from 'react';

useEffect(() => {
  const timer = window.setInterval(tick, 1000);
  const controller = new AbortController();
  window.addEventListener('resize', onResize, {signal: controller.signal});

  return () => {
    window.clearInterval(timer);
    controller.abort();
  };
}, [onResize, tick]);
  • Clear every timer and interval, cancel every animation frame, disconnect every observer, and remove every listener.
  • Render portal content with createPortal so React unmounts it with the owning component. Never append nodes into the portal root imperatively — see Floating Portals.
  • Call clearPathCache() only when tearing down a long-lived surface that generated many one-off geometries, not on a schedule. See Smooth Corners.

Load routes lazily, warm them early

Split each route into its own chunk and warm the chunk before the user commits to the navigation.
import {createPreloadableLazyComponent} from '@wearables-ui-toolkit/mrbd';

const SettingsRoute = createPreloadableLazyComponent(
  () => import('./SettingsRoute'),
  module => module.SettingsRoute,
);
SettingsRoute.Component renders the lazy route; SettingsRoute.preload() warms it. Wire the warming automatically by wrapping the app in RoutePreloadProvider and giving it a preloadRoute handler that maps a preloadKey to the matching preload() — without that handler preloading stays off — then marking entry points with RoutePreloadTarget (or useRoutePreloadTarget when you own the element) and its preloadKey. A target warms on focus, on pointer enter, and on visibility, and each of those triggers can be turned off individually. Warm only the routes a user is plausibly about to open; preloading everything just moves the cost earlier.

Validation checklist

Run this against the built app on the target runtime, not only in unit tests.
  1. Type-check, format, lint, run your tests, and produce a production build. None of those prove behavior; they only clear the floor for the steps below.
  2. Exercise every reachable interaction state: default, focused, pressed, disabled, selected, loading, and empty.
  3. Navigate exclusively with directional input and the activation key.
  4. Inspect initial focus, focus restoration, route history, and system Back.
  5. Scroll to the exact beginning and end of every scroll region, and repeat the same direction rapidly at each end.
  6. Capture each surface and each meaningful scroll state at representative viewport sizes, including a materially different one.
  7. Wait for animations and web fonts to settle before comparing anything visually; a measurement taken mid-transition is not a bug.
  8. Check for clipping, overflow, incorrect insets, doubled materials, unstable layout, and text or icon misalignment.
  9. Validate custom themes and materials over the toolkit window background, where the additive display makes low-contrast pairings fail first.
  10. Re-run the walkthrough with reduced motion enabled.
  11. Confirm every toolkit import resolves to @wearables-ui-toolkit/mrbd, a documented per-component subpath, or the react-router subpath — nothing deeper.

Profiling

When something is slow, measure before changing anything:
  • Record a performance profile while reproducing the interaction and look for long tasks, forced reflow warnings, and layout thrash in the same frame.
  • Use the React profiler to find components re-rendering on every frame. A spring or scroll handler that re-renders React is the usual cause.
  • Check paint and layer counts before adding another animated layer; a full-size animated overlay is expensive even when it is transparent.
  • Animate transform and opacity with AnimationDurations and Interpolators so the browser can composite consistent motion.
  • Let components measure and size themselves from available space.
  • Keep one scroller per axis per route so directional navigation has one owner.
  • Release timers, listeners, observers, frames, and portals on unmount.

Avoid

  • Keep layout reads out of render, scroll handlers, and animation frames to prevent synchronous layout.
  • Use imperative frame callbacks instead of React state for frame-rate updates.
  • Keep focused regions, animated layers, and material hosts mounted while updating props; remounting drops focus and animation state.
  • Use one same-axis scroller per route rather than nesting competing scrollers.
  • Derive sizes from the available viewport, content, and semantic tokens. This keeps layouts responsive across supported viewports; avoid fixed device-resolution and private component-metric assumptions.
  • Complete the validation checklist instead of treating a successful build and tests as rendered-behavior evidence.