advanced15 min

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

AngularJSNot executed
This lab does not run in your browser or on VisaSparkSchools's servers. Read the code, predict what it does, then reveal the real expected output.

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

Knowledge check

1. What does one-time binding (`{{::value}}`) do once it gets a non-undefined value?
2. What commonly causes watcher-count explosion in real AngularJS apps?
3. Why is an expensive expression directly inside a template binding a performance concern?

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.