Why does an AngularJS app get slow, and how do you fix it?
Quick Answer
Every digest cycle re-evaluates every watcher on the page, and it loops until stable. With thousands of bindings (big ng-repeat lists, deep nested scopes) each digest becomes slow, and it runs on every interaction. Fix it by cutting watcher count: one-time bindings ({{::x}}), track by, pagination/virtual scroll, ng-if instead of ng-show for large hidden subtrees, and debouncing model updates.
The digest cycle checks every watcher on the page and repeats until nothing changes, and it fires on basically every interaction. So the more bindings you have — a big ng-repeat, deeply nested scopes — the slower every single digest gets. The fixes are all about reducing watcher count: one-time bindings with the :: syntax for data that won't change, track by on repeats so the DOM is reused, paginating or virtual-scrolling long lists, and using ng-if instead of ng-show for big hidden sections so their watchers are removed entirely. I also debounce model updates on text inputs and never call a function directly in a binding, because that re-runs every digest.
- One-time binding:
{{::item.name}}— watched once, then the watcher is removed. Use for data that doesn't change after first render. track by item.idinng-repeat— reuses DOM nodes instead of rebuilding the list on every change.- Limit rows — paginate, or use a virtual-scroll directive so only visible rows have watchers.
ng-ifvsng-show—ng-ifremoves the subtree (and its watchers) from the DOM;ng-showkeeps them all active.- Debounce inputs:
ng-model-options="{ debounce: 300 }"so keystrokes don't trigger a digest each. - Avoid function calls in bindings:
{{ getTotal() }}runs every digest; compute it once and bind the value. - Batch out-of-Angular work with
$applyAsync; use$scope.$digest(local) over$scope.$apply(root) where possible.