Zvessa docs

Performance, battery and size

Zvessa has to feel instant on a mid-range phone, stay cool, and stay small. These rules apply to every screen, block, kit and engine. A change that breaks one says why in its description.

Budgets #

Drawing #

  1. Few views. Draw decoration (grids, dots, gradients, glows) with style properties (backgroundImage, boxShadow), never with one view per line or dot.
  2. Lists that can grow are virtualized. Chat, records, history, members, feeds: use a list component (FlashList once installed, FlatList until then), never .map() inside a ScrollView. Give rows a stable key and wrap the row component in memo.
  3. Lists inside a page load in pages. A block inside a scrolling page can't virtualize, so it asks the server for a page (about 50) and shows "Show more" (blueprint.words.showMore).
  4. Don't redraw what didn't change. Derived values (resolveStyle, lookups, filtered lists) go in useMemo; look things up through a Map, not .find() inside a loop.
  5. Images come in at the size they're shown (ask the server for a thumbnail), with a placeholder (thumbhash) and expo-image caching once installed.

Motion #

  1. Animations run on the UI thread. useNativeDriver: true today, Reanimated worklets once installed. Animate transform and opacity only, never width, height, top or colours.
  2. Decoration stops when it isn't seen. Anything that moves on its own uses useDecorativeMotion() from src/lib/activity.ts: it is false when the screen is covered, the app is in the background, or the person turned on Reduce Motion.
  3. A style's motion.speed: "off" turns off all decorative motion. Respect it everywhere.

Work that keeps running #

  1. Gate it on useLive(). Timers, sensors, location, camera, audio, live streams and game loops start only while useLive() (screen showing and app in the foreground) is true, and stop in the effect's cleanup.
  2. Sensors (expo-sensors): subscribe in an effect gated on useLive(), set the slowest setUpdateInterval that works (100 ms for tilt and shake, 16 ms only for a game that needs it), and always remove the listener. Location: lowest accuracy that works, never in the background unless a kit is about background location and the person turned it on.
  3. Mini-apps pause themselves. MiniApp calls window.myaibio.setPaused() when hidden: animation frames, timers and media stop, and the mini-app gets myaibio:pause and myaibio:resume events to save state. Timers can't fire faster than every 16 ms.
  4. No polling. Live data comes from Convex subscriptions. Something outside Convex is fetched by a Convex action or a scheduled job, never by a timer on the phone.

Data #

  1. Subscribe to what the screen shows. Every useQuery returns a bounded result (.take(n) or pagination, never .collect() on a table that grows). Counts and totals come from @convex-dev/aggregate, not from reading every row (records.count moves there with the Events engine).
  2. Ask once. Two blocks that need the same data call the same query with the same arguments; Convex shares one subscription.
  3. Small results. Queries return only the fields a screen draws; big text and files are fetched when opened.
  4. Writes are cheap. Mutations take what changed (a patch), not the whole document.

Loading and size #

  1. Native first, package second. Use what React Native and Expo already ship (style gradients, boxShadow, expo-symbols) before adding a package. Every new package goes through the package plan and npx expo install.
  2. Heavy and rare code loads on demand on the web. Kits a workspace hasn't added (and their editors) are imported with React.lazy so first load stays small. On phones the bundle is Hermes bytecode read from disk, so this matters less there.
  3. Release builds shrink. Android uses R8 and resource shrinking (expo-build-properties), iOS and Android ship Hermes bytecode, and the web build is minified with tree shaking.
  4. Measure before and after. Use the React Native DevTools profiler and Expo Atlas (EXPO_ATLAS=1 npx expo export) to check render counts and bundle size.

Icons #

Use Icon from src/icons with a name from src/icons/icons.json ("send", "delete", "ai"…):

<Icon name="send" color="accent" size={20} label="Send" />