SPEAKER: One of the fundamental
goals of web development is to create applications
that people enjoy using. Delivering an
enjoyable experience hinges on web
performance, which becomes increasingly
difficult to control as an app grows in complexity. End-users expect content
to flow quickly and respond to user interaction instantly. But that's easier
said than done when an app has dozens of features
and multiple megabytes of dependencies in its
production JavaScript bundle. When it comes to performance,
every millisecond counts. A recent study
conducted by Deloitte observed nontrivial gains in
user interaction and revenue based on a seemingly small
load-time improvement of just 1/10 of a second,
or 100 milliseconds. Luckily, Angular developers
have multiple tools at their disposal for
analyzing and avoiding performance bottlenecks. The goal of this video is
to analyze and optimize a production JavaScript
bundle in Angular. we'll use tools like
source map explorer to understand what's
inside the bundle, then look at a variety
of common pitfalls that may be making it too large. We'll look at architectural
patterns like lazy loading or code splitting, and
once the app is optimized, we'll use budgets to prevent
performance regressions in the future. In order to address
performance, you first need to understand how your app
performs in a real environment. If you have an existing
application deployed on the web, one of the
easiest ways to analyze it is with the Lighthouse
tool in Chrome. Visit your site, then find
the Lighthouse tool in Chrome DevTools and generate a report. The tool will emulate
mobile devices and slow connections,
then generate a report that contains
a variety of metrics, like how long it takes for
content to first appear on the page, and how long
it takes your JavaScript code to become interactive. It'll also detect
opportunities for improvement, where you can determine which
assets are creating performance bottlenecks. And if you want to
dive deeper, you can view the
original performance trace in Chrome,
which breaks down how the browser parses
your application at a highly-granular level. Lighthouse is great
for detecting issues that we may want to optimize. But if we're facing an issue
with the size of our JavaScript bundle, we need to dive
deeper into our own code to fully understand
how to address it. Let's jump in to our editor and
take a look at our code base. What we have here is a fairly
simple Angular application. The Angular router is used to
manage two different pages-- the home page and
the feature page, which contains the majority
of the app's complexity. The main implementation
details are not relevant, but over the next
few minutes, we'll look at issues related to
dependencies and code motion that can affect the size
of the JavaScript bundle in ways that are
not totally obvious. In addition, if we
open the package.json you'll notice several
popular libraries like angular material,
firebase, lodash, and moment.js. Now at this point the app works
perfectly in all our test pass, but we're getting
complaints from customers that the app is slow, especially
when used on a poor connection. After running a
Lighthouse audit we get a low performance score
due to render blocking assets like our JavaScript
bundle and CSS bundle. But our app is relatively
simple, so the question becomes, why are
these files so large? A tool that can help
us answer that question is Source Map Explorer. It allows us to take the source
maps generated by Angular and analyze them visually. The tree map visual allows us
to drill down into our own code to see which modules
might be causing bloat and it also allows us
to compare dependencies to determine if we
have any packages that are unexpectedly large. To put this tool
to use, we'll first need to install it with npm. And we can use the
save-dev flag to put it in the development environment. From there we need to
run a production build of our code, which we'd normally
do with the ng build command. However, if we go
into the dist folder, you'll notice by
default, it doesn't generate source maps on
the production build. We can easily address that
by adding the source map flag to the ng build command. Then you'll notice
in the dist folder, every JavaScript file has
a corresponding source map file as well. A source map file is
basically just a way to take your minified
production code and map it back to its
original uncombined state used in development. They make it much easier
to debug runtime issues in your code and as we'll
see, analyze performance based on the JavaScript bundle size. It's also worth
noting at this point that you can generate source
maps in the Angular json configuration. The build configuration contains
an option for source map, which is currently set to false. However, we can set
that to true or we can pass an object
to control which source maps are generated. In this demo we'll generate
maps for all of our own scripts in addition to CSS stylesheets
and vendor packages, which are the dependencies
we install via npm. And now with this
configuration set, we can run the regular
ng build command and it will generate
the source maps. Now in order to run the
Source Map Explorer, we'll go back to
our packaged json and add a script that runs the
Source Map Explorer command. We need to point the
command to the files we want to analyze which live
in the dist directory, and then we'll
use a glob pattern to capture all of the
files in that directory. Now from the command line,
we can run npm run explore and that should
bring up the tree map visualization in the browser. At first glance, this visual
likely looks very overwhelming, but the thing to
look for here is the size of the items
relative to each other. The bigger the
item, the more space it takes up in the
JavaScript bundle and you'll also notice that
each item has a percentage next to it telling you how much
space that module takes up relative to the entire
file or application. In this demo, you'll
notice our main source code all the way down here at the
bottom, taking up only 1.7% of the total bundle size. Meanwhile we have
firebase taking up more than half the
bundle size along with additional big libraries
like lodash and moment. When you click on an item,
it allows you to drill deeper into its makeup. That's especially useful
for your own source code to see which components,
directives, and services are contributing most
to the bundle size. Now that we know how to
use Source Map Explorer, I want to point out
another common tool that you may come across
called Webpack Bundle Analyzer. It's a great tool and very
similar to Source Map Explorer, but because the Angular
CLI extends Webpack, it's generally not
the best option for analyzing an
Angular application. If you happen to
be using that tool, just keep in mind
that you may not be getting perfectly accurate
results when compared to Source Map Explorer. In any case, it's obvious
that our current application has a major issue with the
way it consumes dependencies. Let's take a look at
a few common pitfalls that may be causing an
unnecessarily large JavaScript bundle here. One of the most important
concepts to understand is tree shaking, or a
dead code elimination, which is the process of
eliminating unnecessary modules from your JavaScript bundle. Notice how in this
component we're importing two popular JavaScript
libraries-- moment and lodash. These are both utility
libraries where in most cases the developer only needs one
or two functions or classes with everything
else being unused. But when we import
it like this we're basically telling the
Angular CLI and Webpack to include the entire library
in our JavaScript bundle. First, let's take
a look at moment. This happens to
be a library that is not very
tree-shakeable, meaning if we want to use it then
we have to bring along its 250 kilobytes with us. With that being said, the first
thing you should ask yourself is, do I really need
this dependency? If you're only using a
small handful of functions from moment it may be
beneficial to simply reimplement that logic natively
in JavaScript. Eliminating a dependency
entirely is great, but in some cases
the extra complexity is just not worth it. Another option is to look
for alternative packages that prioritize performance. In the case of moment,
a good alternative is date-fns, which uses
functional programming principles to make the
code more tree-shakeable. Now let's shift our
attention over to lodash. It happens to be a
tree-tree-shakeable library, we're just not
using it properly. Instead of the
default lodash package we can install lodash-es,
which exports each function as an ECMAScript module. Now instead of the
entire package, we can import just
the functions we need and eliminate all
the unnecessary code from the bundle. Another major dependency in
this project is firebase, and currently we're
importing the entire firebase SDK that includes
a bunch of services that we're not actually
using in the app. In this particular
dependency, exports different packages from namespaces. The only package that's actually
required is firebase app, then we can selectively
import anything else we need beyond that like
firebase/auth, for example. All of the other packages
will then automatically be left out of the
production bundle. Now one other
thing you might run into as an Angular developer is
an accidental import from rxjs. rx is one of the fundamental
dependencies of Angular, but sometimes when you import
an operator or class from rxjs, your IDE might accidentally
structure that import from one of its internal modules. It's an easy mistake
to make, and might add a few additional
kilobytes to your bundle. So just be aware
of that possibility and audit your
imports from rxjs. It's also important
to understand that unnecessary imports
don't just affect JavaScript, but also CSS. One thing to watch out for
is the import of CSS styles into individual components. For example, we might
have a global button style but have one particular
component with a custom button. In order to customize the
button in that component, we might re-import our
button styles there, but because each component
has style encapsulation we'd end up with two
copies of the button CSS code in our bundle. When importing styles
into a component, it's important to
consider whether or not you want those styles
encapsulated or not. In general, styles that can
be used by multiple components should be taken to
a more global level rather than be
encapsulated and duplicated across multiple components. Now that our dependencies
are under control, we can turn our
attention to our own code and implement a technique
known as lazy loading or code splitting. As your app grows in
complexity, you likely have multiple
features and a user won't be expected to use all
these features at any given moment. Therefore, it's
often a good idea to leave out unnecessary
features from the main app bundle, then load
them asynchronously in the background
or when they become necessary after the user clicks
on a link to that feature. We can implement lazy loading by
first making our code modular. Currently, our app is organized
into a single app module but earlier in the
video, I mentioned that our feature module
contains most of the complexity. We can organize this
complexity into a module by generating a new
module with the CLI. Once generated, we can
go into our app module and remove any
components, directives, or services that are
not directly needed by the app module and move them
over to the feature module. That isolates our code
into an ng module, and now we can shift our
attention to the Angular routing configuration. Currently, you'll notice
we're routing to a path a feature that loads the
feature component, which is a setup that requires
the feature component to be included in the main app bundle. However, now that that component
lives in its own module, we can use the load
children property to perform a dynamic import
that loads it asynchronously. A dynamic import is a relatively
new ECMAScript standard that makes it possible to import
a module at runtime, where it's resolved as a promise. And in this case, the promise
resolves with the feature module that we want to load. That will defer any additional
routing to the feature module So. If we go into the
feature module itself, we can add a route for
the feature component there, and then include
the router module for a child in
the imports array. The end result here is that
when the user lands on the home component, the
browser doesn't need to load the JavaScript that
goes along with the main app feature. Instead, it can be
loaded in the background or after the user clicks
on a link to that feature. At this point, we can run
the ng build command again, and you'll notice
a new JavaScript file in the dist directory. This new file or chunk
contains the code for the lazy loaded module. We can see how this
affects performance by opening the app in the
browser with the Network tab open in Chrome DevTools. When we navigate
to the home page, the main js file is
loaded, but the JavaScript required for the
feature module is delayed until the
user actually clicks the link to that feature. You can perform lazy
loading for multiple modules and you can even lazy load
individual components. In the bottom line,
though, is that it's a highly effective
technique for managing the size of your application
as it grows in complexity. Now that we have our
JavaScript bundle down to an acceptable size,
there's a tool in Angular that we can use to prevent
performance regressions in the future. When you run the
ng build command, you may notice output in
the console warning you that your bundle size has
exceeded a certain limit. The limit is based on a
budget that you can configure in the Angular json file. You can set up multiple
budgets where each budget has an object that has a type,
then a threshold for producing a warning in the console,
and another error threshold at which point the build will
fail if it exceeds that amount. That's especially useful
for a continuous integration where your build will
fail on the CI server rather than be pushed
out to production with a massive bundle size. Now let's go ahead
and recap what we've covered in this video. Tools like Lighthouse
and Source Map Explorer can help you find and understand
performance bottlenecks. If you run into issues
related to dependencies, then verify that your imports
are structured properly, and attempt to use
tree-shakeable libraries. But if the bundle size is
related to your own code, look into lazy loading
or code splitting to remove your own
JavaScript from the browser's critical path. And once the app
has been optimized, use CLI budgets to prevent
regressions in the future. For additional
examples and details, refer to the official
Angular documentation.