Using AngularJS One-Time Binding (::) to Reduce Watchers in Legacy Apps
Use AngularJS one-time binding (::) to drop watcher count and speed up digest cycles in large lists.
09 Jul 2026, 21:45 UTC

Quick answer: use :: to make bindings evaluate once
In AngularJS 1.x the digest cycle checks every registered watcher on each loop. When a view contains many repeated items (for example, an ng-repeat over a list of hundreds of rows), each binding adds a watcher and the cost of the digest grows linearly. Adding the double‑colon prefix (::) to an expression creates a one‑time binding: the expression is watched until it returns a defined (non‑undefined) value, after which AngularJS automatically deregisters that watcher. Subsequent digests no longer re‑evaluate the binding, so the watcher count drops dramatically for data that only needs to be rendered once.
How one‑time binding works
During the first digest after the template is compiled, AngularJS evaluates the expression. If the result is undefined, the watcher stays alive and the expression is evaluated again on the next digest. Once the expression yields any value other than undefined (including null, 0, false, or an object), the watcher is removed from the scope’s $$watchers list. From that point forward the DOM node retains the value that was captured at stabilization, and no further dirty‑checking occurs for that binding.
Worked example: large ng-repeat list
<!-- index.html -->
<div ng-app='myApp' ng-controller='ListCtrl'>
<ul>
<li ng-repeat='item in ::items'>
<span>{{::item.name}}</span> <!-- one‑time binding -->
<input type='text' ng-model='item.name' /> <!-- normal binding for comparison -->
</li>
</ul>
</div>
<script>
angular.module('myApp', [])
.controller('ListCtrl', function($scope) {
// Simulate data that arrives after a short delay
$scope.items = [];
setTimeout(function() {
$scope.items = [
{name: 'Alice'},
{name: 'Bob'},
{name: 'Charlie'}
// … add more rows to reach a realistic size
];
$scope.$apply();
}, 500);
});
</script>
In this snippet the ng-repeat itself uses ::items, so the array is watched only until it becomes defined. Each list item uses {{::item.name}} for the displayed text, which becomes a one‑time binding as soon as the name property is set. The accompanying input uses a normal ng‑model binding, so you can verify that typing into the field updates the model but does not change the displayed after the first render.
Limits and common mistakes
- Stale UI for live data. If you apply
::to a value that should change after the first definition (e.g., a counter, a clock, or a locale‑dependent string), the view will never reflect later updates. This is a frequent source of 'UI not updating' bugs in maintained codebases. - Expressions that stay undefined. If the bound expression never becomes defined (perhaps because of a failed async request), the watcher remains active and you gain no performance benefit.
- Filters are also one‑time. A construct like
{{::item.price | currency}}evaluates the filter once; if the filter depends on a changing locale or timezone, the output will stay stuck with the initial value. - Version requirement. The
::syntax was introduced in AngularJS 1.3. Verify the version withangular.version.fullbefore relying on it; older releases will treat the double colon as a syntax error.
Practical way to check the effect: open the browser’s developer tools, select an element rendered by the one‑time binding, and run angular.element($0).scope().$$watchers.length (or use a watcher‑counting snippet). Compare the count before and after removing the :: prefixes; you should see a noticeable drop when the prefixes are present.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.