By Katie Miller
Reposted from the New Relic Blog
JavaScript frameworks can be a blessing and a curse–a fact that New Relic has become intimately familiar with as we continually work to improve the New Relic APM UI. We’ve chosen to use Angular.js as our frontend framework of choice and have loved the way that it makes our Web applications feel like they’re running native code. But we’ve discovered that without close attention to performance tuning, Angular can bring a browser to its knees.
We got some firsthand experience tackling huge JS and Angular performance hurdles while improving the application summary table functionality. The usual Angular performance suggestions helped somewhat, but we got even larger improvements by revising our application architecture and JavaScript practices. Here’s how we did it, along with some of the challenges that came up along the way.
Pushing server-side logic to client-side
There are a number of reasons to push application logic to the JS layer, the biggest being that letting JS do the work of massaging the data for presentation in the view layer boosts app server performance. In the case of our app, we converted our Rails endpoints to serve raw JSON and eliminated unnecessary view processing.
This improved our user experience so that customers can now seamlessly navigate among all the applications on their account, quickly finding the ones relevant to the task at hand. This goes hand-in-hand with loading and managing a large amount of data on the client side. With a small to moderately sized application, this isn’t a problem. But many of our customers monitor hundreds or thousands of applications that need to have their data updated in near-real-time.
We initially developed the page using our own account’s data, at which point everything seemed to be working smoothly. Then, close to the end of the project we loaded an account with lots of applications (around 3,000!). It was like that spinning gif was judging us!
Challenge 1: Data binding large amounts of data with Angular isn’t always optimal
Since this was our first big Angular application in APM we had some idea that using Angular’s
ng-repeat
with lots of data could cause a “dip” in performance. There has been a lot written about this phenomenon, so I won’t dwell on how we dealt with large numbers of watchers here. In fact the release of Angular 1.3 introduced one-time binding syntax — double colon {{::name}}
— that addresses this very problem.)
At the time Angular hadn’t released its one-time binding, so we used Bindonce. However, reducing the number of watchers didn’t solve all of our problems. We still were showing our users a lot of applications in the table, raising the bigger question of, “Is it really helpful to show that much data at once?” By committing to better solutions for navigating through applications, such as pagination and filters, we were able to confidently cut down on amount of data bindings on the screen at any given time.
Challenge 2: Garbage collection
We went back to a smaller data set and profiled the page in the browser’s dev tools. What we found was that our JS app was spending a lot of time in garbage collection (GC). This meant we had a memory usage problem.
In case you aren’t familiar with JS GC issues, a saw tooth shape like the one shown in the Used JS Heap line (blue) above indicates a high rate of short-lived objects being created that are then de-allocated by the browser’s memory. When your rate of GC is high you begin to experience “jank” or possibly even kill the page entirely. Understanding why we were experiencing such a large number of GC events requires understanding something about the structure of the JS app.
New Relic is in the business of keeping users in the know about the current state of their applications. For example, if the current view is ordered by application health and an application that was in good health suddenly experiences problems, we need to make sure that change becomes visible. In order to keep the data fresh for the JS frontend, we basically set a timer and periodically update the account’s applications in the background. When the data comes back from the server we need to process the response and display changes.
Our original approach was to completely reassign the variable with a new array with all of the fresh application data. This approach was dead simple to code in Angular but, obviously, if you’re worried about object references being allocated and de-allocated, it’s too heavy handed. We were simply throwing away the old array of applications. Queue the garbage collectors!
So we essentially came up with a homegrown in-memory cache of all the customer’s application data. We no longer build up and tear down arrays after each successful response from the server. In order to do this we keep a reference map using the application’s ID to tell us where it is in the cache. When we iterate over the new data we look up each application and update its properties. We return a reference to the cache to be used in controllers and/or directives. This created complexity since it’s extremely important to never reassign the cache variable instance once it’s been initialized.
It works kind of like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| angular.factory( "Application" , function ($http){ // We found in benchmarking that $resource was slower than bare $http this .get = function (){ return $http( "url/to/application/data" , {params: anythingNeeded}); } }); angular.service( "ApplicationCache" , function ($timeout, Application){ var cache = [], cacheMap = {}, timerDelay = 1 * 60 * 1000; // 1 minute // Return the cache array reference that you can bind to in a controller or directive. // This adds complexity since you should NEVER break the reference by reassigning // the cache variable. this .all = function (){ return cache; }; // Convenient lookup helper this .find = function (id){ var applicationIndex = cacheMap[id]; return cache[applicationIndex]; }; var fetch = function (){ Application.get() .success(updateCache) .error(someErrorHandlingFunctionOfYourChoosing) }; var updateCache = function (applicationData){ var application, availableLocation = cache.length; for ( var i = 0, len = applicationData.length; i < len; i++){ application = applicationData[i]; existingLocation = cacheMap[application.id]; if (existingLocation !== undefined) { updateCachedAttributes(application, existingLocation); } else { addNewApplication(application, availableLocation); cacheMap[application.id] = availableLocation++; } } }; var updateCachedAttributes = function (applicationData, location){ var application = cache[location]; for ( var attr in application){ application[attr] = applicationData[attr]; } }; var addNewApplication = function (applicationData, location){ var application = new Application(applicationData); cache[location] = application; cacheMap[application.id] = location; }; $timeout(fetch, timerDelay); }) |
Here’s how the memory usage looked in the profiler after implementing the cache and changing how we used arrays:
Challenge 3: Use array best practices.
Even after we solved the problem of making too many objects we didn’t need, the page still wasn’t as snappy as we wanted it. Every time a label was applied or a user resorted the table there was a noticeably slow response. After reading a few great blog posts andJSPerf tests we began benchmarking our array iterators and completely overhauled how we were doing things:
- We stopped using
myArray.push()
in favor ofmyArray[myArray.length] = somethingNew
- We started preallocating the array when we knew the length of the data
myArray = new Array(knownLengthOfData)
- We rewrote our iterators and sorting algorithms. We needed to know the intersection of arrays for filtering with our labels. We ran a JSPerf test on different iterators and algorithms and used the best one.
Conclusion
As front-end applications replace more and more heavy server-side computing, the chances of writing poorly performing JavaScript becomes greater. Especially when the tools, like Angular, make it really fast to create a slick UI. The larger the data gets the more important it becomes to pay attention to Garbage Collection in your devtools, be mindful about object creation, and use the fastest iterators you can find.
Katie is a Jill of all trades with a passion for making people’s lives better through design and craftmanship. Pursuing software was the next not so seemingly logical step after having worked in fields ranging from architecture to heavy construction. She currently is part of the APM team at New Relic writing Ruby and Javascript. When not building something, she’s exploring the forest in the PNW or on the hunt for good food and drink.
Source:- http://goo.gl/hohZBD
No comments:
Post a Comment