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.
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 and particles by default, or exactly what your FX Spec lists in
performance.lowPower.disable.
{
"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) 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. Liquid is the exception: it computes a lot while drawing little, so the badge can rank it too low.
Low power:
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 and particles 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:
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 });
},
});
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.