Performance¶
Rezi’s performance model is built around bounded work and deterministic scheduling. You get the best results when you keep views pure, keep identities stable, and use virtualization for large datasets.
Reconciliation keys¶
When rendering lists, provide a stable key so Rezi can reconcile efficiently:
import { ui } from "@rezi-ui/core";
ui.column({ gap: 1 }, items.map((it) => ui.text(it.name, { key: it.id })));
Guidelines:
keymust be unique among siblings.- Prefer real IDs over array indices.
- Changing keys forces unmount/mount and can invalidate local state for complex widgets.
Avoiding re-renders¶
Views are re-run when committed state changes. Keep view(state):
- pure (no side effects)
- cheap (avoid large allocations each frame)
- stable (avoid rebuilding huge derived structures inline)
Patterns:
- Precompute derived data in state updates instead of inside
view. - Keep large static arrays/constants outside the view closure.
- Prefer event handlers that call
app.update(...)without doing heavy work in render. - For animation, prefer declarative hooks (
useTransition,useSpring,useSequence,useStagger) andui.boxtransition props over ad-hoc timer loops in app/view code. - Rezi bounds animation cadence to keep render and input responsive.
Virtual lists¶
For large datasets, use ui.virtualList to window the rendered items:
import { ui } from "@rezi-ui/core";
ui.virtualList({
id: "results",
items: state.rows,
itemHeight: 1,
overscan: 3,
renderItem: (row, index, focused) =>
ui.text(`${focused ? "> " : " "}${row.name}`, { key: row.id }),
});
For variable-height rows, use estimate mode and let Rezi correct from measured visible items:
ui.virtualList({
id: "chat",
items: state.messages,
estimateItemHeight: (msg) => (msg.hasPreview ? 3 : 2),
renderItem: (msg) => ui.column({}, [ui.text(msg.author), ui.text(msg.body)]),
});
Guidelines for estimate mode:
- Keep
estimateItemHeightclose to the real average to reduce correction churn. - Prefer stable item identity/order; large reorders increase correction work.
- If you know exact heights ahead of time, keep using
itemHeight.
This keeps render + layout work proportional to the visible window rather than the full dataset size.
Caps and limits¶
The runtime enforces hard limits (drawlist sizes, event batch validation, etc.). These failures are deterministic and surfaced as fatal errors in development to prevent “slow corruption” or non-deterministic behavior.
Commit-time guardrails also keep large widget trees predictable:
- Interactive widget
iduniqueness is checked with a hash index during each commit cycle (O(1)lookups) to keep duplicate detection fast even for large dashboards. - Layout nesting depth emits a dev warning after depth
200, and fails fast with aZRUI_MAX_DEPTHdiagnostic (inZRUI_INVALID_PROPSdetail) after depth500to avoid opaque JavaScript stack overflows.
If you hit limits:
- reduce per-frame draw commands (simplify UI or virtualize)
- avoid rendering off-screen content
- ensure your view isn’t duplicating large subtrees unnecessarily
Profiling¶
Use the debug trace system to inspect frame timing and hotspots. For normal
apps, use createNodeApp() and read the backend debug interface from app.backend.
import { createDebugController, categoriesToMask } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";
const app = createNodeApp({ initialState: {} });
const debug = createDebugController({ backend: app.backend.debug, maxFrames: 300 });
await debug.enable({ categoryMask: categoriesToMask(["perf", "frame"]) });
See: Debugging.
Common pitfalls¶
- Missing
keyin dynamic lists → high churn, lost local state - Using array index as
key→ reordering causes remounts - Allocating large arrays/objects inside
viewevery frame - Calling
update()during render → throws deterministically (ZRUI_UPDATE_DURING_RENDER) - Rendering large tables without
ui.table/ui.virtualList→ unbounded work
Related¶
- Lifecycle & Updates - Commit points and frame coalescing
- Layout - How size constraints and clipping affect work
Next: Debugging.