Performance
Whisq’s signal-based reactivity is efficient by default — only the DOM nodes that read a signal update when it changes. But in complex applications, a few patterns can make a significant difference.
Fine-Grained Signals
Section titled “Fine-Grained Signals”Prefer many small signals over one big object:
// ❌ Less efficient — entire UI re-checks when any field changesconst user = signal({ name: "Alice", age: 30, email: "alice@example.com" });
// ✅ More efficient — only name UI updates when name changesconst name = signal("Alice");const age = signal(30);const email = signal("alice@example.com");With fine-grained signals, changing name.value only updates DOM nodes that read name.value. With a single object signal, every node that reads any property re-evaluates.
Batch Updates
Section titled “Batch Updates”When updating multiple signals at once, wrap them in batch():
import { signal, batch } from "@whisq/core";
const x = signal(0);const y = signal(0);const z = signal(0);
// ❌ Three separate UI updatesx.value = 1;y.value = 2;z.value = 3;
// ✅ One UI updatebatch(() => { x.value = 1; y.value = 2; z.value = 3;});batch() defers all reactive updates until the function completes, then flushes them once.
Use peek() in Effects
Section titled “Use peek() in Effects”When an effect needs to read a signal without subscribing to it, use peek():
import { signal, effect } from "@whisq/core";
const count = signal(0);const log = signal<string[]>([]);
// ❌ Infinite loop — effect reads log.value, which triggers re-runeffect(() => { log.value = [...log.value, `Count changed to ${count.value}`];});
// ✅ peek() reads without creating a dependencyeffect(() => { const current = log.peek(); log.value = [...current, `Count changed to ${count.value}`];});peek() reads the signal’s current value without tracking it as a dependency. The effect above re-runs when count changes but not when log changes.
Avoid Unnecessary Computed Values
Section titled “Avoid Unnecessary Computed Values”computed() caches its result and only re-evaluates when dependencies change. But avoid creating computed values for trivial derivations:
// ❌ Overkill — computed overhead for a simple checkconst isEmpty = computed(() => items.value.length === 0);
// ✅ Just inline itwhen(() => items.value.length === 0, () => p("No items"));Use computed() when:
- The derivation is expensive (filtering, sorting, mapping large arrays)
- Multiple parts of the UI read the same derived value
- You want to name the value for readability
Lazy Component Loading
Section titled “Lazy Component Loading”Split large apps by loading components on demand:
import { signal, component, div, when } from "@whisq/core";
const Dashboard = component(() => { const loaded = signal<(() => Node) | null>(null);
// Load the heavy component on demand const loadChart = async () => { const { ChartWidget } = await import("./ChartWidget"); loaded.value = () => ChartWidget({}); };
return div( button({ onclick: loadChart }, "Load Chart"), when(() => !!loaded.value, () => loaded.value!()), );});Profiling with DevTools
Section titled “Profiling with DevTools”Use @whisq/devtools to inspect signal updates and identify performance bottlenecks:
import { installDevtools } from "@whisq/devtools";
// Enable in developmentif (import.meta.env.DEV) { installDevtools();}The devtools show:
- Signal update frequency — which signals change most often
- Effect execution count — which effects re-run frequently
- Component tree — the current component hierarchy
- Dependency graph — which signals each effect depends on
Performance Checklist
Section titled “Performance Checklist”| Pattern | Impact | When to use |
|---|---|---|
| Fine-grained signals | High | Always — prefer small signals |
| batch() | Medium | When updating 2+ signals together |
| peek() | Medium | When reading a signal in an effect without tracking |
| Lazy loading | High | For large components not needed at startup |
| computed() caching | Medium | For expensive derivations read in multiple places |