Key Takeaways
- Optimize measured digest and rendering work—not the number of properties in a controller. Unbound properties don't automatically create watchers.
- One-time bindings remove their watches after their values become defined and settle; they aren't watcher-free from the start.
- Stable tracking keys improve list DOM reuse. Pagination and virtualization address a different problem: how many rows remain rendered.
- Official AngularJS support ended in January 2022. Performance tuning is useful maintenance work, not a replacement for a support or migration strategy.
AngularJS performance optimization means reducing the work your application does during change detection and browser rendering. When an immediate rewrite isn't practical, cheaper watched expressions, carefully chosen one-time bindings, and smaller rendered lists can make legacy views more responsive.
This guide covers AngularJS 1.x, not Angular (2+). They're separate frameworks, not interchangeable releases; see our Angular versus AngularJS comparison for that distinction.
Understanding AngularJS's Digest Cycle
A watcher observes an expression so AngularJS can react when its value changes. Dirty checking means evaluating that expression and comparing its result with the previous value. A digest can evaluate watchers repeatedly because a watch listener can change another watched value.
$apply() evaluates an expression and then initiates a root-scope digest. Calling $digest() directly processes the current scope and its descendants. AngularJS-managed interactions commonly handle this integration for you; a raw browser or third-party callback can run outside AngularJS's execution context. Not every browser event automatically triggers a digest.
AngularJS-managed change / explicit $apply(...)
|
v
Digest: evaluate watched expressions
|
Listeners may update model and DOM
|
More changes or queued work? -- yes --> repeat
|
no
v
Digest ends; browser can renderThe loop settles when watched values stop changing and its asynchronous evaluation queue is empty. That makes digest frequency, expression cost, and the number of iterations important—not just watcher count.
What ng-if Actually Adds and Removes
<!-- Fragment: enabled is a Boolean on the current scope. -->
<div ng-if="enabled"></div>ng-if watches its condition. When the condition becomes truthy, it creates the associated DOM and a child scope; when falsy, it removes that DOM and destroys the child scope. The condition watch remains, but watches belonging to the destroyed child scope no longer participate.
By contrast, ng-show changes visibility rather than removing the subtree. Prefer ng-if for expensive sections that stay closed, but measure frequently toggled sections: recreating their DOM has a cost too.
Supporting Tools: A Reproducible Profiling Workflow
Start with Chrome DevTools rather than requiring an AngularJS-specific browser extension. Its Performance tools expose JavaScript execution and browser rendering; its Memory tools help investigate retained objects and detached DOM.
- Choose one repeatable interaction. Use the same dataset and steps: open the slow route, type a search term, refresh a list, or expand a row. Record the device, build, and test conditions.
- Capture a baseline. Open Performance, start recording, perform the interaction, and stop. Inspect interaction latency and long tasks—tasks exceeding 50 milliseconds—rather than relying on how fast the page feels.
- Separate scripting from rendering. Select the slow interval. Use Call tree to follow the execution path and Bottom-up to find expensive functions. With readable function names, inspect the time beneath
$digest, including application callbacks, separately from layout and paint. - Check growth over time. Performance monitor reports DOM-node counts and JavaScript heap size. For a view that slows after repeated navigation, compare Memory heap snapshots and inspect detached nodes still retained by JavaScript.
- Add targeted diagnostics only when needed. For watcher counts, use a development-only probe tested against your deployed AngularJS build. Fields such as
$$watchersare private implementation details; don't modify them as an optimization. - Change one thing and repeat. Save before-and-after traces under equivalent conditions. Compare interaction latency, digest execution time, DOM size, and retained memory—not merely a lower watcher total.
Use watcher count as a diagnostic, not a performance budget by itself: watcher expressions can perform very different amounts of work. Our guide to AngularJS watchers explains the underlying model.
How to Improve AngularJS Performance
Reduce Watcher Cost, Not Controller Variables
AngularJS watches registered expressions; it doesn't recursively watch every property you put on a controller. A plain DOM element doesn't automatically create a watcher either. The practical consequence: deleting an unbound property doesn't remove a watcher.
Follow the expensive call stacks back to their templates, directives, or explicit watches, then address the work they reveal:
- Precompute expensive display values. Move costly calculations out of repeatedly evaluated expressions, and recompute when their actual inputs change.
- Inspect collection filters. Filtering or sorting large arrays in templates can repeatedly process the collection during digest. Compute a displayed collection when the data or search criteria change instead.
- Avoid unnecessary deep watches.
$watch(expression, listener, true)uses deep comparison and copies the watched value for later comparison. Watch a specific property, or use$watchCollection()when shallow collection changes are sufficient. - Keep watch listeners small. Listeners that change other watched values can cause additional digest iterations. Avoid chains of derived-state updates when you can compute the result directly.
- Inspect repeated directives. A directive's template bindings and setup work are multiplied across rendered rows. Remove unnecessary per-row bindings before rewriting an entire view.
Keep reusable processing in services rather than expanding controllers into catch-all objects; see AngularJS controller best practices for that separation.
Use One-Time Bindings for Immutable Display Values
AngularJS 1.3 introduced one-time binding with the :: prefix; AngularJS 1.2 and earlier don't support it. Ordinary interpolation remains live, while one-time interpolation eventually stops observing its expression.
<!-- Alternative template fragments inside an existing item scope. -->
<!-- Live: use when the name can change. -->
<span>{{item.name}}</span>
<!-- One-time: use only when the name should stay unchanged. -->
<span>{{::item.name}}</span>The one-time watch remains while the expression is undefined. Once its value is defined, AngularJS schedules deregistration after the digest and checks that the value is still defined before removing the watch. It isn't immediately watcher-free.
Consequently, values such as null, 0, false, and an empty string can settle a scalar one-time binding. Don't initialize an asynchronously loaded label to an empty string if you expect that binding to display its later value.
Use one-time bindings for genuinely fixed display data, not editable names, changing status, or localized text that must update. Also keep the terminology straight: {{item.name}} is interpolation, not a two-way form binding; ngModel connects form controls with model properties. One-time interpolation isn't a general one-way data-flow architecture.
Improve ng-repeat DOM Reuse with Stable Keys
For object arrays, ngRepeat tracks object identity by default. An explicit tracking expression chooses the key used to associate records with their DOM blocks.
<!-- Before: template fragment using default object identity. -->
<li ng-repeat="item in vm.items">{{item.name}}</li>
<!-- After: alternative fragment using stable record IDs. -->
<li ng-repeat="item in vm.items track by item.id">
{{item.name}}
</li>If a refresh replaces objects with new objects representing the same records, stable IDs let AngularJS reuse existing DOM blocks. track by item.id doesn't inherently reduce watchers; its benefit is avoiding unnecessary DOM destruction and recreation. The ngRepeat reference documents tracking and reuse behavior.
Keys must be unique within the collection and stable for each record. track by $index identifies positions, not records, so it's a poor default for lists that reorder or contain row-specific state.
Bound Lists with Pagination or Virtual Scrolling
Large lists combine AngularJS work with browser work. Repeated templates create scopes and bindings, while a large DOM also increases rendering costs. Reducing bindings alone doesn't remove the browser's obligation to manage those nodes.
| Approach | Rendered content | Main trade-off |
|---|---|---|
| Pagination | The current page | Bounds rendered rows; users navigate between pages. |
| Append-only infinite scrolling | Everything loaded so far | Defers initial work, but DOM and live bindings can keep growing. |
| Virtual scrolling | A moving window of rows, usually with a small buffer | Bounds rendered rows, but requires careful scrolling and focus behavior. |
Infinite loading controls when records arrive; virtualization controls how many remain rendered. Combining them can bound the DOM while loading more data, but virtualization only bounds AngularJS watcher work if off-window row scopes and watches are actually removed or otherwise excluded from checking.
A Small Client-Side Pagination Example
These fragments assume an existing AngularJS application module named legacyApp. Load the controller registration before bootstrapping that application, then place the template inside its managed page.
// Fragment: register on the application's existing module.
angular.module('legacyApp').controller('ListController', function ListController() {
const vm = this;
vm.items = [
{ id: 'a', name: 'Alpha' },
{ id: 'b', name: 'Beta' },
{ id: 'c', name: 'Gamma' }
];
vm.pageSize = 2;
vm.showPage = function (offset) {
if (offset < 0 || (offset >= vm.items.length && offset !== 0)) {
return;
}
vm.offset = offset;
vm.visibleItems = vm.items.slice(offset, offset + vm.pageSize);
};
vm.showPage(0);
});<!-- Fragment: inside the existing AngularJS application. -->
<section ng-controller="ListController as vm">
<ul>
<li ng-repeat="item in vm.visibleItems track by item.id">
{{item.name}}
</li>
</ul>
<nav aria-label="List pagination">
<button type="button" ng-click="vm.showPage(vm.offset - vm.pageSize)"
ng-disabled="vm.offset === 0">Previous</button>
<button type="button" ng-click="vm.showPage(vm.offset + vm.pageSize)"
ng-disabled="vm.offset + vm.pageSize >= vm.items.length">Next</button>
</nav>
</section>This example renders at most two records and calculates the slice during page selection rather than from a repeatedly evaluated template function. It still retains the full source collection in memory. When downloading or processing the complete dataset is itself the bottleneck, use server-side pagination and filtering instead.
After replacing or filtering the source collection, reset the page and rebuild visibleItems. For infinite or virtual scrolling, test keyboard navigation, focus retention, screen-reader behavior, and loading announcements; dynamically adding and removing content requires deliberate accessibility behavior.
Production Checklist and Legacy Containment
- Test edits, refreshes, reordering, empty results, and page resets—not just the initial render.
- Clean up application-owned listeners and recurring work when their view is destroyed. AngularJS provides the scope
$destroyevent for cleanup, and$intervaltasks aren't automatically canceled when a scope is destroyed. - Inventory the legacy application's dependencies, assign vulnerability-triage ownership, and evaluate the actual coverage of any independent maintenance provider.
- Keep new work behind clear application and API boundaries. Don't treat route separation alone as a security boundary.
If Angular is your migration target, UpgradeModule supports hybrid applications in which AngularJS and Angular coexist. Evaluate that path against replacing bounded routes; a hybrid application still runs both frameworks, so profile the integration rather than assuming migration immediately eliminates digest costs.
FAQ
Is AngularJS still supported in 2026?
No. Official AngularJS support ended in January 2022, and its security policy lists no supported versions. Performance tuning doesn't restore support; assess migration or independently maintained support separately.
How do I find the cause of a slow AngularJS digest cycle?
Record a representative interaction in Chrome DevTools Performance, select the slow interval, and inspect Call tree and Bottom-up for expensive execution beneath the digest. Compare scripting with layout and paint, then repeat the same interaction after one targeted change.
Does track by reduce watchers in AngularJS?
No, not inherently. A stable tracking expression such as track by item.id improves DOM reuse when records are refreshed; reducing rendered rows or eligible live bindings addresses watcher work.
When should I use one-time binding (::) in AngularJS?
Use it in AngularJS 1.3 or later for values that won't need updating after initialization. Its watch remains until the value becomes defined and settles, then AngularJS deregisters it.
Does infinite scrolling improve AngularJS performance if all rows remain in the DOM?
It can reduce initial loading and rendering work, but it doesn't bound the eventual DOM or live bindings when previous rows remain. Pagination or virtualization addresses that accumulated rendering cost.
Further Reading
- AngularJS security policy — official support status.
- AngularJS Scope API — watches, digest processing, and deep versus shallow observation.
- AngularJS expressions guide — one-time binding and value stabilization.
- AngularJS ngRepeat reference — tracking keys and DOM reuse.
- Archived AngularJS filters guide — moving costly collection processing out of templates.
- Chrome DevTools performance reference — recording analysis, call trees, and rendering work.
- Chrome DevTools memory troubleshooting — heap snapshots and detached DOM investigation.
- Angular UpgradeModule documentation — hybrid AngularJS-to-Angular migration.
