Common Performance Pitfalls
Watcher-count explosion and other real, well-documented AngularJS performance issues.
What you'll learn
- Explain how a large ng-repeat list can cause watcher-count-related slowdowns
- Identify one-time binding (::) as a real mitigation for data that never changes after initial render
- Recognize other common causes of digest-cycle slowness in legacy code
Explanation
Following on from the digest-cycle lesson, the single most common real-world AngularJS performance complaint is watcher-count explosion: every binding ({{ }}), every ng-model, and every iteration of an ng-repeat registers its own watcher, and every digest cycle re-checks all of them. A page rendering a list of a few hundred items, each with several bindings, can easily register thousands of watchers -- and every user interaction re-runs the whole dirty-checking pass across every one of them.
One real, commonly-used mitigation for data that's set once and never changes afterward is one-time binding, written with a double-colon prefix: {{::user.name}} -- AngularJS evaluates this expression, and once it gets a non-undefined value, it stops watching it entirely, removing that watcher from future digest cycles. This is a genuine, meaningful optimization for template sections that display static or rarely-changing data (a page title, a user's name that won't change during the page's lifetime) without needing continuous two-way binding.
Other common causes of digest-cycle slowness in real legacy code: complex, expensive expressions directly inside a template binding (better computed once in the controller and exposed as a simple property instead), and unnecessarily broad $watch calls that fire far more often than the code actually needs.
Guided lab
Predict: One-time binding stops future re-checks
This models one-time binding's 'stop watching after first real value' behavior. Predict which digest passes still report a value for the one-time-bound watcher.
let oneTimeWatcherActive = true;
let oneTimeValue;
function digestPass(passNumber, sourceValue) {
if (oneTimeWatcherActive) {
oneTimeValue = sourceValue;
console.log(`Pass ${passNumber}: watched, value = ${oneTimeValue}`);
if (oneTimeValue !== undefined) {
oneTimeWatcherActive = false; // one-time binding deregisters itself
}
} else {
console.log(`Pass ${passNumber}: not watched anymore (value stays ${oneTimeValue})`);
}
}
digestPass(1, undefined);
digestPass(2, "Ada");
digestPass(3, "changed later, but ignored");Stuck? Get a hint.
Common mistakes
- Using regular two-way binding for data that's genuinely set once and never changes, missing the one-time binding (::) optimization.
- Putting an expensive computation directly inline in a template expression, causing it to re-run on every single digest cycle rather than being computed once and cached.
- Assuming watcher count is never a real concern -- for a small page it usually isn't, but a large ng-repeat-heavy page is a well-documented case where it genuinely matters.
Knowledge check
Takeaway
Use one-time binding (::) for data that never changes after initial render, and avoid expensive inline template expressions -- both are real, well-documented performance fixes.
Summary
Watcher-count explosion (especially from large ng-repeat lists) is a genuine AngularJS performance concern; one-time binding (::) and avoiding expensive inline expressions are real mitigations.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.