Optimising Angular
Setup
- node 12 required
- clone
npm i
(oryarn
as you prefer)npm ci
if you encounter errorsnpm start
(browser -> localhost:4200)
What influences the app performance?
- On the Component level (low performance). That's usually caused by unnecessary Change Detection cycles or too many of them.
- On the Module level (slow loading)
Low performance (components)
Performance - trackBy (ngFor)
-
Notice the
/trackby
route of the app accessible via the Task TrackBy on the navigation. Interact with the controls on the left (width, height, by) and notice the updating count of all components. That's because we keep changing the referenced objects filtered and updated by thetrackby-article.service
(link) with the input provided in theadmin-article-visualize-control.component
(i.e. the aforementioned controls - width, height, by). Every new component instance will prompt Angular to destroy the old instance and replace it with the new in the DOM.- the number in front of the component indicates the serial number of the component instance - the first is 1., second 2. and so on
- notice how every time we interact with the width controls that causes a whole set of new components to be instantiated
- and there is a slow operation to demonstrate a sluggish user experience
- this might be avoided by using the
trackBy
capability of thengFor
directive
-
Add an
articleSlug
property in thetask-trackby/components/article-list.component
(link) -
Let it be of type
TrackByFunction<Article>
-
Assign a function to the property that accepts index and an item if type
Article
and return the slug of the article. -
Now notice the template of
article-list.component
. -
Add a
;trackBy:articleSlug
to the end of the*ngFor
declaration. That will instruct Angular to take the returned value and check that for equality with the previous one instead of just comparing object references. -
Notice how the controls no longer cause the redrawing of the whole list and rather make the existing components change.
Performance hit - redundant component instances
Performance - OnPush
-
Notice the
/on-push
route accessible via the Task OnPush on the navigation. -
What is happening?
- each event that triggers change detection triggers change detection for each of the article components, including:
- typing in the input (each letter counts as an event)
- interacting with the
[+]
,[-]
buttons
- the initial number is caused by router events (see app.component ngOnInit and uncomment router events logging)
- the first of the component gets twice as many changes because it's getting used for the debugChangeDetection second cycle which gives us the infamous
expressionChangedAfterItHasBeenCheckedError
only in dev mode - How to check if that's not the case in a production build? Run
ng serve --prod
for prod build serve, where we do not get the debugChangeDetection second cycle.
- the first of the component gets twice as many changes because it's getting used for the debugChangeDetection second cycle which gives us the infamous
- each event that triggers change detection triggers change detection for each of the article components, including:
-
How to fix?
- using change detections strategy OnPush
- each article component will only run change detection if its input changes
- any async pipe input counts too
-
Adjust the change detection strategy of the
task-onpush/components/article.component
(link) to on-push. -
Try typing in the input again and notice if the change detection is triggered in the article
-
Review (for help see task-onpush/article.component.ts.help)
Performance hit - too many change detections
Performance - debounce
-
Notice the
/debounce
route accessible via the Task Debounce nav link. -
The
debounce-search.component
initializes the search by providing the changes observable to thedebounce-article.service.ts
. Then the service will construct the (mock) request out of each emission of that observable. -
The effect is manifested by typing in the search resulting in a request for each typed character
-
Apply the
debounceTime
operator in thedebounce-search.component
(exdebounceTime(400)
)- or in the
debounce-article.service.ts
- or in the
-
Notice that the service waits for you to finish writing before sending the request
Performance hit - too many change detections
Performance - window:mouseout
- Visit the
/listener
app route. - Try interacting with the articles and notice the app "freezes". There is an indicator which spins while the app renders smoothly and stops if the main loop is blocked
- Use the Angular tools or Browser perf tools to notice where the main thread spends its time
Performance hit - too many change detections
Slow loading
- long running operations
- multiple data fetching with no data change
Bundle size
-
[SETUP step] Run
npm i -g webpack-bundle-analyzer
see help -
Run
ng build --prod --stats-json --named-chunks
-
Run
webpack-bundle-analyzer dist/stats.json
(keep tab open for comparison)- capabilities: searching, zooming in, and out, etc.
- sizes explanation
-
Notice
Lazy loading
-
Make the Article module lazy
- remove ArticleModule from AppModule
- make the route use
loadChildren: "./article/article.module#ArticleModule"
-
Make Settings module lazy - same steps as above
-
Note the bundle sizes change (run steps 2. and 3. from the Bundle Size section above)
ng build --stats-json --named-chunks && webpack-bundle-analyzer dist/stats.json
-
Review (see app-routing.module.ts and app.module.ts)
-
Explore what Angular does automatically for us
- Run
ng build --prod --common-chunk false --stats-json --named-chunks && webpack-bundle-analyzer dist/stats.json
- Run
Removal of unused modules manually
- Note the moment locales (keep the browser tab open for comparison)
- Add
"postinstall": "node ./tools/remove-unused-locales.js"
toscripts
section of package.json - Run
npm i
to invoke the post-install hook script ng build --prod --stats-json --named-chunks
andwebpack-bundle-analyzer ./dist/stats.json
and `` and see the bundle size differ- Review
Manual JS lazy module load
-
Notice the
Pusher
is a large part of our main bundle. Turns out the user needs to agree for us to send them notifications. Let's make the pusher module lazy-loaded - that's a JavaScript module (vs Angular Module - which gets lazy-loaded via Routes primarily though there are options) -
Notice pusher-service.ts. It imports the Pusher library - no matter if anyone uses it or not: (i.e. if
I want notifications
has been pressed)ts import * as Pusher from 'pusher-js';
-
To lazy load that module we need to:
-
change the
module
setting intsconfig.app.json
toesnext
"module": "esnext"
-
replace
getPusherInstance
method in pusher service with:private getPusherInstance() { if (this.instance != null) { return Promise.resolve(this.instance); } return import('pusher-js').then((p: any) => { // we know this is imported as { default: PusherStatic } contrary to what our import types this as const Pusher: Pusher.PusherStatic = p.default; this.instance = new Pusher(this.key, this.config); return this.instance; }); }
-
-
Now run the
ng build --prod --stats-json --named-chunks && webpack-bundle-analyzer dist/stats.json
and notice now Pusher has its own bundle -
Review (see help)
Pusher config
[Bonus] How to actually use pusher
- sign in with the Conduit app - your user email will be the "channel"
- visit https://dashboard.pusher.com/accounts/sign_up to sign up
- get a token:
- create an app in Channels sandbox plan from the dashboard
Get started
orManage -> Add app
- get a token from
App keys
in the app - add the
key
toapp.module.ts
where/** Pusher key here*/
is
- create an app in Channels sandbox plan from the dashboard
- push some messages from the dashboard at pusher.com and see them appear in the app by:
- find the
your app -> debug -> Event creator
- the channel is your user email
- the event should be 'notify' (as that's what's been setup - change or add more in the
PusherService
) - notice the events arrive in the lower right corner of the app
- find the
Tree shaker
Explore what Angular does automatically with the tree shaker
- Note the
projects/ts
folder. (ts = tree shaker) - There are 2 components in the
SharedModule
AppHeader
AppFooter
- Note the
main
,secondary
andthird
components and- see that
MainComponent
only uses theAppHeader
- see that
SecondaryComponent
only uses theAppFooter
- see that
ThirdComponent
uses both theAppHeader
andAppFooter
- see that
- Run
ng build ts --prod --common-chunk false --stats-json --named-chunks && webpack-bundle-analyzer dist/ts/stats.json
- notice we are building the ts project
- see that only the used components end up in the lazy loading bundles, even though each imports the whole
SharedModule
using the shared module and its shared components - the
--common-chunk false
prevents Angular build from bunching up all those in one bundle (remove it and see where they end up) - the
--prod
makes Angular build use the tree shaker to remove the unused components from the modules (try the stats without it)
The es5 vs es2015
How does the browser know which version to load?
See the index.html
resulting from the ng build
. Notice how there is type="module"
in the es2015 script. That will be ignored by older browsers because they only know about type="application/javascript"
and not about "module"
. So the older browsers will skip this one and load the others. On the other hand the newer browsers will skip the script
tags that have the nomodule
attribute.
Links
-
Angular docs
-
Angular dev tools
-
Change detection resources
- https://medium.com/@guptagaruda/angular-2-4-visualizing-change-detection-default-vs-onpush-3d7ed1f69f8e
- https://www.mokkapps.de/blog/the-last-guide-for-angular-change-detection-you-will-ever-need/
- https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwi5yan2o6PzAhWegv0HHU7zCBoQwqsBegQIBRAB&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dl8mCutUMh78&usg=AOvVaw1kCanWwIdtOSmQx4qQlzCy
-
webpack bundle analyzer