# 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.