Webpack Bundle Analyzer report

Last but not least is the Webpack Bundle Analyzer, which gives a helpful breakdown about the anatomy of your bundles. It allows you to easily see what is taking up all those precious bytes in a visual manner. It really helps to find out what to remove or lazy load to decrease the initial page load time.

This tool helped us notice an embarrassing mistake — for some reason we were including our polyfills in both the main and polyfill bundles and therefore downloading them twice for no reason!

Optimization

This section is divided into different optimization techniques and tricks we have found and exploited. Some are hopefully useful to you but some are very specific to our application so they might not directly help you, but it might give you some ideas that you can apply to your problems.

The history of our Lighthouse scores

Once we started optimizing a funny thing happened. Our performance score actually went down at first! In the first reports we saw that some audits failed to complete because they timed out! That is, once we started making it faster the audits started working and lowered our score.

TypeScript imports

We noticed that some of our dependencies were pulling in modules and code that we didn’t need. Here’s some examples:

A validation utility was pulling in libphonenumber-js that weighs in about 110 KB and we weren’t even using it

Moment.js is quite big and by default includes all locales

A dependency was pulling in crypto-js but using only 2 functions

Lodash was importing everything even though we only imported the functions we were using

Our application is written with Angular and TypeScript. This means we have a tsconfig.json file that allows us to tell TypeScript how to compile our code. One immensely useful option is paths. It allows you to add nice shortcuts for imports.

But how did this help us reduce the bundle size? Well, what we did was create proxy files for the big dependencies and load those instead. Also using lodash-es to allow tree shaking to work properly and replacing Moment with moment.min.js which doesn’t include the locale files.

Lambda@Edge

We use a AWS Lambda@Edge service to run code at the CloudFront CDN edge locations. This allows us to modify the HTTP requests but keep latency down. Talking about CloudFront, make sure you have enabled HTTP2 to allow the browser to request multiple resources using the same TCP connection and prioritise them better.

For example we render the index.html with a Lambda function and embed a JSON payload to the HTML to avoid a request to the Grano Shop API before being able to render anything.

Cache Headers

As mentioned earlier we use AWS S3 and CloudFront to host our application. By default CloudFront caches resources, but it doesn’t send HTTP cache headers to the browser.

We have another Lambda function to add a HSTS security header to the requests telling the browser to always load the site over a secure connection. We modified the function to look at the request and if the file name contains a hash before the file extension it will add a cache header with a long expiry time. Another option would have been to set the headers in S3 as metadata, but that would have required more changes to our deployment scripts.

With Angular the hashing is a simple configuration option, but keep in mind that when using the Angular CLI the assets folder is copied as-is so keep your assets in the src folder preferably next to the component that requires it to get it hashed and therefore cached properly. For example we had some vector images that were referenced in the components and once we moved them in the component folders they were hashed properly during the build process.

Lazy Loading

Talking about Angular and the CLI make sure to enable lazy loading and lazy load your routes, that way the initial payload stays smaller as your application grows more and more complex and full of features. Since the generated bundles are hashed they are cached properly thanks to the cache headers from the earlier section.

Resource Hints

Another useful browser feature is the Resource Hints. It allows you to use HTTP headers or HTML link tags to tell the browser ahead of time that it will need a specific resource. The most common example is web fonts.

In the index.html renderer Lambda we load a list of web font files from S3 and add a preload tag for each to tell the browser to download the fonts as soon as it can before downloading and parsing the CSS. This allows the browser to render the text as soon as the CSS has been processed instead of waiting for the fonts to load. Note that web fonts always require the crossorigin attribute!