Choosing Between Knockout pureComputed and computed: A Practical Guide
Default to pureComputed for UI-derived values in Knockout; it evaluates only when subscribed, auto-cleans on unsubscribe, and avoids the memory leaks of regular computed. Reserve computed for side effects and writable derived state.
01 Sept 2025, 18:56 UTC

The Problem: Derived State That Drags Down Performance
You've built a Knockout view model with a handful of ko.computed properties. Everything works, but the profiler shows excessive re-evaluation — even when the bound UI elements are hidden or destroyed. Memory grows because computeds hold onto their dependency subscriptions long after they're needed. The fix isn't more dispose() calls; it's understanding when ko.pureComputed (introduced in Knockout 3.2.0) should be your default.
Thesis: Default to pureComputed for UI-Derived Values
Use pureComputed for any derived value that feeds a UI binding (text, visible, foreach, etc.). The binding system creates subscriptions automatically, so pureComputed evaluates only when something is actually listening. Reserve regular computed for side effects — logging, AJAX, manual subscriptions — or when you need synchronous read access without a subscription.
How pureComputed Differs: Lazy Evaluation and Subscription Awareness
A regular computed re-evaluates immediately whenever any of its dependencies change, regardless of whether anyone cares about the result. pureComputed defers evaluation until the first subscription arrives, then caches the result until a dependency notifies a change. When the last subscriber disposes (e.g., a binding is removed via if:false), pureComputed releases its dependency subscriptions, allowing garbage collection of the entire chain.
In Knockout 3.5+, pureComputed also enables deferEvaluation: true by default, delaying the initial evaluation until the first subscription. This is valuable for expensive derivations that may never be needed — such as a filtered list that only renders when a tab is activated.
Memory Behavior: Automatic Cleanup vs. Manual Disposal
Regular computed retains its dependency subscriptions until you explicitly call computed.dispose(). If you create view models in a loop (e.g., rows in a virtualized grid) and forget to dispose, those computeds — and their dependency graphs — leak. pureComputed solves this by tying its lifecycle to subscriptions: when the UI binding goes away, the subscription count drops to zero and the computed cleans up itself.
You can verify this with ko.computedContext.getDependenciesCount(). On an unsubscribed pureComputed, it returns zero; on a regular computed, it returns the number of tracked dependencies even when nothing is listening.
Worked Example: A Searchable Product List
Consider a view model that filters products based on a search term. The filtered list binds to a foreach; the search term is an observable.
function ProductListViewModel() {
this.searchTerm = ko.observable('');
this.allProducts = ko.observableArray([
{ name: 'Widget A', price: 10 },
{ name: 'Widget B', price: 20 },
{ name: 'Gadget X', price: 15 }
]);
// UI-derived: use pureComputed
this.filteredProducts = ko.pureComputed(function() {
var term = this.searchTerm().toLowerCase();
return this.allProducts().filter(function(p) {
return p.name.toLowerCase().indexOf(term) >= 0;
});
}, this);
// Side effect: log search changes — use regular computed
this.searchLog = ko.computed(function() {
console.log('Search changed to:', this.searchTerm());
}, this);
}
ko.applyBindings(new ProductListViewModel());
When the foreach binding is active, filteredProducts has a subscription and evaluates on each searchTerm change. If you wrap the list in <!-- ko if: showResults --> and set showResults(false), the binding disposes its subscription. filteredProducts then releases its dependency on searchTerm and allProducts, stopping evaluation entirely. searchLog continues logging because it has no subscribers but still re-evaluates on every change — exactly what you want for a side effect.
Trade-offs and Limitations
- No writable pureComputed: You cannot pass a write function to
pureComputed; doing so throws. If you need a writable derived value, use regularcomputedwithread/write. - Testing requires subscription: In Jest/Vitest, reading a
pureComputedwithout subscribing returnsundefinedor a stale value. Callcomputed.subscribe()in your test setup to trigger evaluation. - Hidden subscriptions: Third-party binding handlers (e.g.,
knockout-validation,knockout-mapping) may create subscriptions you don't see, keeping apureComputedalive longer than expected. Verify withgetDependenciesCount(). - Error caching: If the evaluator throws, the error is cached and re-thrown on subsequent reads until dependencies change. Regular
computedbehaves similarly but may re-evaluate more often, potentially clearing the error sooner.
Actionable Closing: Verify Your Choice
- Audit your view models: any
computedthat only feeds UI bindings? Change topureComputed. - Add
ko.computedContext.getDependenciesCount()checks in development to confirm unsubscribedpureComputedinstances show zero dependencies. - Profile memory in Chrome DevTools: create/destroy view models, force GC, and confirm
pureComputedinstances are collected while undisposed regularcomputeds persist. - Upgrade to Knockout 3.5.1+ to avoid the 3.4.x stale-subscription bug.
Default to pureComputed for derived UI state. It aligns evaluation with actual usage, eliminates manual disposal boilerplate, and prevents the silent memory leaks that plague long-running single-page applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.