Taming Digest Lag in AngularJS with One-Time Binding ::
Use AngularJS 1.3+ :: one-time binding to remove watchers from read-only grid columns and reduce digest-cycle lag in legacy CRUD apps.
14 Jul 2026, 23:34 UTC

The lag you feel in big read-only grids
Legacy AngularJS CRUD apps often render tables with hundreds of rows. Each {{value}} interpolation and each ng-model creates a watcher that runs on every digest cycle. With 500 rows and 10 columns you can easily reach several thousand watchers, which makes typing and AJAX updates feel sluggish.
AngularJS 1.3 added one-time binding with the :: prefix. It tells the compiler to render the value once and then deregister the watcher. For read-only data this cuts memory overhead and digest work without rewriting the app.
How :: one-time binding works
Standard binding:
<td>{{ user.email }}</td>
Creates a persistent watcher that updates the DOM whenever user.email changes and watches the DOM for changes back.
One-time binding:
<td>{{ ::user.email }}</td>
The expression is evaluated on the first digest when the value is defined. Once resolved, Angular removes the watcher. The view updates once, then stays static. It is one-way from model to view and is not suitable for inputs.
Version assumption
:: is supported in AngularJS 1.3 and later. Projects on 1.2 cannot use it.
Worked example: read-only columns in a user grid
An admin list shows id, email, role as read-only and status as editable.
<tr ng-repeat="user in users">
<td>{{ ::user.id }}</td>
<td>{{ ::user.email }}</td>
<td>{{ ::user.role }}</td>
<td>
<select ng-model="user.status" ng-options="s for s in statuses"></select>
</td>
</tr>
Only the select retains a two-way watcher. The three :: columns stop watching after initial render, which reduces the watcher count for large ng-repeat blocks.
Verifying the reduction
Open DevTools console on a page with the grid and inspect the repeat scope:
var scope = angular.element($0).scope();
console.log(scope.$$watchers ? scope.$$watchers.length : 0);
Compare a version with and without :: on the same data set. $$watchers is an internal structure, not part of the public API, and is for inspection only.
Also confirm behavior: changing the model after initial render does not update a :: bound element, which is the intended one-time semantics.
Trade-offs and limits
- Data must be present at first digest. If user.email is undefined initially and later populated, :: will never update.
- Not for inputs. Use ng-model for editable fields; :: is for display only.
- Debugging can be harder because the binding disappears after resolution.
- Overuse on data that changes frequently defeats the purpose and can cause stale UI.
Actionable checklist
- Identify read-only columns in large ng-repeat blocks.
- Apply :: to interpolations that are static after load, e.g., {{ ::user.email }}.
- Keep ng-model on editable controls without ::.
- Inspect watcher count via $$watchers for a representative scope before and after.
- Document which fields are one-time to avoid surprise when data arrives late.
Start with one grid, measure watcher count, then expand. The mental shift from always-watching to explicitly choosing what watches is the larger maintainability win for long-lived AngularJS apps.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.