Performance Patterns & Guidance

April 23, 2026 · View on GitHub

Practical patterns for avoiding common performance pitfalls in SignalTree + Angular apps.

Batching: When and Why

Use batching() when you make multiple writes in one synchronous turn and want to consolidate change notifications.

// Without batching: each set() triggers a notification
tree.$.firstName.set('Alice');
tree.$.lastName.set('Smith');
tree.$.age.set(30);
// → 3 notifications

// With batching: notifications are coalesced
const tree = signalTree(state).with(batching());
tree.$.firstName.set('Alice');
tree.$.lastName.set('Smith');
tree.$.age.set(30);
// → 1 notification (coalesced)

Don't use batching if you only write one field at a time. The enhancer adds overhead per write.

Memoization: Use Angular's computed()

Angular's computed() already memoizes by reference equality. In 9.0.1 the memoization() enhancer was removed — computed() covers every common case with zero additional runtime cost and smaller bundles.

import { computed } from '@angular/core';

const totalItems = computed(() => tree.$.items().length);
const activeUsers = computed(() => tree.$.users().filter((u) => u.active));

If you need value-equality semantics (e.g. API responses that rebuild the same object), compare inside the consumer or gate updates at the writer (set/update) rather than re-adding a cache layer.

Selector Sharing

Avoid creating identical computed() expressions across multiple components. Share selectors:

// selectors/user.selectors.ts
export const selectActiveUsers = (tree: AppTree) => tree.$.users.where(isActive);

Why: Reuse avoids creating many computed() instances for the same derived query. EntitySignal.where(predicate) caches by function reference — named predicates benefit from this caching; inline arrow functions do not.

EntitySignal Predicate Caching

const isActive = (u: User) => u.active;
const s1 = tree.$.users.where(isActive); // cached
const s2 = tree.$.users.where(isActive); // same Signal instance

// ❌ Not cached — new function reference each time
const s3 = tree.$.users.where((u) => u.active);

Anti-Patterns to Avoid

Synchronous tight loops

// ❌ Don't benchmark like this — it's not how Angular apps behave
for (let i = 0; i < 100000; i++) {
  tree.$.counter.set(i);
}

Angular's microtask-based change detection already batches many updates in real apps.

Thousands of subscribers to one node

Split hot state into separate nodes. Use shared selectors instead of per-component inline computed expressions. Virtualize large lists.

Lazy Trees

SignalTree automatically uses lazy proxy-based signal creation for state shapes with more than 50 estimated nodes. This means signals are created on-demand when accessed, not upfront.

You can override this:

signalTree(largeState, { useLazySignals: true }); // Force lazy
signalTree(smallState, { useLazySignals: false }); // Force eager

Diagnostics

The demo app supports ?quickRun URL parameter to reduce iterations for CI smoke tests. Do not use for production-quality measurements.