Slow performance with angular material md-select and ng-repeat - angularjs

I'm writing an enterprise application using angular and angular material and have problem with the performance of a medium sized (in my opinion) form. Especially in IE.
(Working demo, see https://codepen.io/tkarls/pen/vGrqWv . Klick on the card title and it pauses slightly before opens. Especially using IE and mobile. Desktop chrome works pretty well.)
The worst offenders in the form seem to be some md-selects with ng-repeat on them.
<md-select ng-model="form.subchannelId" ng-disabled="vm.readOnly">
<md-option ng-repeat="id in subchannelIds" value="{{::id}}">{{::id}}</md-option>
</md-select>
<md-select ng-model="form.serviceReference" ng-disabled="vm.readOnly">
<md-option ng-repeat="id in serviceReferences" value="{{::id}}">{{::countryId}}{{::id}}</md-option>
</md-select>
<md-select ng-model="form.audioCodec" ng-disabled="vm.readOnly">
<md-option ng-repeat="audioCodec in audioCodecs | orderBy:'toString()'" value="{{audioCodec}}">{{::systemVariables.encoders.aac[audioCodec].displayName}}</md-option>
</md-select>
<md-select ng-model="form.audioSource" ng-disabled="vm.readOnly">
<md-option ng-repeat="audioSource in audioSources | orderBy:'toString()'" value="{{audioSource}}">{{audioSource}}</md-option>
</md-select>
<md-select ng-model="form.padSource" ng-disabled="vm.readOnly">
<md-option ng-repeat="padSource in padSources | orderBy:'toString()'" value="{{::padSource}}">{{::padSource}}</md-option>
</md-select>
<md-select ng-model="form.lang" ng-disabled="!form.generateStaticPty || vm.readOnly">
<md-option ng-repeat="langKey in langKeys | orderBy:'toString()'" value="{{::langs[langKey]}}">{{::langKey}}</md-option>
</md-select>
<md-select ng-model="form.pty" ng-disabled="!form.generateStaticPty || vm.readOnly">
<md-option ng-repeat="ptyKey in ptyKeys | orderBy:'toString()'" value="{{::ptys[ptyKey]}}">{{::ptyKey}}</md-option>
</md-select>
The data model looks like:
$scope.subchannelIds = [0, 1, 2]; //up to 63 in real life
$scope.serviceReferences = ["000", "001", "002"]; //up to 999 in real life
$scope.ptys = {
"No programme type": 0,
"News": 1,
"Current Affairs": 2}; //Up to ~30 in real life
$scope.ptyKeys = Object.keys($scope.ptys);
$scope.langs = {
"Unknown": "00",
"Albanian": "01",
"Breton": "02"}; //Up to ~100 in real life
$scope.langKeys = Object.keys($scope.langs);
The other ng-repeats are small with 3-5 items each. I think that a modern browser should handle datasets of this size and render it very quickly. So hopefully I'm doing something wildly wrong with my HTML code. The data is fetched from the server in real life but I do pre-fetch it so once the form is ready to be displayed it is already in the $scope.
I tried to pre-generate HTML after I fetched the data using normal js loops. And then insert just the html snippet like:
{{::preGeneratedHtmlHere}}
But then angular would not treat it as html but text...
Any help on how to optimize this is appreciated!

Angular material has very poor performance, because the objects pinned to the scope are huge, which makes the digest cycle very long and inperformant.
You should try it first with the default select and ng-options (DOCS HERE). If this works better for you, I'd suggest using plain html and then use MaterializeCSS to get the look and feel of Material Design.

Yes, making it all plain old html will speed it up, however then you lose all the eye candy. To have the good parts from both of the worlds you can do some basic optimizations.
Do you really need to watch the collection - are the collections
going to change and if so can't you trigger a digest then? As you did
with the id you can also one-way bind the repeated collection as
well.
ng-repeat="id in ::serviceReferences"
You don't really need
all the options preloaded, right? Since you're using
angular-material, the default drop-down will be exchanged with
multiple elements, to emulate the drop-down behavior. I did just
remove the options list, replaced it with the actually selected
element and populate the list only when the control has gained
focus. See documentation.
Still I agree the angular-material has a poor performance. It simply does not scale well. 1-2 controls work but if you have more then 10 it starts to fail.
PS.: Don't cook the $scope soup!

For a big amount of items in ng-repeat will cause some issues. When angular use ng-repeat to create nested list , a single watcher will be created for each item. Hundreds of watchers will slow down the performance obviously on moible (and IE probably). We used have this issue with ng-repeat, so the best practice is avoid using ng-repeat if you could, create and attach the watcher when you really need to.
So I think the possible solution is, try to use normal for loop instead of ng-repeat.

Related

Angular Material : Why md-select when used with ng-model-options triggers the ng-change?

I have a use case wherein I need to use a md-select and provide options from a array. The selected value is saved and fetched from the database. In case, the database has no value stored it returns null.
Now if the value from database is null, I need to set the 4th value of array (i.e Default). So I have the following logic:
$scope.MR= $scope.data.MR !== null ? $scope.data.MR : $scope.MRList[3];
and my template is :
<md-input-container class="flex flex-sm-100 TS_action_dropdown md-input-has-value">
<md-select ng-model="MR" ng-change="updateManualRating(target, MR)" class="new_delta_select" ng-model-options="{trackBy: '$value.name'}">
<md-option ng-value="t" ng-repeat="t in MRList track by $index">{{t.name}}</md-option>
</md-select>
</md-input-container>
As per Angular docs https://material.angularjs.org/latest/api/directive/mdSelect, we need to use ng-model-options to set a pre-defined value, that is matched against the option list. I have done the same, however, after this whole implementation, my ng-change function (written on change of md-select) gets triggered automatically while page loading.
As per Angular docs, this is triggered explicitly on user manual change. I believe too that's how it works. But I am confused, why is it happening. Does Angular material somewhere plays with $viewController and triggers a similar effect?
Or is this a bug or known issue. If yes, How it can be avoided or worked around?

AngularJS RDBMS data handling best practices

With SPA app growth, some things become unclear with Angular JS.
Suppose, we have a RDBMS data structure with lots of referencing entities, like Country (id), State (id, countryId), City (id, stateId), Street (id, cityId).
And we need to render most of it within a single view, just like a street index grouped by parent recursively. I know it's not so practical example, especially considering that our real application entities have mutual data that we need to bind to.
How would "Best Practice" look like?
Now, we're using lots of ng-repeat's like this:
<ul>
<li ng-repeat="country in countryList">
{{ country.name }}
<ul ng-if="(stateList | filter:{countryId === country.id}).length">
<li ng-repeat="state in filterStatesByCountryId(country.id)">
{{ state.name }}
<ul ng-if="(cityList | filter:{stateId === state.id}).length">
<li ng-repeat="city in cityList | filter:{stateId === state.id}">{{ city.name }}</li>
</ul>
</li>
</ul>
</li>
</li>
filterStatesByCountryId(country.id) is the same as stateList | filter:{countryId === country.id}.
Our app must have bindings in place, so that if street.name property gets updated in the StreetService, the view reflects this change. Therefore $scope.streetList = StreetService.all is not an option right out of the box and instead of $scope.streetList we would have $scope.street = StreetService and then use it as $scope.street.all | filter:{cityId: city.id}.
Must say this looks scary.
What would be the most optimal both performance and service architecture wise way of rendering and displaying this kind of lists of data?
One of idea would be to store whole merged hierarchy of entities into one. So that country with all states, cities and streets becomes one object. Then country entity would have states {Array.<StateEntity>} property, state {StateEntity} would have cities {Array.<CityEnity>} prop, etc. But then if a single street.name would get updated, we would need to traverse whole country data object to find what street we need to update, do the update and then re-render the view, or just have a $scope.watch(expr, fn, true) which would be an overkill for that complex hierarchy.
Thanks.
P.S. We cannot switch to NoSQL for storing data.
Angular is not really built to deal with relational data structures I can see that what you're trying to do here mimicks C# MVC templating for loops.
The thing is let's say you have:
- 100 countries in your list => 100 watchers
- 20-ish states per country => 2000-ish watchers
- 30-ish cities per country => 60 000 watchers
IF you would show everything on one screen, now that is not what you're doing of course, but keep that in mind.
As an initial performance improvement I would add a track by to each ng-repeat statement.
<li ng-repeat="state in filterStatesByCountryId(country.id) track by country.id">
From the angular documentation it states:
If you are working with objects that have an identifier property, you can track by the identifier instead of the whole object. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones
and also
Note: track by must always be the last expression
Now, ng-repeat can be a bit sluggish in performance, I would not 2-way databind the view properties:
{{ city.name }} => {{ ::city.name }}
This could give you a significant speed increase in general, because angular won't bind watchers to those fields.
To solve your base problem however, I would model it as a singular JSON dataset. However you don't want to go through the hassle of mutating your dataset to work with Angular. At least I wouldn't, so what I would go for just a simple one-way databinding in this case. I don't know how often a street name is going to change on the fly.
It would look something like this:
<li ng-repeat="country in ::countryList track by country.id">
Also maybe try using controllerAs syntax, helps for cleaner code.
However if you really bump into speed issues with ng-repeat it's always best to write your own version of it tailored to your use-case.

One-way / One-time data binding in AngularJS

While it is not really a performance issue with my very small and very quick application, I am interested in becoming proficient in dealing with large applications BEFORE I get to working on large applications. This is causing me to identify ways to optimize AngularJS in as many ways as possible, and to integrate these optimizations as part of my "normal" coding routine. So! I've been scratching my head for several hours now, scouring articles and the documentation, trying to get ahold of the best method for me to achieve the following:
I have a drop-down select menu with options that I would like to be one-way data bound, from view to model, as there is no need to have the model update the view in this case.
Some questions about the above:
Will this save me time in the $digest cycle? Or does it not matter if I just use the two-way data binding of ng-model?
My other question is a little more involved.
EDIT: This issue has been fixed (as seen below), but I am now encountering a new issue. I have posted this new issue to the bottom of this question for better visibility.
Here is my template:
DropdownTemplate.html
<div class="form-group">
<label for="{{::regarding}}" class="col-sm-12 control-label">{{::regarding}}</label>
<div class="col-sm-12">
<select class="form-control" id="{{::regarding}}" data-ng-model="data" data-ng-change="update()">
<option selected>0</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
<option>13</option>
<option>14</option>
<option>15</option>
<option>16</option>
<option>17</option>
<option>18</option>
<option>19</option>
<option>20</option>
</select>
</div>
</div>
Here is my directive:
.directive('trDropdown', ['Ticket', function(Ticket) {
return {
restrict: 'E',
templateUrl: 'DropdownTemplate.html',
scope: {
regarding: '#'
},
link: function($scope, $elem, $attrs){
$scope.data = 0;
$scope.update = function (){
Ticket[$scope.regarding] = $scope.data;
//console.log(Ticket[$scope.regarding]);
}
}
}
}]);
And here is what it looks like when I utilize this custom element in my main html page:
<tr-dropdown regarding="Adults"></tr-dropdown>
So far, my code runs the way it should, except for one small bug. The bug is that, if I don't include the bit about $scope.data = 0; in the link part of the directive, the attribute "selected" (or even selected="selected") doesn't register when my custom element is appended to the main html page. Despite the bug (which is not my primary concern), I don't feel like this is the most elegant way for me to handle the concept of one-way binding, if I even need it at all. I know that there is a plugin called bindonce that I am willing to use, although I have been searching first for a native solution.
Any discoveries found through the struggles of others is most welcome on this end!
**EDIT: One-time binding is now successfully working but I've encountered a strange anomaly. Batarang shows my expression {{::regarding}} as continuing to pull resources, each time one of the dropdowns is changed. As far as I understand, {{::regarding}} should be de-registered and not even a factor in the further performance of the page. What gives? **

Angular performance in ng-repeat

I was faced the performance issue of ng-repeat directive, I rendered the PDF formFields using ng-repeat directive, somehow it halts my browser. But when I use track by $index and limitTo:1 together in same ng-repeat, it works fine and it enhances the rendering speed. I can't understand the logic behind this but it works extraordinary fast :)
This works faster and fine for me, also limitTo:1 not work, some how it is binding the limit with $index and halting of browser issue fixed.
<div ng-repeat="friend in friends track by $index | limitTo:1">
{{ friend.id }} — {{ friend.name }}
</div>
if we use limitTo before track by $index than it behaves normally the limit behavior of angular js which is understandable
<div ng-repeat="friend in friends | limitTo:1 track by $index">
{{ friend.id }} — {{ friend.name }}
</div>
OR
<div ng-repeat="friend in friends | limitTo:1">
{{ friend.id }} — {{ friend.name }}
</div>
Although i achieve my performance goal, but i want to know the logic behind this.
Here is the link of jsfiddle
http://jsfiddle.net/neglingeyes/G6q84/
You can not measure multiple ng-repeat directives at once with post-repeat.
This is because the way it tries to figure out when the rendering is complete is to set a timeout function for the next event loop tick, and that really means all the directives on the page have finished rendering, not just this specific ng-repeat.
In your demo fiddle, I guess each next post-repeat timeout gets processed in a separate event loop tick or something like that, and this is why their "measured" performance seems to get worse and worse. If you changed their order, again the first will seem to be the fastest. But only by a couple of ms - a trivial difference.
Have you tried to benchmark each case separately? Also, on what platform and with what data? On desktop browsers and with such short data it is too fast to make a difference either way.
As to what the first code segment does - it is parsed as friend in friends track by ($index | limitTo:1) ("What brackets?" ;-) )
The limitTo filter can only be applied to arrays, not to scalar variables like the number $index. But the angular filters normally return their input unchanged if they can not make sense of it, so I guess that is what happens here too.
Here is an explanation:
by default ng-repeat creates a dom node for each item and destroys that dom node when the item is removed; As it watches the ng-repeat object, it expects the object to change & implement the change & hence keep adding or deleting nodes. But using $index it reuses DOM nodes.
Here is the link where I picked up this information- http://www.reddit.com/r/angularjs/comments/2cvo16/speeding_up_angularjs_with_simple_optimizations/

Dynamic data-binding in AngularJS

I'm building an AngularJS app and I have ran into an issue. I have been playing with the framework for a while and I have yet to see documentation for something like this or any examples. I'm not sure which path to go down, Directive, Module, or something that I haven't heard of yet...
Problem:
Basically my app allows the user to add objects, we will say spans for this example, that have certain attribute's that are editable: height and an associated label. Rather than every span have its own dedicated input fields for height and label manipulation I would like to use one set of input fields that are able to control all iterations of our span object.
So my approx. working code is something like this:
<span ng-repeat="widget in chart.object">
<label>{{widget.label}}</label>
<span id="obj-js" class="obj" style="height:{{widget.amt}}px"></span>
</span>
<button ng-click="addObject()" class="add">ADD</button>
<input type="text" class="builder-input" ng-model="chart.object[0]['label']"/>
<input type="range" class="slider" ng-model="chart.object[0]['amt']"/>
The above code will let users add new objects, but the UI is obviously hardcoded to the first object in the array.
Desired Functionality:
When a user clicks on an object it updates the value of the input's ng-model to bind to the object clicked. So if "object_2" is clicked the input's ng-model updates to sync with the object_2's value. If the user clicks on "object_4" it updates the input's ng-model, you get the idea. Smart UI, essentially.
I've thought about writing a directive attribute called "sync" that could push the ng-model status to the bound UI. I've though about completely creating a new tag called <object> and construct these in the controller. And I've thought about using ng-click="someFn()" that updates the input fields. All of these are 'possibilities' that have their own pros and cons, but I thought before I either spin out on something or go down the wrong road I would ask the community.
Has anyone done this before (if so, examples)? If not, what would be the cleanest, AngularJS way to perform this? Cheers.
I don't think you need to use a custom directive specifically for this situation - although that may be helpful in your app once your controls are more involved.
Take as look at this possible solution, with a bit of formatting added:
http://jsfiddle.net/tLfYt/
I think the simplest way to solve this requires:
- Store 'selected' index in scope
- Bind ng-click to each repeated span, and use this to update the index.
From there, you can do exactly as you proposed: update the model on your inputs. This way of declarative thinking is something I love about Angular - your application can flow the way you would logically think about the problem.
In your controller:
$scope.selectedObjectIndex = null;
$scope.selectObject = function($index) {
$scope.selectedObjectIndex = $index;
}
In your ng-repeat:
<span ng-repeat="widget in chart.object" ng-click="selectObject($index)">
Your inputs:
<input type="text" class="builder-input" ng-model="chart.object[selectedObjectIndex]['label']"/>
<input type="range" class="slider" ng-model="chart.object[selectedObjectIndex]['amt']"/>

Resources