Sinua
Performance & accessibility

Performance

What a visual costs, how to cap it, how low-power mode trims it, and how to measure it on your devices.

A visual is a small Rust engine computing one frame, plus the platform drawing it. Views already do the cheap things for you: they pause off screen and in the background, and draw nothing extra while paused.

Cap the frame rate

maxFps limits how often a view draws, on an exact grid (30 on a 60 Hz or 120 Hz display is exactly 30). The animation still runs on real time, so a cap lowers smoothness, never speed. Ambient visuals look fine at 30. An FX Spec can carry its own cap in performance.maxFps, and the lower cap wins.

Many small views

A chat list with an orb per row, or a grid of thumbnails, is many views at once. Views handle it:

  • on the web, every view on a page shares one animation-frame loop;
  • a view whose short side is under 48 px (pt, dp) draws at 30 fps by default; your maxFps, or the spec's performance.maxFps, wins;
  • on Android, a view scrolled out of a clipping parent (a LazyColumn row, say) stops drawing.

Measured on the web, 50 orbs at 32 px went from 869 to 459 ms of work per second, with no dropped frames. For more than about 100 views, pass paused and show one still frame each; animate only the one that matters.

Low-power mode

When the device is in Low Power Mode (iOS) or Battery Saver (Android), views switch to low power automatically. On the web there is no reliable signal, so pass lowPower: true yourself when your app knows. In low power:

  • the frame rate drops to 30 fps;
  • the costly finishes are shed: glow by default, or exactly what your FX Spec lists in performance.lowPower.disable.
low-power.fxspec.json
{
  "fxSpec": "1.8",
  "object": "orb",
  "pattern": "working",
  "materials": {
    "glow": {
      "strength": 0.5,
      "mode": "blur"
    }
  },
  "performance": {
    "maxFps": 60,
    "lowPower": {
      "maxFps": 30,
      "disable": [
        "glow"
      ]
    }
  }
}

On iOS and Android, lowPower follows the system by default; force it with .on / .off (SwiftUI) or ON / OFF (Compose). On the web it's a boolean you set, for example from watchLowBattery() where the Battery API exists (Chromium).

Know the cost

Every pattern and material combination has an estimated cost class (light, medium, heavy) from the number of elements it draws, how much of the canvas they cover, and how much blur they need. estimateCost(pattern, size, overrides) from @sinua/core/dev returns it (the third argument takes engine keys, as the Studio exports them), and the Studio shows it as a badge. Glow is by far the biggest multiplier.

Low power:

low-power-web.ts
import { mount, watchLowBattery } from "@sinua/web";

const canvas = document.querySelector<HTMLCanvasElement>("#orb")!;

// Low power: the spec's `performance.lowPower` block if it has one, else 30 fps with
// glow off. The Web has no reliable OS signal, so the app decides:
export const fx = mount(canvas, { pattern: "speaking", lowPower: false });

// ... e.g. from the Battery API where it exists (Chromium): low below 20% and unplugged.
void watchLowBattery((low) => fx.update({ lowPower: low }));

A frame-rate cap:

max-fps-web.ts
import { mount } from "@sinua/web";

const canvas = document.querySelector<HTMLCanvasElement>("#orb")!;

// Cap the frame rate (the clock keeps wall time, so the motion's speed is unchanged).
export const fx = mount(canvas, {
  pattern: "breathing",
  maxFps: 24,
  onFrame: ({ dtMs, computeMs, paintMs }) => {
    if (computeMs + paintMs > 8) console.debug("slow frame", { dtMs, computeMs, paintMs });
  },
});

What Sinua adds to your app

Measured on the release build of 0.1.0-beta.8:

PlatformWhat shipsSize
Web@sinua/core (engine inlined)511,877 bytes, 384,034 gzipped
Android (arm64)the engine library+1.02 MB to download, +2.42 MB installed
iOSthe engine libraryabout +1.07 MB to download, +2.52 MB installed
Voice (iOS, Android)WebRTC, only with a voice package that uses itabout +5.8 MB more

The dev tools live in @sinua/core/dev on the web and in development builds on iOS and Android, so they aren't in these numbers. The npm package is larger (about 870 KB) because it carries both engines; that's install size, not what your app ships.

Measure on your devices

The cost class is a guide, not a measurement. The bench apps run a fixed set of visuals on a real device and report frame times, dropped frames, the hitch ratio (Apple's ms-per-second measure) and CPU, at normal and low power. Run them on your slowest supported phone. The repository's docs/bench.md has the steps for iPhone, Android and the web.

On this page