Angular 4 — The update
Roughly
six month after the release of Angular 2, the next big update for Angular is
now available: Angular 4, or rather Angular v4, because the team decided it
should be called “just Angular” from now on, without stating the version number
explicitly in the name. Originally, the “2” was used to differentiate between
AngularJS and the all new Angular Framework, which came with many reassessed
and refined concepts. The result, Angular, can be used in many different
programming Languages like Dart, TypeScript or ECMAScript 5 among others.
When
Angular 2 got published, the team also decided to use “semantic versioning” for
further versions, making the relationship between different versions in terms
of compatibility and new features visible at a glance.
Version
numbers are, therefore, set up according to the MAJOR.MINOR.PATCH schema. The
particular digit is changed in accordance with the following criteria:
1.
MAJOR
is increased when there is an incompatible change to the API. For Angular, this
could be the case if newer versions of a utilized library like zone.js or rxjs
gets adopted, that brings along a change to the API.
2.
MINOR
is increased when additional functionality is provided, but existing functions
and the API are kept stable. For Angular, this could be something like an
additional pipe getting shipped as part of the framework.
3.
PATCH
is increased when some troubleshooting is done without altering the API, thus
being backward compatible.
In
addition, there is an optional –QUALIFIER specifying a version in a more
precise way. For example 4.0.0-rc.1 or 4.0.0 beta would be used to mark a beta
version or release candidate.
A jump from 2 to 4. Up next: 8?
Some
might have wondered why the version following Angular 2 isn’t Angular 3 but
rather “Angular” v4. However, there is a good reason for this: version numbers
of all Angular components were to be unified, with the Angular Router having
already been developed up to version 3 and consequently had to be upgraded to
v4 now. Version 4 was therefore chosen for the entire Angular Framework in the
way of unification. Only Angular CLI, the command line tool for project
planning and abstraction of build and test execution, has been released as
Version 1.0.0.
SEE ALSO: Build and test Angular apps using Docker
This is new in Angular 4
The
reasons for this new major release are both new features as well as changes
incompatible with the previous version. Let’s take a look at the new features:
Router ParamMap
Starting
from version 4, it is possible to query a so-called ParamMap in the router,
meaning a request for the route- and queryparameter assigned to a route.
Up
until now, route parameters were stored in a simple key-value object structure,
therefore being accessible by using the standard JavaScript syntax
(parameterObjekt[‘parameter-name’] ).
1
2
3
4
5
6
7
8
9
10
11
|
class MyComponent
{ sessionId:
Observable<string>; constructor(private route:
ActivatedRoute) {} ngOnInit() { this.sessionId
= this.route .queryParams .map(params
=> params['session_id'] || 'None'); } } |
Now,
the parameters are also available as a map, so you can run them as simple
method calls (parameterMap.get(‘parameter-name’)).
1
2
3
4
5
6
7
8
9
10
11
|
class MyComponent
{ sessionId:
Observable<string>; constructor(private route:
ActivatedRoute) {} ngOnInit() { this.sessionId
= this.route .queryParamMap .map(paramMap
=> paramMap.get('session_id') || 'None'); } } |
The
utilization as a map also brings advantages in terms of type security. The old
key-value structure had an unsafe type (type Params = {[key : string]: any}),
whereby the parameter value could take all possible types. With the new map,
however, the parameter value is either a string or an array of strings,
depending on the method used. Type definition for individual values
(ParamMap.get() : string) and type definition for multiple values
(ParamMap.getAll() : string[]) apply.
Animations
Functions
necessary for animations up until now were provided as part of @angular/core
module, implying that these parts of the code were always included in
applications, even if they did not get used in apps without animations. To
avoid creating bundles with unnecessary large sizes, this function has been put
into its own package. (This isn’t just a new feature but also a change making
modifications to existing applications necessary if they contain animations.)
Animations
are to be provided in the module BrowserAnimationsModule from @angular/platform-browser/animations.
Necessary adjustments for Angular 4
Let’s
take a look at the necessary changes on existing projects to make them work
with Angular 4. To facilitate future migrations for developers, the Angular
team provides an interactive guide:
https://angular-update-guide.firebaseapp.com/. However, since this project is
still in its infancy, it should not be used just yet.
Dependencies update
Versions
of Angular-Modules can be updated via npm:
npm
install
@angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@next
–save
Changed lifecycle events
Classes
are not allowed to implement Lifecycle Events via inheritance – they need to
use Interfaces instead:
Foo
extends OnInit
needs
to be changed to:
Foo
implements OnInit
There
won’t be many developers affected by this change, anyway it is rather easy to
implement too.
Renaming template tags
Up to now, <template> was used as a Tag or
Attribute for Templates. This has been marked as deprecated. It has to be
replaced by <ng-template>
Access to Renderer
Renderer and RootRenderer could be used so far but that is not
possible anymore. Access is now provided by RendererFactory2.
Use of animations
Animations
have been part of Angular Core so far. Because of the separation in Angular 4,
imports need to be adjusted when animations are being used.
1
2
3
4
5
6
7
|
import {Component,
OnInit} from '@angular/core'; import { animate, state, style, transition, trigger } from '@angular/animations' |
In
the example, Angular Core still provides “Component” and “OnInit” while
“animate”, “state”, “style”, “transition” and “trigger” are now imported from
Angular Animations.
News in Angular CLI
Angular
CLI is the command line tool that helps developers to comfortably develop,
build and test their Angular project. In conjunction with Angular 4, Angular
CLI version 1.0.0 was released – it is now a core element of the Angular
Project.
A
feature especially worth mentioning is the added support for alternative
package managers besides npm: it now supports Facebook Yarn and cnpm, for
instance.
Usage
of Yarn can be activated this way:
ng
set –global packageManager=yarn
Angular CLI
Further
improvements include the standardized creation of Angular 4 projects by Angular
CLI starting from version 1.0.0 and improved error messages while AoT-compiling
of templates.
Prospect: Angular 5 and beyond
The
world keeps on spinning and Angular keeps pace with it: Further releases are
planned as well.
Ongoing
work includes a module for ServiceWorker to realize stuff like
push-notifications in Angular this way.
The
current schedule is set for a new major release every six month. Angular 5 will
therefore be ready around this October, Angular 6 should be released in March
2018. In between there will be some minor releases and bug fixes, if necessary
– just like it has been since the release of Angular 2. Furthermore, the
Angular team decided to do releases at specific time intervals:
§
Every week: Patch/bugfix releases
§
Every month: Minor release
§
Every six month: Major release, with
emphasis on easy migration from the prior version
It
is important to know that features marked as “deprecated” will be removed in
the next release, giving developers six months to migrate if they want to
update to the next major version immediately. That is quite recommendable,
though, because migration bottlenecks will sooner or later cause problems due
to being stuck with an old version. Also, strongly increased effort when
migrating over many different versions would be a result.
From
feedback they gained from the community, the Angular team learned that the
development pace is considered to be “very quick” especially in enterprise
environments. To meet the special needs of large organizations, offering a
Long-Term-Support (LTS) for Angular is currently being considered, profiting
from bugfixes over an extended period.
This
might put some project managers’ minds at ease. However, it might still get
interesting with regard to the question how upgrades from one LTS version to
the next will be facilitated. Speaking from experience, it is rather beneficial
to try and keep up with progress and profit from current developments.
Furthermore, the exact length of LTS-Support periods for Angular and AngularJS
is still being discussed.
Google
is using a tool for automated code-migration in-house. There are some
discussions going on about whether to release this tool for public use or build
something similar for automated migrations and publish that.
This development shows that Angular –
partially because they are always ready to listen to the community – grew into
a mature and professional framework both suitable for use in small, agile teams
as in large organizations
A single-page application architecture
promised to unlock the backend’s blazing performance, so we began searching for
an appropriate framework, and identified AngularJS as a promising candidate.
Following the “fail fast” principle, we began with our toughest challenge, the
log view.
This is a real acid test for an application
framework. The user can click on any word to search for related log messages,
so there may be thousands of clickable elements on the page; yet we want
instantaneous response for paging through the log. We were already prefetching
the next page of log data, so the user interface update was the bottleneck. A
straightforward AngularJS implementation of the log view took 1.2
seconds to advance to the next page, but with some careful
optimizations we were able to reduce that to 35 milliseconds. These
optimizations proved to be useful in other parts of the application, and fit in
well with the AngularJS philosophy, though we had to break few rules to
implement them. In this article, we’ll discuss the techniques we used.
A log of Github updates, from our live demo.
An AngularJS log viewer
At heart, the Log View is simply a list of log
messages. Each word is clickable, and so must be placed in its own DOM element.
A simple implementation in AngularJS might look like this:
<span
class=’logLine’ ng-repeat=’line in logLinesToShow’><span
class=’logToken’ ng-repeat=’token in line’>{{token | formatToken}}
</span><br></span>
|
One page can easily have several thousand
tokens. In our early tests, we found that advancing to the next log page could
take several agonizing seconds of JavaScript execution. Worse, unrelated
actions (such as clicking on a navigation dropdown) now had noticeable lag. The
conventional wisdom for AngularJS says that you should keep the number of
data-bound elements below 200. With an element per word, we were far above that
level.
Analysis
Using Chrome’s Javascript profiler, we quickly
identified two sources of lag. First, each update spent a lot of time creating
and destroying DOM elements. If the new view has a different number of lines,
or any line has a different number of words, Angular’s ng-repeat directive will
create or destroy DOM elements accordingly. This turned out to be quite
expensive.
Second, each word had its own change watcher,
which AngularJS would invoke on every mouse click. This was causing the lag on
unrelated actions like the navigation dropdown.
Optimization #1: Cache DOM elements
We created a variant of the ng-repeat
directive. In our version, when the number of data elements is reduced, the
excess DOM elements are hidden but not destroyed. If the number of elements later
increases, we re-use these cached elements before creating new ones.
Optimization #2: Aggregate watchers
All that time spent invoking change watchers
was mostly wasted. In our application, the data associated with a particular
word can never change unless the overall array of log messages changes. To
address this, we created a directive that “hides” the change watchers of its
children, allowing them to be invoked only when the value of a specified parent
expression changes. With this change, we avoided invoking thousands of per-word
change watchers on every mouse click or other minor event. (To accomplish
this, we had to slightly break the AngularJS abstraction layer. We’ll say a bit
more about this in the conclusion.)
Optimization #3: Defer element creation
As noted, we create a separate DOM element for
each word in the log. We could get the same visual appearance with a single DOM
element per line; the extra elements are needed only for mouse interactivity.
Therefore, we decided to defer the creation of per-word elements for a
particular line until the mouse moves over that line.
To implement this, we create two versions of
each line. One is a simple text element, showing the complete log message. The
other is a placeholder which will eventually be populated with an element per
word. The placeholder is initially hidden. When the mouse moves over that line,
the placeholder is shown and the simple version is hidden. Showing the
placeholder causes it to be populated with word elements, as described next.
Optimization #4: Bypass watchers for hidden elements
We created one more directive, which prevents
watchers from being executed for an element (or its children) when the element
is hidden. This supports Optimization #1, eliminating any overhead for extra
DOM nodes which have been hidden because we currently have more DOM nodes than
data elements. It also supports Optimization #3, making it easy to defer the
creation of per-word nodes until the tokenized version of the line is shown.
Here is what the code looks like with all
these optimizations applied. Our custom directives are shown in bold.
<span
class=’logLine’ sly-repeat=’line in logLinesToShow’ sly-evaluate-only-when=’logLines’><div
ng-mouseenter=”mouseHasEntered = true”><span
ng-show=’!mouseHasEntered’>{{logLine | formatLine }} </span><div
ng-show=’mouseHasEntered’ sly-prevent-evaluation-when-hidden><span
class=’logToken’ sly-repeat=’tokens in line’>{{token |
formatToken }}</span></div></div>
<br>
</span>
|
sly-repeat is our variant of ng-repeat, which
hides extra DOM elements rather than destroying them. sly-evaluate-only-when
prevents inner change watchers from executing unless the “logLines” variable
changes, indicating that the user has advanced to a new section of the log. And
sly-prevent-evaluation-when-hidden prevents the inner repeat clause from
executing until the mouse moves over this line and the div is displayed.
This shows the power of AngularJS for
encapsulation and separation of concerns. We’ve applied some fairly
sophisticated optimizations without much impact on the structure of the
template. (This isn’t the exact code we’re using in production, but it captures
all of the important elements.)
Results
To evaluate performance, we added code to measure
the time from a mouse click, until Angular’s $digest cycle finishes (meaning
that we are finished updating the DOM). The elapsed time is displayed in a
widget on the side of the page. We measured performance of the “Next Page”
button while viewing a Tomcat access log, using Chrome on a recent MacBook Pro.
Here are the results (each number is the average of 10 trials):
Data
already cached
|
Data
fetched from server
|
|
Simple
AngularJS
|
1190
ms
|
1300
ms
|
With
Optimizations
|
35
ms
|
201
ms
|
These figures do not include the time the
browser spends in DOM layout and repaint (after JavaScript execution has
finished), which is around 30 milliseconds in each implementation. Even so, the
difference is dramatic; Next Page time dropped from a “stately” 1.2 seconds, to
an imperceptible 35 ms (65 ms with rendering).
The “data fetched from server” figures include
time for an AJAX call to our backend to fetch the log data. This is unusual for
the Next Page button, because we prefetch the next page of logs, but may be
applicable for other UI interactions. But even here, the optimized version
updates almost instantly.
Angular
Performance issues and best practices
ng-if Is Better Than ng-show
The ng-show directive toggles the CSS display property
on a particular element, while the ng-if directive actually removes
the element from DOM and re-creates it if needed. Further, the ng-switch directive
is an alternative to the ng-if with the same performance benefits.
Don't Use ng-repeat
This is the biggest win for any app if it is not using the ng-repeat directive, so
it is advisable to avoid ng-repeat and build the HTML using
JavaScript. For vocalizing applications, the use of ng-if results in
the addition of unnecessary watchers. The use of the ng-bind-html directive
is a better solution to get rid of this problem.
Use $watchCollection (Includes Third
Parameter)
Using $watch with two parameters is good — but
using $watch with three parameters, i.e. $watch(‘value’,function(){},true),
makes Angular perform deep checking (to check every property of the object).
But this could be expensive. Hence, to resolve such a performance issue,
Angular provides $watchCollection(‘value’, function(){}) which acts
almost alike $watch with 3rd parameter except it only checks first layer of
object’s properties at low cost.
Use
console.time for Debugging Issues
If your application is struggling with debugging issues and
effecting Angular performance, then use console.time, which is a great
API.
Debounce
ng-model
You can debounce the input with ng-model. For example, to
de-bounce the search input like GOOGLE, you have to use ng-model-options=”{debounce:250}”.
This makes the digest cycle get triggered no more than once per 250ms due to
the changes in this input model.
ngIf: Can also be used with “else”
It’s
quite a frequent thing to use “conditional rendering” in templates to display
information depending on some condition. This is done by using *ngIf. If a
condition isn’t met, the corresponding element and all child elements are not
added to the DOM-tree. Many times there was also a need for the opposing case,
making it necessary to formulate the same condition just the other way around
and add another *ngIf.
This
has some nasty implications for readability and maintainability of the code –
after all, you have to work on multiple lines of code when implementing some
changes.
In
Angular 4, this use case can be solved with a newly added else. Maybe
unexpected for some, Angular uses a separately referenced template
fragment, which in the else-case will be used in place of the element marked
with *ngIf.
This
example illustrates the difference in code between old and new syntax. Use case
is to show the user name of a user which is logged in, or a login button for a
user not logged in.
Old syntax:
Old syntax:
1
2
3
4
5
|
Logged in as {{auth.username}} Please log in:
<button>Login</button> |
New
syntax:
1
2
3
4
5
6
7
8
9
|
<ng-template #needsLogin> Please log in:
<button>Login</button> </ng-template> Eingeloggt als
{{auth.username}} |
The
functionality of *ngIf in the field of reactive programming got improved when
interacting with the async-pipe. It was already possible to “subscribe” (and
“unsubscribe”) to asynchronous objects like observables and such from within a
template by using the async-Pipe. The new *ngIf-Syntax now makes it possible to
also add a local template variable to the result of the if-clause. In the
example below the observable, placed inside the variable auth, is resolved by
the async-pipe. The result can be used within the template by means of the user
variable.
1
2
3
4
5
6
7
8
9
|
<ng-template #loading> Loading User Data... </ng-template> {{user.username }} |
Note that the async-Pipe is not a necessary precondition for using
the new syntax, but rather used for illustrational purposes in this example.
Using any other Pipe or no Pipe at all will also work.
Dynamic Components with
NgComponentOutlet
The
new *ngComponentOutlet-Directive makes it possible to build dynamic components
in a declarative way. Up until now, it has been quite a lot of work to build
and produce components dynamically at runtime. It’s not enough to just write
some HTML code! Angular needs to be notified about the component and add it to
the lifecycle, take care of the data binding and change detection. The old way
of using ComponentFactory therefore involved relatively much programming work.
The
following example shows how that looks like with *ngComponentOutlet:
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
|
@Component({ selector:
‘app-first-time-visitor’, template: ‘ <h1>Welcome to our
Page!</h1> ’, }) export class FirstTimeVisitorComponent
{} @Component({ selector:
‘app-frequent-visitor’, template: ‘ <h1>Welcome
Back!</h1> ’, }) export class FrequentVisitorComponent
{} @Component({ selector:
‘app-root', template: ` <h1>Hello Angular
v4!</h1> <ng-container
*ngComponentOutlet="welcome"></ng-container> `, }) export class App
implements OnInit{ welcome =
FirstTimeVisitorComponent; ngOnInit() { if(!this.user.isfirstVisit){ this.alert
= FrequentVisitorComponent; } } } |
In this example, either FirstTimeVisitorComponent or FrequentVisitorComponent will be displayed as a
greeting to visitors, depending on whether they are accessing the page for the
first time or have visited it before. The check for prior visits is conducted
in the OnInit-Lifecycle-Hook;
based on the result, different components are handed over to the template for
display.
TypeScript 2.1/2.2
Type
security of Angular applications and the speed of ngc-Compiler have been
improved – and for that, we need to thank the official support for the most
recent TypeScript versions.
StrictNullChecks
Sadly,
some parts of the improved Type Checks could not be included in Angular 4 [for
now] because some incompatibilities were found in the RC phase. There are plans
to include this feature in v4.1, though.
Angular Universal
With
Angular Universal, it’s possible to render Angular applications outside of the
browser, for instance directly on the web server. With that, JavaScript is no
longer necessary for initially rendering the page content, so websites can be
optimized better for search engines. Another use case is the utilization of
WebWorker Threads to render content outside the GUI Thread. This rendered
content can simply be added to the DOM Tree for display later.
For
Java developers, the tickets for Spring Framework support [or rather Spring
Boot] might be of interest because it adds the option for server side rendering
via Nashorn or J2V8:
Unfortunately,
no results that could be used in production are available yet. Angular
Universal can already be used in different programming languages through
node-Runtime.
New
Pipe: Title Case
The
new titlecase-Pipe will change the first letter of each word to upper case,
while all other letters remain in lower case.
Forms
get assigned “novalidate” automatically
Up
until now, forms had to be marked with “novalidate” if HTML5-validation by the
browsers was supposed to be suppressed and full control over the validation was
to be given to the Angular application.
Therefore,
developers marked all their forms as “novalidate” on a regular basis. Angular 4
will set this attribute automatically.
Source
maps also for templates
When
it comes to debugging and finding errors, source maps are of vital importance.
They show the relation from source code to result and thereby help to narrow
down the error. The new Template Compiler will create such source maps also for
templates. Therefore there is more contextual information available both when
working with the browser debugger and also in Crash-Reports and Log-Messages.
Angular
Performance: ng-show vs ng-if
You’ve probably come across
ng-if
and ng-show
and wondered why
they both exist and what’s the difference between them. After all they usually
have the same behavior as far as the user is concerned.
The devil is in the details and the differences between these
directives can allow your to boost your application’s performance easily.
The Differences
Both
ng-show
and ng-if
receive a condition and
hide from view the directive’s element in case the condition evaluates to false
. The mechanics they use to
hide the view, though, are different.ng-show
(and
its sibling ng-hide
)
toggle the appearance of the element by adding the CSS display: none
style.ng-if
, on
the other hand, actually removes the element from the DOM when the
condition is false
and
only adds the element back once the condition turns true
.
Since
ng-show
leaves
the elements alive in the DOM, it means that all of their watch expressions and
performance cost are still there even though the user doesn’t see the view at
all. In cases where you have a few big views that are toggled with ng-show
you might be
noticing that things are a bit laggy (like clicking on buttons or typing inside
input fields).
If you just replace that
ng-show
with an ng-if
you might witness considerable improvements in
the responsiveness of your app because those extra watches are no longer
happening.
That’s it: replace
ng-show
and ng-hide
with ng-if
!
Caveats and Pitfalls of ng-if
- Measure, measure, measure: As with every optimization, you should not apply
this without measuring and validating that it does speed up your app. It
can, potentially, actually make things slower, as I explain below.
- Your controllers will be rerun: The
controllers and directives in the element that’s being removed and added
again will actually be recreated and so their initialization logic will run again. This is in contrast to
ng-show
where things are always there in memory, and so are only initialized once. You need to make sure your code handles being rerun properly. - Sometimes initialization is more expensive than keeping
things around: That’s to say that in some cases the cost of
removing the element from the DOM and then recreating it to add again can
be a heavy operation all by itself. In those cases you might feel that it
takes too long for the element to reappear. In those cases this trick
might actually degrade your app’s performance, so remember to check and measure!
That’s it. Enjoy making your app a bit snappier. If you care
about performance in Angular you really should read about speeding ng-repeat with the track by syntax.
Flat
ES-Modules (Flat ESM / FESM)
Instead
of many small files that belong to a module, “flat” versions of modules will
also be delivered. In this context, flat means that only one file is shipped
per module, containing everything belonging to that module. These flat modules
are supposed to help with better performance on compiling and on execution in
the browser. Furthermore, this shall improve Tree Shaking and Build, so
applications will become smaller.
New
View Engine for phenomenal speed
From
templates and the @Component Elements, Angular generates the View-Layer. In
doing so, there are the Just-In-Time Compiler (JIT) and the Ahead-of-Time
Compiler (AoT). The JIT is primarily used in development, essentially being an
Interpreter. The AoT Compiler generates executable (JavaScript-)Code with
embedded HTML fragments from templates and components. This step, often called
Codegen, produces a lot of code: Event-Handling, Change-Detection, Data-Binding
and handling of dynamic component behaviour will be woven into the result.
Applications
generated this way will be very fast but still got a lot of code that slows
down the initial launch-time. All that code has to be downloaded first anyway!
The
Angular-Team takes a very open approach to this, using other frameworks, for
instance the Inferno.js framework, as a reference for performance and design
decisions. The different requirements, goals and approaches for the template
layer in Angular are also discussed in the extensive design documentation for
the View Engine.
As
a result, AOT-compiled templates in Angular 4 are substantially smaller, both
reducing loading times and at the same time increasing overall page speed
through the reduced code size. On average, one can expect a reduction of 60
percent, making for some significant improvement especially on mobile
applications.
Additionally,
the amount of work to be done by garbage collectors in the browser was reduced,
also resulting in some noticeable improvements on performance. The generated
code is not just well suited to be packed with classic tools like gzip. It can
also be minified with Google Closure Compiler, reducing the size even further.
In this article by Chandermani, the author of AngularJS by Example, we focus our discussion on the
performance aspect of AngularJS. For most scenarios, we can all agree that
AngularJS is insanely fast. For standard size views, we rarely see any performance
bottlenecks. But many views start small and then grow over time. And sometimes
the requirement dictates we build large pages/views with a sizable amount of
HTML and data. In such a case, there are things that we need to keep in mind to
provide an optimal user experience.
Take any framework and the
performance discussion on the framework always requires one to understand the
internal working of the framework. When it comes to Angular, we need to
understand how Angular detects model changes. What are watches? What is a
digest cycle? What roles do scope objects play? Without a conceptual
understanding of these subjects, any performance guidance is merely a checklist
that we follow without understanding the why part.
Let’s look at some pointers
before we begin our discussion on performance of AngularJS:
·
The live binding between the view elements and model data is set
up using watches.
When a model changes, one or many watches linked to the model are triggered.
Angular’s view binding infrastructure uses these watches to synchronize the
view with the updated model value.
·
Model change detection only happens when a digest
cycle is triggered.
·
Angular does
not track model changes in real time; instead, on every digest
cycle, it runs through every watch to compare the previous and new values of
the model to detect changes.
·
A digest cycle is triggered when $scope.$apply is invoked. A
number of directives and services internally invoke $scope.$apply:
o Directives
such as ng-click, ng-mouse* do
it on user action
o Services
such as $http and $resource do
it when a response is received from server
o $timeout or $interval call $scope.$apply when
they lapse
·
A digest cycle tracks the old value of the watched expression
and compares it with the new value to detect if the model has changed. Simply
put, the digest cycle is a workflow used to detect model changes.
·
A digest cycle runs multiple times till the model data is stable
and no watch is triggered.
Once you have a clear
understanding of the digest cycle, watches, and scopes, we can look at some
performance guidelines that can help us manage views as they start to grow.
(For more resources related
to this topic, see here.)
Performance guidelines
When building any Angular app,
any performance optimization boils down to:
·
Minimizing the number of binding expressions and hence watches
·
Making sure that binding expression evaluation is quick
·
Optimizing the number of digest cycles that take place
The next few sections provide
some useful pointers in this direction.
Remember, a lot of these
optimization may only be necessary if the view is large.
Keeping the page/view small
The sanest advice is to keep
the amount of content available on a page small. The user cannot
interact/process too much data on the page, so remember that screen real estate
is at a premium and only keep necessary details on a page.
The lesser the content, the
lesser the number of binding expressions; hence, fewer watches and less
processing are required during the digest cycle. Remember, each watch adds to
the overall execution time of the digest cycle. The time required for a single
watch can be insignificant but, after combining hundreds and maybe thousands of
them, they start to matter.
Angular’s data binding
infrastructure is insanely fast and relies on a rudimentary dirty check that
compares the old and the new values. Check out the stack
overflow (SO) post (http://stackoverflow.com/questions/9682092/databinding-in-angularjs),
where Misko
Hevery (creator of Angular) talks about how data binding works in
Angular.
Data binding also adds to the
memory footprint of the application. Each watch has to track the current and
previous value of a data-binding expression to compare and verify if data has
changed.
Keeping a page/view small may
not always be possible, and the view may grow. In such a case, we need to make
sure that the number of bindings does not grow exponentially (linear growth is
OK) with the page size. The next two tips can help minimize the number of
bindings in the page and should be seriously considered for large views.
Optimizing watches for read-once data
In any Angular view, there is
always content that, once bound, does not change. Any read-only data on the
view can fall into this category. This implies that once the data is bound to
the view, we no longer need watches to track model changes, as we don’t expect
the model to update.
Is it possible to remove the
watch after one-time binding? Angular itself does not have something inbuilt,
but a community project bindonce(https://github.com/Pasvaz/bindonce)
is there to fill this gap.
Angular 1.3 has added support for bind and forget in
the native framework. Using the syntax {{::title}}, we can
achieve one-time binding. If you are on Angular 1.3, use it!
Hiding (ng-show) versus conditional rendering (ng-if/ng-switch) content
You have learned two ways to
conditionally render content in Angular. The ng-show/ng-hidedirective shows/hides the DOM element based
on the expression provided and ng-if/ng-switch creates and destroys the DOM based on
an expression.
For some scenarios, ng-if can be really beneficial as it can reduce the number of
binding expressions/watches for the DOM content not rendered. Consider the
following example:
<div ng-if='user.isAdmin'>
<div ng-include="'admin-panel.html'"></div>
</div>
The snippet renders an admin
panel if the user is an admin. With ng-if, if
the user is not an admin, the ng-include directive
template is neither requested nor rendered saving us of all the bindings and
watches that are part of the admin-panel.html view.
From the preceding discussion,
it may seem that we should get rid of all ng-show/ng-hidedirectives and use ng-if. Well, not really! It again depends; for small size pages, ng-show/ng-hide works
just fine. Also, remember that there is a cost to creating and destroying the
DOM. If the expression to show/hide flips too often, this will mean too many
DOMs create-and-destroy cycles, which are detrimental to the overall
performance of the app.
Expressions being watched should not be slow
Since watches are evaluated too
often, the expression being watched should return results fast.
The first way we can make sure
of this is by using properties instead of functions to bind expressions. These
expressions are as follows:
{{user.name}}
ng-show='user.Authorized'
The preceding code is always
better than this:
{{getUserName()}}
ng-show = 'isUserAuthorized(user)'
Try to minimize function
expressions in bindings. If a function expression is required, make sure that
the function returns a result quickly. Make sure a function being watched does not:
·
Make any remote calls
·
Use $timeout/$interval
·
Perform sorting/filtering
·
Perform DOM manipulation (this can happen inside directive
implementation)
·
Or perform any other time-consuming operation
Be sure to avoid such
operations inside a bound function.
To reiterate, Angular will
evaluate a watched expression multiple times during every digest cycle just to
know if the return value (a model) has changed and the view needs to be
synchronized.
Minimizing the deep model watch
When using $scope.$watch to watch for model
changes in controllers, be careful while setting the third $watch function parameter to true. The general syntax of watch looks like this:
$watch(watchExpression, listener, [objectEquality]);
In the standard scenario,
Angular does an object comparison based on the reference only. But if objectEquality is true, Angular does a deep comparison between the last value and new
value of the watched expression. This can have an adverse memory and
performance impact if the object is large.
Handling large datasets with ng-repeat
The ng-repeat directive undoubtedly is the most useful
directive Angular has. But it can cause the most performance-related headaches.
The reason is not because of the directive design, but because it is the only
directive that allows us to generate HTML on the fly. There is always the
possibility of generating enormous HTML just by binding ng-repeat to a big model list. Some tips that
can help us when working with ng-repeat are:
·
Page data and
use limitTo: Implement a server-side paging mechanism when a number of
items returned are large.
Also use the limitTo filter to limit the number of items
rendered. Its syntax is as follows:
<tr ng-repeat="user in users |limitTo:pageSize">…</tr>
Look at modules such as ngInfiniteScroll(http://binarymuse.github.io/ngInfiniteScroll/)
that provide an alternate mechanism to render large lists.
·
Use the track
by expression: The ng-repeat directive for
performance tries to make sure it does not unnecessarily create or delete HTML
nodes when items are added, updated, deleted, or moved in the list. To achieve
this, it adds a $$hashKey property to every model item allowing it
to associate the DOM node with the model item.
We can override this behavior
and provide our own item key using the track by expression such as:
<tr ng-repeat="user in users track by user.id">…</tr>
This
allows us to use our own mechanism to identify an item. Using your own track by
expression has a distinct advantage over the default hash key approach. Consider an example where
you make an initial AJAX call to get users:
$scope.getUsers().then(function(users){ $scope.users = users;})
Later
again, refresh the data from the server and call something similar again:
$scope.users = users;
With user.id as a key, Angular is able to determine
what elements were added/deleted and moved; it can also determine
created/deleted DOM nodes for such elements. Remaining elements are not touched
by ng-repeat (internal
bindings are still evaluated). This saves a lot of CPU cycles for the browser as
fewer DOM elements are created and destroyed.
·
Do not bind
ng-repeat to a function expression: Using a function’s return
value for ng-repeat can also be problematic, depending upon
how the function is implemented.
Consider a repeat with this:
<tr ng-repeat="user in getUsers()">…</tr>
And
consider the controller getUsers function
with this:
$scope.getUser = function() {
var orderBy = $filter('orderBy');
return orderBy($scope.users, predicate);
}
Angular
is going to evaluate this expression and hence call this function every time
the digest cycle takes place. A lot of CPU cycles were wasted sorting user data
again and again. It is better to use scope properties and presort the data
before binding.
·
Minimize
filters in views, use filter elements in the controller:
Filters defined on ng-repeat are also evaluated
every time the digest cycle takes place. For large lists, if the same filtering
can be implemented in the controller, we can avoid constant filter evaluation.
This holds true for any filter function that is used with arrays including filterand orderBy.
Avoiding mouse-movement tracking events
The ng-mousemove, ng-mouseenter, ng-mouseleave, and ng-mouseover directives can just kill
performance. If an expression is attached to any of these event directives,
Angular triggers a digest cycle every time the corresponding event occurs and
for events like mouse move, this can be a lot.
We have already seen this
behavior when working with 7 Minute
Workout, when we tried to show a pause overlay on the exercise image
when the mouse hovers over it.
Avoid them at all cost. If we
just want to trigger some style changes on mouse events, CSS is a better tool.
Avoiding calling $scope.$apply
Angular is smart enough to call $scope.$apply at appropriate times
without us explicitly calling it. This can be confirmed from the fact that the
only place we have seen and used $scope.$apply is
within directives.
The ng-click and updateOnBlur directives
use $scope.$apply to
transition from a DOM event handler execution to an Angular execution context.
Even when wrapping the jQuery plugin, we may require to do a similar transition
for an event raised by the JQuery plugin.
Other than this, there is no
reason to use $scope.$apply.
Remember, every invocation of $apply results
in the execution of a complete digest cycle.
The $timeout and $interval services
take a Boolean argument invokeApply. If
set to false, the
lapsed $timeout/$interval services does not call $scope.$apply or trigger a digest
cycle. Therefore, if you are going to perform background operations that do not
require $scopeand the
view to be updated, set the last argument to false.
Always use Angular wrappers over
standard JavaScript objects/functions such as $timeout and $interval to avoid manually calling $scope.$apply. These wrapper functions
internally call $scope.$apply.
Also, understand the difference
between $scope.$apply and $scope.$digest. $scope.$applytriggers $rootScope.$digest that evaluates all
application watches whereas, $scope.$digestonly
performs dirty checks on the current scope and its children. If we are sure
that the model changes are not going to affect anything other than the child
scopes, we can use $scope.$digest instead
of $scope.$apply.
Lazy-loading, minification, and creating multiple SPAs
I hope you are not assuming
that the apps that we have built will continue to use the numerous small script
files that we have created to separate modules and module artefacts (controllers, directives, filters, and services). Any
modern build system has the capability to concatenate and minify these files
and replace the original file reference with a unified and minified version.
Therefore, like any JavaScript library, use minified script files for
production.
The problem with the Angular
bootstrapping process is that it expects all Angular application scripts to be
loaded before the application can bootstrap. We cannot load modules,
controllers, or in fact, any of the other Angular constructs on demand. This
means we need to provide every artefact required by our app, upfront.
For small applications, this is
not a problem as the content is concatenated and minified; also, the Angular
application code itself is far more compact as compared to the traditional
JavaScript of jQuery-based apps. But, as the size of the application starts to
grow, it may start to hurt when we need to load everything upfront.
There are at least two possible
solutions to this problem; the first one is about breaking our application into
multiple SPAs.
Breaking
applications into multiple SPAs
This advice may seem
counterintuitive as the whole point of SPAs is to get rid of full page loads.
By creating multiple SPAs, we break the app into multiple small SPAs, each
supporting parts of the overall app functionality.
When we say app, it implies a
combination of the main (such as index.html) page
with ng-app and
all the scripts/libraries and partial views that the app loads over time.
For example, we can break the Personal Trainer application into a Workout Builder app and a Workout Runner app. Both have their own
start up page and scripts. Common scripts such as the Angular framework scripts
and any third-party libraries can be referenced in both the applications. On
similar lines, common controllers, directives, services, and filters too can be
referenced in both the apps.
The way we have designed Personal Trainer makes it easy to achieve
our objective. The segregation into what belongs where has already been done.
The advantage of breaking an
app into multiple SPAs is that only relevant scripts related to the app are
loaded. For a small app, this may be an overkill but for large apps, it can
improve the app performance.
The challenge with this
approach is to identify what parts of an application can be created as
independent SPAs; it totally depends upon the usage pattern of the application.
For example, assume an
application has an admin module and an end consumer/user module. Creating two
SPAs, one for admin and the other for the end customer, is a great way to keep
user-specific features and admin-specific features separate. A standard user
may never transition to the admin section/area, whereas an admin user can still
work on both areas; but transitioning from the admin area to a user-specific
area will require a full page refresh.
If breaking the application into
multiple SPAs is not possible, the other option is to perform the lazy loading
of a module.
Lazy-loading
modules
Lazy-loading modules or loading
module on demand is a viable option for large Angular apps. But unfortunately,
Angular itself does not have any in-built support for lazy-loading modules.
Furthermore, the additional
complexity of lazy loading may be unwarranted as Angular produces far less code
as compared to other JavaScript framework implementations. Also once we gzip and minify the
code, the amount of code that is transferred over the wire is minimal.
If we still want to try our
hands on lazy loading, there are two libraries that can help:
·
ocLazyLoad (https://github.com/ocombe/ocLazyLoad):
This is a library that uses script.js to load modules on
the fly
·
angularAMD (http://marcoslin.github.io/angularAMD):
This is a library that usesrequire.js to lazy load
modules
With lazy loading in place, we
can delay the loading of a controller, directive, filter, or service script,
until the page that requires them is loaded.
The overall concept of lazy
loading seems to be great but I’m still not sold on this idea. Before we adopt
a lazy-load solution, there are things that we need to evaluate:
·
Loading
multiple script files lazily: When scripts are concatenated
and minified, we load the complete app at once. Contrast it to lazy loading
where we do not concatenate but load them on demand. What we gain in terms of
lazy-load module flexibility we lose in terms of performance. We now have to
make a number of network requests to load individual files.
Given these facts, the ideal
approach is to combine lazy loading with concatenation and minification. In
this approach, we identify those feature modules that can be concatenated and
minified together and served on demand using lazy loading. For example, Personal Trainer scripts can be divided
into three categories:
o The common app modules: This consists of any
script that has common code used across the app and can be combined together
and loaded upfront
o The Workout Runner module(s):
Scripts that support workout execution can be concatenated and minified
together but are loaded only when the Workout Runnerpages are loaded.
o The Workout Builder module(s): On
similar lines to the preceding categories, scripts that support workout
building can be combined together and served only when the Workout
Builder pages are loaded.
As we
can see, there is a decent amount of effort required to refactor the app in a
manner that makes module segregation, concatenation, and lazy loading possible.
·
The effect on
unit and integration testing: We also need to evaluate the
effect of lazy-loading modules in unit and integration testing. The way we test
is also affected with lazy loading in place. This implies that, if lazy loading
is added as an afterthought, the test setup may require tweaking to make sure
existing tests still run.
Given these facts, we should
evaluate our options and check whether we really need lazy loading or we can
manage by breaking a monolithic SPA into multiple smaller SPAs.
Caching remote data wherever appropriate
Caching data is the one of the
oldest tricks to improve any webpage/application performance. Analyze your GET requests and determine what data can be cached. Once such
data is identified, it can be cached from a number of locations.
Data cached outside the app can
be cached in:
·
Servers: The
server can cache repeated GET requests to resources that
do not change very often. This whole process is transparent to the client and
the implementation depends on the server stack used.
·
Browsers: In
this case, the browser caches the response. Browser caching depends upon the
server sending HTTP cache headers such as ETag and cache-control to
guide the browser about how long a particular resource can be cached. Browsers
can honor these cache headers and cache data appropriately for future use.
If server and browser caching
is not available or if we also want to incorporate any amount of caching in the
client app, we do have some choices:
·
Cache data in
memory: A simple Angular service can cache the HTTP response in the
memory. Since Angular is SPA, the data is not lost unless the page refreshes.
This is how a service function looks when it caches data:
var workouts;
service.getWorkouts = function () {
if (workouts) return $q.resolve(workouts);
return $http.get("/workouts").then(function (response)
{
workouts = response.data;
return workouts;
});
};
The
implementation caches a list of workouts into the workouts variable for future use. The first
request makes a HTTP call to retrieve data, but subsequent requests just return
the cached data as promised. The usage of $q.resolve makes
sure that the function always returns a promise.
·
Angular $http
cache: Angular’s $http service comes with a
configuration option cache. When set to true, $http caches
the response of the particular GET request into a local cache
(again an in-memory cache). Here is how we cache a GET request:
$http.get(url, { cache: true});
Angular
caches this cache for the lifetime of the app, and clearing it is not easy. We
need to get hold of the cache dedicated to caching HTTP responses and clear the
cache key manually.
The caching strategy of an
application is never complete without a cache invalidation strategy. With
cache, there is always a possibility that caches are out of sync with respect
to the actual data store.
We cannot affect the
server-side caching behavior from the client; consequently, let’s focus on how
to perform cache invalidation (clearing) for the two client-side caching
mechanisms described earlier.
If we use the first approach to
cache data, we are responsible for clearing cache ourselves.
In the case of the second
approach, the default $http service
does not support clearing cache. We either need to get hold of the underlying $http cache store and clear the cache key manually (as shown
here) or implement our own cache that manages cache data and invalidates cache
based on some criteria:
var cache = $cacheFactory.get('$http');
cache.remove("http://myserver/workouts"); //full url
Using Batarang to measure performance
Batarang (a
Chrome extension), as we have already seen, is an extremely handy tool for
Angular applications. Using Batarang to visualize app usage is like looking at
an X-Ray of the app. It allows us to:
·
View the scope data, scope hierarchy, and how the scopes are linked
to HTML elements
·
Evaluate the performance of the application
·
Check the application dependency graph, helping us understand
how components are linked to each other, and with other framework components.
If we enable Batarang and then play around with our
application, Batarang captures performance metrics for all watched expressions
in the app. This data is nicely presented as a graph available on the Performance tab
inside Batarang:
That is pretty sweet!
When building an app, use
Batarang to gauge the most expensive watches and take corrective measures, if
required.
Play around with Batarang and
see what other features it has. This is a very handy tool for Angular
applications.
This brings us to the end of
the performance guidelines that we wanted to share in this article. Some of
these guidelines are preventive measures that we should take to make sure we
get optimal app performance whereas others are there to help when the
performance is not up to the mark.
Summary
In this article, we looked at
the ever-so-important topic of performance, where you learned ways to optimize
an Angular app performance.
AngularJS is one of the
most popular Javascript frameworks available today. One of AngularJS's goals is
to simplify the development process which makes it great for prototyping small
apps, but its power allows scaling to full featured client side applications.
The combination ease of development, breadth of features, and performance has
led to wide adoption, and with wide adoption comes many common pitfalls. This
list captures common AngularJS mistakes, especially when scaling an app.
1 MVC
directory structure
AngularJS is,
for lack of a better term, an MVC framework. The models aren't as clearly
defined as a framework like backbone.js, but the architectural pattern still
fits well. When working in an MVC framework a common practice is to group files
together based on file type:
templates
/
_login
.html
_feed
.html
app
/
app
.js
controllers
/
LoginController
.js
FeedController
.js
directives
/
FeedEntryDirective
.js
services
/
LoginService
.js
FeedService
.js
filters
/
CapatalizeFilter
.js
This seems
like an obvious layout, especially if coming from a Rails background. However
once the app begins to scale, this layout causes many folders to be open at
once. Whether using Sublime, Visual Studio, or Vim with Nerd Tree, a lot of
time is spent scrolling through the directory tree.
Instead of
keeping the files grouped by type, group files based on features:
app
/
app
.js
Feed
/
_feed
.html
FeedController
.js
FeedEntryDirective
.js
FeedService
.js
Login
/
_login
.html
LoginController
.js
LoginService
.js
Shared
/
CapatalizeFilter
.js
This
directory structure makes it much easier to find all the files related to a
particular feature which will speed up development. It may be controversial to
keep
.html
files with .js
files, but the time savings are more
valuable.
2 Modules
(or lack thereof)
It's common
when starting out to hang everything off of the main module. This works fine
when starting a small app, but it quickly becomes unmanageable.
varapp
=angular
.module('app',[]);
app
.service('MyService',function(){
//service code
});
app
.controller('MyCtrl',function(
$scope
,MyService
){
//controller code
});
A common
strategy after this is to group similar types of objects:
varservices
=angular
.module('services',[]);
services
.service('MyService',function(){
//service code
});
varcontrollers
=angular
.module('controllers',['services']);
controllers
.controller('MyCtrl',function(
$scope
,MyService
){
//controller code
});
varapp
=angular
.module('app',['controllers','services']);
This strategy
scales as well as the directory structures from part 1: not great. Following
the same concept of grouping features together will allow scalability.
varsharedServicesModule
=angular
.module('sharedServices',[]);
sharedServices
.service('NetworkService',function(
$http
){});
varloginModule
=angular
.module('login',['sharedServices']);
loginModule
.service('loginService',function(
NetworkService
){});
loginModule
.controller('loginCtrl',function(
$scope
,loginService
){});
varapp
=angular
.module('app',['sharedServices',
'login']);
When working
on large applications everything might not be contained on a single page, and
by having features contained within modules it's much simpler to reuse modules
across apps.
3 Dependency
injection
Dependency injection
is one of AngularJS's best patterns. It makes testing much simpler, as well as
making it more clear upon which any particular object depends. AngularJS is
very flexible on how things can be injected. The simplest version requires just
passing the name of the dependency into the function for the module:
varapp
=angular
.module('app',[]);
app
.controller('MainCtrl',function(
$scope
,$timeout
){
$
timeout(function(){
console
.log($scope
);
},
1000);
});
Here, it's
very clear that
MainCtrl
depends on $scope
and $timeout
.
This works
well until you're ready to go to production and want to minify your code. Using UglifyJS the example becomes the following:
varapp
=angular
.module("app",[]);app
.controller("MainCtrl",function(e
,t
){t(function(){console
.log(e
)},1e3)})
Now how does
AngularJS know what
MainCtrl
depends upon? AngularJS provides a very simple solution to
this; pass the dependencies as an array of strings, with the last element of
the array being a function which takes all the dependencies as parameters.app
.controller('MainCtrl',['$scope',
'$timeout',
function(
$scope
,$timeout
){
$
timeout(function(){
console
.log($scope
);
},
1000);
}]);
This then
minifies to code with clear dependencies that AngularJS knows how to interpret:
app
.controller("MainCtrl",["$scope","$timeout",function(e
,t
){t(function(){console
.log(e
)},1e3)}])
3.1 Global dependencies
Often when
writing AngularJS apps there will be a dependency on an object that binds
itself to the global scope. This means it's available in any AngularJS code,
but this breaks the dependency injection model and leads to a few issues,
especially in testing.
AngularJS
makes it simple to encapsulate these globals into modules so they can be
injected like standard AngularJS modules.
Underscore.js is
a great library for simplifying Javascript code in a functional pattern, and it
can be turned into a module by doing the following:
varunderscore
=angular
.module('underscore',[]);
underscore
.factory('_',function()
{
return
window
._
;//Underscore must already be loaded on the page
});
varapp
=angular
.module('app',['underscore']);
app
.controller('MainCtrl',['$scope',
'_',
function(
$scope
,_
){
init
=function()
{
_
.keys($scope
);
}
init();
}]);
This allows
the application to continue in the AngularJS dependency injection fashion which
would allow underscore to be swapped out at test time.
This may seem
trivial and like unnecessary work, but if your code is using
use
strict
(and it should be!), then this becomes a requirement.
4 Controller
bloat
Controllers
are the meat and potatoes of AngularJS apps. It's easy, especially when
starting out, to put too much logic in the controller. Controllers should never
do DOM manipulation or hold DOM selectors; that's where directives and using
ng-model
come in. Likewise
business logic should live in services, not controllers.
Data should
also be stored in services, except where it is being bound to the
$scope
. Services are
singletons that persist throughout the lifetime of the application, while
controllers are transient between application states. If data is stored in the
controller then it will need to be fetched from somewhere when it is
reinstantiated. Even if the data is stored in localStorage, it's an order of
magnitude slower to retrieve than from with a Javascript variable.
AngularJS
works best when following the Single Responsibility Principle (SRP). If the
controller is a coordinator between the view and the model, then the amount of
logic it has should be minimal. This will also make testing much simpler.
5 Service vs
Factory
These names
cause confusion for almost every AngularJS developer when starting out. They
really shouldn't because they're syntactic sugar for (almost) the same thing!
Here are
their definitions from the AngularJS source:
functionfactory(
name
,factoryFn
){
return
provider(
name
,{
$
get:factoryFn
});
}
functionservice(
name
,constructor
){
return
factory(
name
,['$injector',
function(
$injector
){
return
$injector
.instantiate(constructor
);
}]);
}
From the
source you can see that
service
just calls the factory
function which in
turn calls the provider
function. In fact, AngularJS also offers a few additional provider
wrappers with value
, constant
, and decorator
. These other objects
don't cause nearly the same level of confusion and the docs are pretty clear on
their use cases.
Since
service
just calls the factory
function, what
makes it different? The clue is in $injector.instantiate
; within this function
the $injector
creates a new
instance of the service's constructor
function.
Here's an
example of a service and a factory that accomplish the same thing.
varapp
=angular
.module('app',[]);
app
.service('helloWorldService',function(){
this.
hello
=function()
{
return
"Hello World";
};
});
app
.factory('helloWorldFactory',function(){
return
{
hello
:function()
{
return
"Hello World";
}
}
});
When either
helloWorldService
or helloWorldFactory
are injected into
a controller, they will both have a hello
method that returns "Hello
World". The service constructor function is instantiated once at declaration
and the factory object is passed around every time it is injected, but there is
still just one instance of the factory. All providers
are singletons.
Why do the
two styles exist if they can accomplish the same thing? Factories offer
slightly more flexibility than services because they can return functions which
can then be
new
'd. This follows the factory pattern from
object oriented programming. A factory can be an object for creating other
objects.app
.factory('helloFactory',function()
{
return
function(
name
){
this.
name
=name
;
this.
hello
=function()
{
return
"Hello "
+
this.
name
;
};
};
});
Here's an
example controller using the service and the two factories. With the
helloFactory
returning a function,
the name value is set when the object is new
'd.app
.controller('helloCtrl',function(
$scope
,helloWorldService
,helloWorldFactory
,helloFactory
){
init
=function()
{
helloWorldService
.hello();//'Hello World'
helloWorldFactory
.hello();//'Hello World'
new
helloFactory('Readers').hello()
//'Hello Readers'
}
init();
});
When starting out it is best to just use services.
Factories can
also be more useful when designing a class with more private methods:
app
.factory('privateFactory',function(){
var
privateFunc
=function(
name
){
return
name
.split("").reverse().join("");//reverses the name
};
return
{
hello
:function(
name
){
return
"Hello "
+
privateFunc(
name
);
}
};
});
With this
example it's possible to have the
privateFunc
not accessible to
the public API of privateFactory
. This pattern is achievable in services, but factories make it
more clear.
6 Not
utilizing Batarang
Batarang is an
excellent Chrome extension for developing and debugging AngularJS apps.
Batarang
offers Model browsing to get a look inside what Angular has determined to be
the models bound to scopes. This can be useful when working with directives and
to isolate scopes to see where values are bound.
Batarang also
offers a dependency graph. If coming into an untested codebase, this would be a
useful tool to determine which services should get the most attention.
Finally,
Batarang offers performance analysis. AngularJS is quite performant out of the
box, but as an app grows with custom directives and complex logic, it can
sometimes feel less than smooth. Using the Batarang performance tool it is
quite simple to see which functions are taking the most time in the digest
cycle. The performance tool also shows the full watch tree, which can be useful
when having too many watchers.
7 Too many
watchers
As mentioned
in the last point, AngularJS is quite performant out of the box. Because of the
dirty checking done in a digest cycle, once the number of watchers exceeds
about 2,000 the cycle can cause noticeable performance issues. (The 2,000
number isn't guaranteed to be a sharp drop off, but it is a good rule of thumb.
Changes are coming in the 1.3 release of AngularJS that allow tighter control
over digest cycles. Aaron Gray has a nice write up on
this.)
This Immediately
Invoked Function Expression (IIFE) will print out the number of watchers
currently on the page. Simply paste it into the console to see how many
watchers are currently on the page. This IIFE was taken from Words Like Jared's
answer on StackOverflow:
(function()
{
var
root
=$
(document
.getElementsByTagName('body'));
var
watchers
=[];
var
f
=function
(
element
){
if
(
element
.data().hasOwnProperty('$scope')){
angular
.forEach(element
.data().$scope
.$$watchers
,function
(
watcher
){
watchers
.push(watcher
);
});
}
angular
.forEach(element
.children(),function
(
childElement
){
f(
$
(childElement
));
});
};
f(
root
);
console
.log(watchers
.length
);
})();
By using this
to determine the number of watchers and the watch tree from the performance
section of Batarang, it should be possible to see where duplication exists or
where unchanging data has a watch.
When there is
data that does not change, but you want it to be templated using Angular,
consider using bindonce. Bindonce is a
simple directive which allows you to use templates within Angular, but this
does not add a watch to keep the count from increasing.
8 Scoping
$scope's
Javascript's
prototype-based inheritance differs from class-based inheritance in nuanced
ways. This normally isn't a problem, but the nuances often arise when working
with
$scope
. In AngularJS every $scope
inherits from its parent $scope
with the highest
level being $rootScope
. ($scope
behaves slightly differently in directives, with isolate scopes
only inherit properties explicitly declared.)
Sharing data
from a parent to a child is trivial because of the prototype inheritance. It is
easy however to shadow a property of the parent
$scope
if caution is not
taken.
Let's say we
want to have a username displayed in a navbar, and it is entered in a login
form. This would be a good first try at how this might work:
<div ng
-controller
="navCtrl">
<
span
>{{user
}}</span
>
<
div ng
-controller
="loginCtrl">
<
span
>{{user
}}</span
>
<
input ng
-model
="user"></input
>
</
div
>
</div
>
Quiz time:
when a user types in the text input with the
user
ng-model set on
it, which template will update? The navCtrl
, loginCtrl
, or both?
If you
selected
loginCtrl
then you probably already understand how prototypical
inheritance works.
When looking
up literal values, the prototype chain is not consulted. If
navCtrl
is to be updated
simultaneously then a prototype chain lookup is required; this will happen when
the value is an object. (Remember, in Javascript, functions, arrays, and
objects are objects)
So to get the
desired behavior it is necessary to create an object on the
navCtrl
that can be
referenced from loginCtrl
.<div ng
-controller
="navCtrl">
<
span
>{{user
.name
}}</span
>
<
div ng
-controller
="loginCtrl">
<
span
>{{user
.name
}}</span
>
<
input ng
-model
="user.name"></input
>
</
div
>
</div
>
Now since
user
is an object, the
prototype chain will be consulted and the navCtrl
's template and $scope
will be updated
along with loginCtrl
.
This may seem
like a contrived example, but when working with directives that create child
$scope
's like ngRepeat
this issue can
arise easily.
9. Manual Testing
While TDD
might not be every developer's preferred development method, every time a
developer checks to see if code works or has broken something else he or she is
doing manual testing.
There are no
excuses for not testing an AngularJS app. AngularJS was designed to be testable
from the ground up. Dependency injection and the
ngMock
module are
evidence of this. The core team has developed a couple of tools that can bring
testing to the next level.
9.1 Protractor
Unit tests
are the building blocks of a test suite, but as the complexity of an app grows
integration tests tease out more realistic situations. Luckily the AngularJS
core team has provided the necessary tool.
We have built Protractor, an end to end test runner which simulates user
interactions that will help you verify the health of your Angular application.
Protractor
uses the Jasmine test
framework for defining tests. Protractor has a very robust API for different
page interactions.
There are
other end to end test tools, but Protractor has the advantage of understanding
how to work with AngularJS code, especially when it comes to
$digest
cycles.
9.2. Karma
Once
integration tests have been written using Protractor, the tests need to run.
Waiting for tests to run, especially integration tests, can be frustrating for
developers. The AngularJS core team also felt this pain and developed Karma.
Karma is a
Javascript test runner which helps close the feedback loop. Karma does this by
running the tests any time specified files are changed. Karma will also run
tests in parallel across different browsers. Different devices can also be
pointed to the Karma server to get better coverage of real world usage
scenarios.
10 Using jQuery
jQuery is an
amazing library. It has standardized cross-platform development and is almost a
requirement in modern web development. While jQuery has many great features,
its philosophy does not align with AngularJS.
AngularJS is
a framework for building applications; jQuery is a library for simplifying
"HTML document traversal and manipulation, event handling, animation, and
Ajax". This is the fundamental difference between the two. AngularJS is
about architecture of applications, not augmenting HTML pages.
In order to
really understand how to build an AngularJS application, stop
using jQuery. jQuery keeps the developer thinking of existing
HTML standards, but as the docs say "AngularJS lets you extend HTML
vocabulary for your application."
DOM
manipulation should only be done in directives, but this doesn't mean they have
to be jQuery wrappers. Always consider what features AngularJS already provides
before reaching for jQuery. Directives work really well when they build upon
each other to create powerful tools.
The day may
come when a very nice jQuery library is necessary, but including it from the
beginning is an all too common mistake.
Conclusion
This code has been in production for two
months, and we’re very happy with the results. You can see it in action at
the Scalyr Logs demo site. After entering the
demo, click the “Log View” link, and play with the Next / Prev buttons. It’s so
fast, you’ll find it hard to believe you’re seeing live data from a real
server.
Implementing these optimizations in a clean
manner was a fair amount of work. It would have been simpler to create a single
custom directive that directly generated all of the HTML for the log view,
bypassing ng-repeat. However, this would have been against the spirit of
AngularJS, bearing a cost in code maintainability, testability, and other
considerations. Since the log view was our test project for AngularJS, we
wanted to verify that a clean solution was possible. Also, the new directives
we created have already been used in other parts of our application.
We did our best to follow the Angular
philosophy, but we did have to bend the AngularJS abstraction layer to
implement some of these optimizations. We overrode the Scope’s $watch method to
intercept watcher registration, and then had to do some careful manipulation of
Scope’s instance variables to control which watchers are evaluated during a
$digest.
No comments:
Post a Comment