Three ways to put a select in an AngularJS 1.x app, on the same data and the same page. See angularjs/README.md for what each one is.
The dataset is a real timezone picker: every IANA zone your browser knows
(Intl.supportedValuesOf('timeZone'), ...
of them - the exact count is your browser's), built
... times over. A field real apps ship, and
already enough to make ui-select crawl.
Change the two numbers below and re-run to watch the ratios move (they are
also the ?items= and ?widgets= query parameters, so
a run is linkable). The incumbent <select ng-options> builds every
option up front, so its cost is linear in the item count; llselect builds the
list only on open, so its cost is flat. That is what widens the gap as the
list grows - not superlinearity. ui-select is the one that is genuinely
superlinear, and only per digest.
One live widget each, all four bound to the same
ng-model, on the same ...-zone
list. Two things to do with them.
Feel the difference. Open each one and type in its search box. The
table above says ui-select costs around 100x more per digest than
<llselect-single>; this is what that number actually
feels like. Raise the candidate count and re-run to widen it.
Check the binding both ways. Choosing in any one of them must move all four, and the model is printed below. The two buttons are the interesting part, because they ask each widget what it thinks "the same item" means:
angular.copy. Watch the two booleans, not the text: every
widget still displays "the right thing", which is exactly what makes this
worth checking.
=== on the
modelMapper (uiSelectSingleDirective.js:38) -
not angular.equals, and its track by does not
enter into it. So it cannot match the copy, and falls back to
return inputValue: it displays the copy, and
$select.selected is an object that is not in its own list.
The text looks right and the state is wrong.
<llselect-single> is given track by i.id,
which becomes llselect's compareFn, so it treats the copy as
the same item - the popup still marks the right row.
ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}"
on every row, every digest. Both functions do an
indexOf over the whole item list
(uiSelectController.js:317 and :358), so the cost
is O(n^2) per digest. llselect has no watchers on the list at all, so a
digest does not touch it. Measured in microseconds because that is the unit
it lives in: reported in ms, the fastest contestant printed 0.00 and the
column said nothing.
$compile link per row, which
<llselect-single> does not pay.
validation="required" on <llselect-single>
must not cost more than putting it on the <select ng-options>
it replaces. angular-validation is element-agnostic (it watches
ctrl.$modelValue), so the difference should be widget cost
only, not validation cost.
docs/llm/SPEC_BENCHMARK.md applies to the native select
in the main benchmark. Its build and digest are real, and it renders every
option up front rather than lazily.
ng-if="$select.open" on its rows), so this column is mostly
trigger cost.
performance.now() to 100us, which a single digest lands under. Timed one at a time, every contestant here reads 0.scope.$digest() and synthetic clicks, not real input, so it is a lower bound on real perceived latency for all of them equally.positioning.ts isAnchorHidden), so an off-screen stage would measure an open that never happened. Paint is therefore included, for everyone equally.