
Optimize the bundle size of an Angular application
Optimize the bundle size of an Angular application
In this video learn how to optimize the bundle size of your Angular applications. You’ll learn:
‣ What’s tree-shaking
‣ Understanding the structure of your production bundles
‣ Using code splitting and finding suboptimal imports
Content
0.56 -> one of the fundamental goals of web
2.32 -> development
3.04 -> is to create applications that people
5.12 -> enjoy using
6.24 -> delivering an enjoyable experience
8.08 -> hinges on web performance which becomes
10.08 -> increasingly difficult to control
11.759 -> as an app grows in complexity end users
14.4 -> expect content to load quickly
16.08 -> and respond to user interaction
17.92 -> instantly but that's easier said than
19.68 -> done
20 -> when an app has dozens of features and
22.08 -> multiple megabytes of dependencies in
23.92 -> its production javascript bundle
25.76 -> when it comes to performance every
27.279 -> millisecond counts a recent study
29.199 -> conducted by deloitte
30.48 -> observed non-trivial gains in user
32.559 -> interaction and revenue
34.16 -> based on a seemingly small load time
35.92 -> improvement of just 1 10 of a second
38 -> or 100 milliseconds luckily angular
40.719 -> developers have multiple tools at their
42.559 -> disposal for
43.52 -> analyzing and avoiding performance
45.36 -> bottlenecks the goal of this video
47.36 -> is to analyze and optimize a production
50.079 -> javascript bundle in angular
51.76 -> we'll use tools like source map explorer
53.76 -> to understand what's inside the bundle
55.6 -> then look at a variety of common
56.96 -> pitfalls that may be making it too large
59.039 -> we'll look at architectural patterns
60.719 -> like lazy loading or code splitting
62.64 -> and once the app is optimized we'll use
64.72 -> budgets to prevent performance
66.24 -> regressions in the future
68 -> in order to address performance you
69.68 -> first need to understand how your app
71.439 -> performs
72 -> in a real environment if you have an
73.92 -> existing application deployed on the web
75.92 -> one of the easiest ways to analyze it is
78.08 -> with the lighthouse tool in chrome
80.159 -> visit your site then find the lighthouse
82.08 -> tool in chrome devtools
83.439 -> and generate a report the tool will
85.52 -> emulate mobile devices and slow
87.36 -> connections
88.159 -> then generate a report that contains a
90.4 -> variety of metrics
91.52 -> like how long it takes for content to
93.439 -> first appear on the page
94.64 -> and how long it takes your javascript
96.24 -> code to become interactive
98 -> it'll also detect opportunities for
99.759 -> improvement where you can determine
101.119 -> which assets are creating performance
102.96 -> bottlenecks
103.68 -> and if you want to dive deeper you can
105.36 -> view the original performance trace in
107.04 -> chrome
107.6 -> which breaks down how the browser parses
109.439 -> your application at a highly granular
111.439 -> level
112.159 -> lighthouse is great for detecting issues
113.92 -> that we may want to optimize
115.84 -> but if we're facing an issue with the
117.36 -> size of our javascript bundle
119.04 -> we need to dive deeper into our own code
121.119 -> to fully understand how to address
123.04 -> it let's jump into our editor and take a
125.119 -> look at our code base
126.32 -> what we have here is a fairly simple
128.08 -> angular application
129.599 -> the angular router is used to manage two
131.76 -> different pages the home page
133.44 -> and the feature page which contains the
135.36 -> majority of the app's complexity
137.52 -> the main implementation details are not
139.44 -> relevant but over the next few minutes
141.44 -> we'll look at issues related to
142.879 -> dependencies and code motion
144.72 -> that can affect the size of the
145.92 -> javascript bundle in ways that are not
147.92 -> totally obvious
149.04 -> in addition if we open the package json
151.36 -> you'll notice several popular libraries
153.519 -> like angular material firebase lowdash
156.56 -> and moment.js now at this point the app
159.28 -> works perfectly and all our tests pass
161.44 -> but we're getting complaints from
162.72 -> customers that the app is slow
164.48 -> especially when used on a poor
165.92 -> connection after running a lighthouse
167.599 -> audit we get a low performance score
169.84 -> due to render blocking assets like our
172 -> javascript bundle
173.04 -> and css bundle but our app is relatively
175.599 -> simple
176.08 -> so the question becomes why are these
177.92 -> files so large a tool that can help us
180.08 -> answer that question
181.04 -> is source map explorer it allows us to
183.519 -> take the source maps generated by
185.2 -> angular and analyze them visually
187.44 -> the treemap visual allows us to drill
189.599 -> down into our own code to see which
191.84 -> modules might be causing bloat
193.519 -> and it also allows us to compare
195.12 -> dependencies to determine if we have any
197.36 -> packages that are unexpectedly large
199.68 -> to put this tool to use we'll first need
201.44 -> to install it with npm
202.959 -> and we can use the save dev flag to put
204.799 -> it in the development environment
206.48 -> from there we need to run a production
208.4 -> build of our code which we'd normally do
210.56 -> with the ng build command however if we
213.12 -> go into the dist folder
214.56 -> you'll notice by default it doesn't
216.48 -> generate source maps on the production
218.239 -> build
218.72 -> we can easily address that by adding the
220.64 -> source map flag to the ng build command
223.28 -> then you'll notice in the dist folder
224.959 -> every javascript file has a
226.48 -> corresponding source map file as well
228.48 -> a source map file is basically just a
230.64 -> way to take your minified production
232.64 -> code
233.12 -> and map it back to its original
234.959 -> uncombined state used in development
237.28 -> they make it much easier to debug
239.04 -> runtime issues in your code
240.64 -> and as we'll see analyze performance
242.64 -> based on the javascript bundle size
244.959 -> it's also worth noting at this point
246.56 -> that you can generate source maps in the
248.64 -> angular json configuration
250.64 -> the build configuration contains an
252.4 -> option for source map which is currently
254.56 -> set to false however
256 -> we can set that to true or we can pass
258.239 -> an object to control which source maps
260.4 -> are generated
261.199 -> in this demo we'll generate maps for all
263.199 -> of our own scripts in addition to css
265.28 -> style sheets
266 -> and vendor packages which are the
267.6 -> dependencies we install via npm
269.759 -> and now with this configuration set we
271.759 -> can run the regular ng build command
274 -> and it will generate the source maps now
276.08 -> in order to run the source map explorer
278 -> we'll go back to our package json and
280.24 -> add a script
281.36 -> that runs the source map explorer
282.96 -> command we need to point the command to
284.88 -> the files we want to analyze
286.32 -> which live in the disk directory and
288.16 -> then we'll use a glob pattern to capture
290.4 -> all the files in that directory
292.24 -> now from the command line we can run npm
294.639 -> run explorer
295.68 -> and that should bring up the tree map
297.12 -> visualization in the browser
299.12 -> at first glance this visual likely looks
301.52 -> very overwhelming
302.72 -> but the thing to look for here is the
304.479 -> size of the items relative to each other
306.88 -> the bigger the item the more space it
308.639 -> takes up in the javascript bundle
310.639 -> and you'll also notice that each item
312.4 -> has a percentage next to it
314.08 -> telling you how much space that module
316 -> takes up relative to the entire file or
318.639 -> application
319.759 -> in this demo you'll notice our main
321.44 -> source code all the way down here at the
322.88 -> bottom
323.52 -> taking up only one point seven percent
325.759 -> of the total bundle size
327.36 -> meanwhile we have firebase taking up
329.199 -> more than half the bundle size
330.639 -> along with additional big libraries like
332.479 -> lowdash and moment
334 -> when you click on an item it allows you
335.759 -> to drill deeper into its makeup that's
338.08 -> especially useful for your own source
340 -> code
340.4 -> to see which components directives and
342.72 -> services are contributing most to the
344.72 -> bundle size
345.759 -> now that we know how to use source map
347.039 -> explorer i want to point out another
348.88 -> common tool that you may come across
350.479 -> called webpack bundle analyzer it's a
353.039 -> great tool
353.6 -> and very similar to source map explorer
356 -> but because the angular cli
357.759 -> extends webpack it's generally not the
359.84 -> best option for
360.96 -> analyzing an angular application if you
363.28 -> happen to be using that tool
364.56 -> just keep in mind that you may not be
366.16 -> getting perfectly accurate results
368 -> when compared to source map explorer in
370.319 -> any case it's obvious that our current
372.24 -> application has a major issue with the
374.24 -> way it consumes dependencies
376.08 -> let's take a look at a few common
377.6 -> pitfalls that may be causing an
379.36 -> unnecessarily large javascript bundle
381.44 -> here
381.919 -> one of the most important concepts to
383.52 -> understand is tree shaking
385.28 -> or dead code elimination which is the
387.52 -> process of eliminating unnecessary
389.6 -> modules from your javascript bundle
391.84 -> notice how in this component we're
393.36 -> importing two popular javascript
395.039 -> libraries
395.84 -> moment and lowdash these are both
398.24 -> utility libraries
399.36 -> where in most cases the developer only
401.199 -> needs one or two functions or classes
403.52 -> with everything else being unused but
405.68 -> when we import it like this
407.039 -> we're basically telling the angular cli
409.039 -> and webpack to include the entire
410.88 -> library in our javascript bundle
412.96 -> first let's take a look at moment this
415.199 -> happens to be a library that is not very
417.199 -> tree shakeable
418.16 -> meaning if we want to use it then we
419.84 -> have to bring along its 250 kilobytes
422.319 -> with us
423.12 -> with that being said the first thing you
424.8 -> should ask yourself is do i really need
426.8 -> this dependency
428 -> if you're only using a small handful of
429.759 -> functions from moment it may be
431.44 -> beneficial
432.16 -> to simply re-implement that logic
434.319 -> natively in javascript eliminating a
436.56 -> dependency entirely is great
438.4 -> but in some cases the extra complexity
440.4 -> is just not worth it
441.599 -> another option is to look for
443.12 -> alternative packages that prioritize
445.28 -> performance
446.08 -> in the case of moment a good alternative
448.16 -> is date fns which uses
450 -> functional programming principles to
451.759 -> make the code more tree shakeable
453.599 -> now let's shift our attention over to
455.199 -> lowdash it happens to be a tree shakable
457.52 -> library we're just not using it properly
459.68 -> instead of the default lodash package we
461.599 -> can install lowdash es which exports
464 -> each function as an ecmascript module
466.56 -> now instead of the entire package
468.16 -> we can import just the functions we need
470.319 -> and eliminate all the unnecessary code
472.24 -> from the bundle
473.199 -> another major dependency in this project
475.039 -> is firebase and currently we're
477.039 -> importing the entire
478 -> firebase sdk that includes a bunch of
480.16 -> services that we're not actually using
481.919 -> in the app
482.56 -> and this particular dependency exports
484.56 -> different packages from namespaces
486.72 -> the only package that's actually
488.08 -> required is firebase app
490 -> then we can selectively import anything
492.24 -> else we need beyond that
493.599 -> like firebase auth for example all of
495.759 -> the other packages will then
496.96 -> automatically be left out of the
498.56 -> production bundle
499.759 -> now one other thing you might run into
501.36 -> as an angular developer is an accidental
503.759 -> import from rxjs rx is one of the
506.639 -> fundamental dependencies of angular
508.56 -> but sometimes when you import an
510.479 -> operator or class from rxjs
512.719 -> your ide might accidentally structure
515.12 -> that import
516.08 -> from one of its internal modules it's an
518.32 -> easy mistake to make and might add a few
520.159 -> additional kilobytes to your bundle
521.919 -> so just be aware of that possibility and
523.919 -> audit your imports from rxjs
526.24 -> it's also important to understand that
528.24 -> unnecessary imports don't just affect
530.24 -> javascript
531.04 -> but also css one thing to watch out for
533.92 -> is the import of css styles into
536.399 -> individual components for example we
538.959 -> might have a global button style
540.88 -> but have one particular component with a
542.88 -> custom button
543.92 -> in order to customize the button in that
545.76 -> component we might re-import our button
547.839 -> styles there
548.8 -> but because each component has style
550.64 -> encapsulation we'd end up with two
552.8 -> copies of the button css code in our
554.959 -> bundle
555.92 -> when importing styles into a component
557.839 -> it's important to consider whether or
559.2 -> not you want those styles encapsulated
560.959 -> or not
562 -> in general styles that can be used by
564.08 -> multiple components should be taken to a
566 -> more global level
567.12 -> rather than be encapsulated and
568.72 -> duplicated across multiple components
571.12 -> now that our dependencies are under
572.399 -> control we can turn our attention to our
574.32 -> own code
574.959 -> and implement a technique known as lazy
577.12 -> loading or code splitting
578.959 -> as your app grows in complexity you
580.8 -> likely have multiple features
582.48 -> and a user won't be expected to use all
584.64 -> these features at any given moment
586.959 -> therefore it's often a good idea to
589.44 -> leave out unnecessary features
591.2 -> from the main app bundle then load them
593.519 -> asynchronously in the background
595.279 -> or when they become necessary after the
597.2 -> user clicks on a link to that feature
599.839 -> we can implement lazy loading by first
601.92 -> making our code modular
603.519 -> currently our app is organized into a
605.68 -> single app module
606.959 -> but earlier in the video i mentioned
608.64 -> that our feature module contains most of
610.56 -> the complexity
611.44 -> we can organize this complexity into a
613.44 -> module by generating a new module with
616 -> the cli
617.12 -> once generated we can go into our app
619.279 -> module and remove
620.56 -> any components directives or services
622.8 -> that are not directly needed by the app
624.72 -> module
625.36 -> and move them over to the feature module
628 -> that isolates our code into an ng module
630.56 -> and now we can shift our attention to
632.32 -> the angular routing configuration
634.24 -> currently you'll notice we're routing to
635.92 -> a path of feature that loads the feature
638.079 -> component
638.8 -> which is a setup that requires the
640.64 -> feature component to be included
642.64 -> in the main app bundle however now that
645.44 -> that component lives in its own module
647.44 -> we can use the load children property to
649.44 -> perform a dynamic
650.64 -> import that loads it asynchronously a
653.279 -> dynamic import is a relatively new
655.12 -> ecmascript standard
656.24 -> that makes it possible to import a
658 -> module at runtime
659.44 -> where it's resolved as a promise and in
661.6 -> this case the promise resolves with the
663.6 -> feature module that we want to load
665.44 -> that'll defer any additional routing to
667.279 -> the feature module
668.48 -> so if we go into the feature module
670.24 -> itself we can add a route for the
672 -> feature component there
673.519 -> and then include the router module for
675.519 -> child in the imports array
677.36 -> the end result here is that when the
679.12 -> user lands on the home component
681.12 -> the browser doesn't need to load the
682.56 -> javascript that goes along with the main
684.64 -> app feature
685.76 -> instead it can be loaded in the
687.2 -> background or after the user clicks on a
689.6 -> link to that feature
690.959 -> at this point we can run the ng build
692.8 -> command again and you'll notice a new
694.48 -> javascript file in the disk directory
696.8 -> this new file or chunk contains the code
699.279 -> for the lazy loaded module
700.959 -> we can see how this affects performance
702.8 -> by opening the app in the browser
704.8 -> with the network tab opened in chrome
706.64 -> dev tools when we navigate to the home
708.32 -> page
708.72 -> the main js file is loaded but the
710.72 -> javascript required for the feature
712.32 -> module is delayed until the user
714.399 -> actually clicks the link to that
716 -> feature you can perform lazy loading for
718.56 -> multiple modules
719.839 -> and you can even lazy load individual
721.68 -> components and the bottom line though
723.68 -> is that it's a highly effective
725.04 -> technique for managing the size of your
727.04 -> application
727.76 -> as it grows in complexity now that we
729.76 -> have our javascript bundle down to an
731.2 -> acceptable size
732.24 -> there's a tool in angular that we can
733.92 -> use to prevent performance regressions
735.839 -> in the future when you run the ng build
737.6 -> command you may notice output in the
739.2 -> console
739.76 -> warning you that your bundle size has
742 -> exceeded a certain limit
743.6 -> the limit is based on a budget that you
745.68 -> can configure in the angular json file
748.24 -> you can set up multiple budgets where
750.079 -> each budget is an object
751.6 -> that has a type then a threshold for
753.839 -> producing a warning in the console
755.6 -> and another error threshold at which
757.519 -> point the build will fail
758.8 -> if it exceeds that amount that's
760.56 -> especially useful for continuous
762.16 -> integration where your build will fail
764.079 -> on the ci server
765.279 -> rather than be pushed out to production
766.88 -> with a massive bundle size
768.8 -> now let's go ahead and recap what we've
770.48 -> covered in this video tools like
772.079 -> lighthouse and source map explorer can
774.16 -> help you find and understand performance
776.24 -> bottlenecks
777.04 -> if you run into issues related to
778.56 -> dependencies verify that your imports
780.8 -> are structured properly and attempt to
782.48 -> use tree shakeable libraries
784.24 -> but if the bundle size is related to
785.839 -> your own code look into lazy loading or
788.24 -> code splitting
789.04 -> to remove your own javascript from the
790.88 -> browser's critical path
792.32 -> and once the app has been optimized use
794.399 -> cli budgets
795.44 -> to prevent regressions in the future for
797.6 -> additional examples and details
799.2 -> refer to the official angular
801 -> documentation
Source: https://www.youtube.com/watch?v=19T3O7XWJkA