# Valdi Performance Patterns
Valdi re-renders a child component whenever its viewModel reference changes. Most
unnecessary re-renders come from creating new object/array/function references in
`onRender()`. Fix the reference — fix the re-render.
## ViewModel Identity Stability
**The #1 performance problem in Valdi apps.** Every object or array literal created
inside `onRender()` is a new reference. Child components will always re-render, even
when the actual values haven't changed.
```typescript
// ❌ New object every render — child always re-renders
onRender(): void {
;
}
// ❌ New array every render
onRender(): void {
;
}
// ✅ Stable class property for constants
private tabs = ['Home', 'Profile', 'Settings'];
onRender(): void {
;
}
// ✅ Pre-compute derived viewModels in onViewModelUpdate
private userRowVM: UserRowViewModel = { name: '', age: 0 };
onViewModelUpdate(): void {
this.userRowVM = { name: this.viewModel.user.name, age: this.viewModel.user.age };
}
onRender(): void {
;
}
```
Only update the pre-computed VM when the relevant input actually changes:
```typescript
onViewModelUpdate(previous?: UserProfileViewModel): void {
if (this.viewModel.userId !== previous?.userId) {
this.userRowVM = buildUserRowVM(this.viewModel.user);
}
}
```
## Navigation Callbacks
Navigation callbacks passed into child viewModels have the same identity problem:
`() => this.navigationController.push(...)` creates a new function each render.
Use a class arrow function — it is defined once and has a stable reference:
```typescript
// ❌ New function every render
onRender(): void {
this.navigationController.push(DetailPage, { id: this.viewModel.userId })} />;
}
// ✅ Class arrow function — stable reference, viewModel.userId read at tap time
private goToDetail = (): void => {
this.navigationController.push(DetailPage, { id: this.viewModel.userId });
};
onRender(): void {
;
}
```
## `` vs ``
`` allocates a native platform view. `` is virtual — it participates in
flexbox layout but creates no native view, which is faster and uses less memory.
```typescript
// ❌ Native view wasted on an invisible spacer
// ✅ No native view allocated
// ❌ Wrapper with no visual properties or tap handler
;
;
;
// ✅ Virtual layout node
;
;
;
```
**Use `` when you need:** `onTap`, `backgroundColor`, `borderRadius`, `style`,
`overflow`, `opacity`, or any visual/interactive property.
**Use `` for everything else:** spacers, invisible wrappers, structural containers.
## Keys in Lists
Keys determine element identity across re-renders. Without a key (or with an index
key), reordering or inserting items causes the wrong component instances to receive
the wrong viewModels.
```typescript
// ❌ No key — identity lost on reorder
{this.viewModel.items.forEach(item => {
;
})}
// ❌ Index key — breaks on insert/remove
{this.viewModel.items.forEach((item, index) => {
;
})}
// ✅ Stable data ID
{this.viewModel.items.forEach(item => {
;
})}
```
## Render Props as Class Arrow Functions
When a parent needs to pass a render function to a child (e.g. a list row renderer),
define it as a class arrow function property so it has a stable reference:
```typescript
// ❌ New function every render — child's renderItem prop always changes
onRender(): void {
{ ; }} />;
}
// ✅ Stable class arrow function
private renderItem = (item: Item): void => {
;
};
onRender(): void {
;
}
```
For loop closures that must capture a loop variable, use `createReusableCallback`
inline in `onRender()`. Valdi's diffing engine recognises `Callback` objects and
updates the internal function reference without treating it as a prop change, so the
child does not re-render:
```typescript
import { createReusableCallback } from 'valdi_core/src/utils/Callback';
// ❌ New plain function every render — child always re-renders
onRender(): void {
{this.viewModel.sections.forEach((section, i) => {
this.handleTap(i)} />;
})}
}
// ✅ Inline Callback — identity-merged by Valdi's diffing engine
onRender(): void {
{this.viewModel.sections.forEach((section, i) => {
this.handleTap(i))} />;
})}
}
```
## Style Objects at Module Level
`new Style({...})` interns style objects — the same property values always produce
the same cached object. This interning only works at module initialization time.
Inside `onRender()` the cache is bypassed and a new allocation happens every render.
```typescript
// ❌ Defeats interning — new allocation every render
onRender(): void {
const s = new Style({ backgroundColor: '#fff', borderRadius: 8 });
;
}
// ✅ Interned at module level
import { View } from 'valdi_tsx/src/NativeTemplateElements';
const styles = {
card: new Style({ backgroundColor: '#fff', borderRadius: 8 }),
};
class MyCard extends Component {
onRender(): void {
;
}
}
```
Group styles in a `const styles = {}` object after the class definition.