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 attach a runtime hook that browser extensions or console scripts can read. Apps opt in explicitly — the hook is a passive surface, not an auto-instrumenter:
import { connectDevTools } from "@whisq/devtools";
if (import.meta.env.DEV) connectDevTools();The installed hook exposes, via globalThis.__WHISQ_DEVTOOLS__:
registerSignal(name, signal)/getSignals()— a ledger of named signals and their current values (read via.peek(), so inspection doesn’t subscribe).registerComponent(name)/getComponents()— a list of active component names.logEvent(type, data)/getEvents()/clearEvents()— an append-only event log with timestamps.
Full API: /api/devtools/.
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 |
Next Steps
Section titled “Next Steps”Docs current to v0.1.0-alpha.9 . All releases →