The ultimate guide to modern web application performance

Gaspar Nagy
Level Up Coding
Published in
38 min readOct 15, 2020

--

When I started to write this article I did not thought that it will be so long. Anyway I hope it will help you to boost your application performance. Good luck!

Table of contents

Here you can find only the main sections as the list of all the subsections is too long — there are ~107 of them.

1. Compression algorithms

2. Code splitting and lazy loading

3. Lazy or partial hydration

4. Tree shaking — dead code elimination

5. Javascript minification and optimization tools

6. Library swaps

7. Images

8. Fonts

9. Layout recalculation

10. External scripts

11. Pre — strategies

12. Protocols

13. Caching

14. Useful tools

1. Compression algorithms

To minimise the size of provided files to the client you can use different compression algorithms. Inside the Lighthouse there is a section for it called Enable text compression. Here you are able to find out what files you are providing with additional compression, or if your third party scripts are provided with compression.

https://web.dev/uses-text-compression/

1.1. Gzip

Gzip uses the LZ77 and Huffman coding compression techniques. Most web browsers and clients support it. You can setup your server to use dynamic or static compression. With dynamic compression your files are compressed by fly. With static compression you need to compress your files during the build.

The compression ratio for Gzip is around 44% as it is shown in next sections.

No compression

Transferred 445.57 KB and took 329 ms to load

https://www.pingdom.com/blog/can-gzip-compression-really-improve-web-performance/

Dynamic compression

Transferred 197.6 KB and took 281 ms to load

https://www.pingdom.com/blog/can-gzip-compression-really-improve-web-performance/

Static Compression

Transferred 197.2 KB and took 287 ms to load

https://www.pingdom.com/blog/can-gzip-compression-really-improve-web-performance/

To achieve even better static compression you can use advanced compressors like Zopfli or 7zip to generate your gzip files.

1.2. Brotli

Brotli is a lossless data compression algorithm developed by Google and works best for text compression. It uses the combination of a modern variant of the LZ77 algorithm, Huffman coding and second order context modeling.

According to certsimple,

  • Javascript files compressed with Brotli are 14% smaller than gzip.
  • HTML files are 21% smaller than gzip.
  • CSS files are 17% smaller than gzip.

Browser support for Brotli algorithm is a little bit limited as it does not support IE11, but don’t be discouraged there is a solution. You can have a setup with a fallback to gzip described in next section. Also NodeJs >11.7. has native support for brotli compression in zlib module.

Implement static Brotli compression in NodeJs server with fallback to gzip

The described functionality is currently useful for express servers where we do not have in the compression package the option for enabling dynamic brotli compression. The PR for accepting brotli compression in compression library is still open: https://github.com/expressjs/compression/pull/156.

To achieve this functionality we need to accomplish two steps:

  1. Generate gzip and brotli compressed files with webpack
  2. Send correct files accordingly to the request headers

For the first step we need to use two webpack plugins. The compression-webpack-plugin and the brotli-webpack-plugin.

The setup is as follows:

Now we need to accomplish the second step. Accordingly to what server we are using we should choose from the following tools:

The following example shows the usage of the express-static-gzip library.

By this two steps we are now able to send the correct compressed files accordingly to the request headers.

1.4. Compression libraries to use for different nodeJS servers

2. Code splitting and lazy loading

With code splitting your bundle can be split to smaller chunks. Then you can take control over the chunk loading using lazy-load techniques. This can be applicable on CSS files as well.

2.1. Dynamic imports

Webpack by default supports code splitting if you use dynamic imports. This helps you load your code on demand accordingly to some condition. The dynamic import returns a Promise.

If you are using babel you need make sure that it is able to parse the dynamic import syntax by using the @babel/plugin-syntax-dynamic-import.

2.2. Granular chunking

In Webpack 3 the CommonsChunkPlugin was introduced to make it possible to output modules shared between different entry points in a single chunk. This is a great feature, but it has some drawback. Modules not shared in every entry point gets also downloaded even if they are not used.

For this reason in Webpack 4 they removed that plugin in favor of a new one called SplitChunksPlugin. The default configuration for this plugin works well for most users. If you likes to implement more advanced configuration the Next.js team came out with a specific configuration called granular chunking. This means:

  • Large third-party modules (greater than 160 KB) are split into their own individual chunk
  • A separate frameworks chunk is created for framework dependencies (react, react-dom, etc.)
  • As many shared chunks as needed are created (up to 25)
  • The minimum size for a chunk to be generated is changed to 20 KB

This strategy provides the following benefits:

  • Improved page load times. The amount of unneeded or duplicated code for any entry point is minimised.
  • Improved caching during navigations. The cache invalidation for large libraries and framework dependencies is reduced as they are split into separated chunks.

After the Next.js team successfully integrated this configuration Gatsby used to follow the same approach.

The webpack config for splitChunks looks like it follows:

2.3. React built-in lazy loading

React.lazy and Suspense are the built-in functionalities what you can use inside your React application. React.lazy takes a function which should return the dynamically imported component. Then this lazy component should be rendered inside the Suspense component.

For example you can use this to load the application only if the user is logged-in otherwise show the login page:

Unfortunately the React.lazy and Suspense with dynamic imports are not working with server side rendering. For this purpose there are several other packages.

2.4. React lazy loading with SSR

The recommended package from the official React documentation is the loadable-components library.

To make the server side rendering working you need to install the following packages:

npm install @loadable/server && npm install --save-dev @loadable/babel-plugin @loadable/webpack-plugin# or using yarnyarn add @loadable/server && yarn add --dev @loadable/babel-plugin @loadable/webpack-plugin

Then you need to setup the babel config, the webpack config, the server side rendering and the client side initialization.

.babelrc

{"plugins": ["@loadable/babel-plugin"]}

webpack.config.js

Server side setup

Client side setup

Other similar libraries — some of them are not maintained anymore:

2.5. Native img and iframe lazy loading

The chromium based browsers has native support for image lazy loading which are out of screen. It loads only 2Kb from each img to obtain the necessary information only. You can achieve this by setting the loading attribute of each image to lazy.

The loading attribute supports three values:

  • lazy: lazy-load the content
  • eager: load the content right away
  • auto: browser will determine whether or not to lazy-load the content

Fortunately we have a solution for older browsers or not chromium based ones. We need to do a feature detection and use a polyfill for this feature.

To prevent loading the images above the fold (which are not in viewport) when the browser does not supports the loading attribute we set the data-src attribute on images instead of the src attribute. Then we check for the loading feature and decide whatever to load the needed polyfill or to set the src attributes from the data-src attribute on the mentioned elements.

As a polyfill you can use the lazysizes library.

Consider you have these images somewhere in your code:

Then you can do the following:

2.6. Images custom lazy loading

For this purpose you can use different libraries like the react-simple-img or the react-lazyload.

The usage of the react-simple-img is really easy:

Out of the box it has a support for lazy loading images below the fold — outside of the users viewport.

2.7. Intersection observer API

You can use the intersection observer API to lazy load resources when some part of the page enters the users viewport — usually by scrolling.

You can read about the intersection observer usage more deeply here:

2.8. Polyfills lazy loading

If you are importing your polyfills directly from core-js or you are setting the needed polyfills accordingly to browserlist using the @babel/preset-env plugin you can find your pollyfills inside your bundles. Even if they are not executed in every time, they are parsed — this means that the start of your app is postponed. We have few option how to avoid to load unnecessary code.

  1. You can use the https://polyfill.io/v3/ which inspects the User-Agent header and serves polyfills targeted specifically at the browser
  2. or you can lazy load your polyfills

The setup for lazy loading your polyfills can be as it follows:

2.9. CSS lazy loading

The CSS files are still just files so you are able to process them in the same way as you would do it with your polyfills, modules, chunks, etc. Also you can use some DOM events or the Intersection Observer API to determine when to load your specific CSS files.

It can look like this:

Or you can use a library like:

2.10. CSS code splitting

One of the best practices is to code split your CSS files by determining what selectors are used Above the fold — which means that load your CSS file for that part of the page which is visible for the user. After that load the remaining styles on user interaction.

This determination can be automated by using one of the following libraries:

3. Lazy or partial hydration

Hydration of a server side rendered React application can be heavy. In some cases you don’t even need to hydrate some part of your application as there is only static content with no interactivity.

For example you have a big menu which needs to be renderer because of the SEO, but it has over 1500 elements. The only interaction here is the dropdown opening. This functionality can be rewritten to use only css for dropdown and skip the hydration for the whole menu or hydrate only the interactive part but skip the hydration for the links themselves.

To achieve this you can use the react-lazy-hydration library.

The React team has plans to implement this functionality into to library itself:

With the current React version you can achieve this easily as it follows:

By this setup you skip the warnings of the SSR mismatches on client side and it completely skips the hydration.

If you are using Next.js there is the next-super-performance library for partial hydration and with some other features to boost you application performance.

4. Tree shaking — dead code elimination

4.1. Javascript

Tree shaking is a methodology or a term commonly used in javascript which means dead code elimination. It relies on static code analysis. Every bigger module bundler has this functionality implemented.

4.2. CSS

For eliminating the dead CSS code you can use some online tools or libraries which can be easily integrated with your building tools.

Do it manually

  1. Open Chrome DevTools
  2. Click on the three dots in top right corner
  3. Co to “More tools” and then select “Coverage”
  4. Click on the reload icon
  5. Select a CSS file from the Coverage tab which will open the file up in the Sources tab
https://www.keycdn.com/

UnusedCSS

  • online tool with paid plans
  • you should manually configure all pages where it should look for used css selectors

PurifyCSS

  • it is free online tool as well as it has a build time integration
  • you must manually specify which files to scan one by one

PurgeCSS

  • free library which has build time integration

CSS in JS libraries

There are tons of CSS in JS libraries, some of them are not maintained anymore. Here are some examples:

By using CSS in JS libraries you eliminate all the dead CSS code from your page, but keep in mind that some of those libraries can have a heavy runtime code which slows down your application or the hydration of your application.

5. Javascript minification and optimization tools

The following tools are reducing the javascript files size by removing the unnecessary characters, renaming variables or even executing some parts of your application to generate more efficient code.

5.1. Prepack

5.2. Closure

5.3. Packer

  • minifies your javascript code
  • copy paste service

5.4. Uglify JS

5.5. Terser

5.6. Minify

  • minifier of js, css, html and img files

5.7. Babel minify

  • minifier for babel
  • not production ready yet

5.8. Producify

  • CLI and node API toolkit

5.9. Snappy

  • compression and decompression library

6. Library swaps

6.1. Preact instead of React

Preact is basically a lightweight version of React. You can prefer using it when performance, speed and size are the priority.

Pros:

  • only 3KB in size when gzipped
  • faster than React (see these tests)
  • it is largely compatible with React, so it’s easy to swap React with Preact for an existing project for performance reasons
  • it has good documentation and examples available from the official website
  • it has a powerful and official CLI

Cons:

  • supports only stateless functional components and ES6 class-based component definition
  • no support for context
  • no support for propTypes
  • smaller community than React

6.2. Linaria instead of styled-components

Styled components can take a long time to hydrate the page with a lot of styles:

CPU i9 2,4Ghz 6x slowdown

The styled-components library comes with an overhead and runtime code which can slow down the page parsing a the hydration process as shown above.

Fortunately there is an alternative to this package with a similar API. It is called Linaria. The main difference between those packages is that Linaria extracts the CSS into separated files during build time and the styled-components extract the CSS during the runtime. This means a zero runtime for Linaria.

Advantages:

  • improved load time because CSS and JavaScript can be loaded in parallel, unlike runtime CSS in JS libraries where the CSS is in the same bundle as JS
  • improved runtime performance because no extra work such as parsing the CSS needs to be done at runtime
  • no style duplication between server side rendered CSS and the JavaScript bundle
  • since Linaria works at build time, you don’t need to have SSR setup to improve the time to first paint or for your page to work without JS

Limitations:

  • no IE11 support when using dynamic styles in components with styled, since it uses CSS custom properties
  • dynamic styles are not supported with css tag
  • modules used in the CSS rules cannot have side-effects. For example:
import { css } from 'linaria';
import colors from './colors';
const title = css`
color: ${colors.text};
`;

There should be no side-effects in the colors.js file, or in any file it imports. You should move helpers and shared configuration to files without any side-effects.

6.4. lodash-es instead of lodash

With more utilities we can save on size using lodash-es instead of lodash. Here is a table with a comparison of the mentioned packages.

Also some packages are using one or other so in webpack config make an alias to use lodash-es everywhere to prevent to bundle those libraries twice.

module.exports = {
resolve: {
alias: {
'lodash-es': 'lodash',
},
},
};

6.5. unfetch instead of axios, etc.

Axios may provide us a lot of functionality, but in most of the cases we are not even using it. In other hand the uncompressed size of this package is nearly 350Kb. This is a lot if you are using only the basic functionality from it.

The alternative for this package is unfetch which is only around 30Kb uncompressed and can provide everything we need. Its around a 90% save in size of this package.

For those of you, who are using isomorphic-fetch to achieve the same functionality for node environment and for frontend, there is the isomorphic-unfetch package.

6.6. date-fns instead of moment.js

For the first look if you check the minified files you can say that date-fns is bigger in size and the moment.js.

Yes, this is correct but as in the real world application you are not gonna use all the functionalities which are provided from those packages, so the tree shaking and uglification can do the job.

If we take some of the common functions from moment.js, lets say the format(), the duration(), the humanize() and the utc() and replace it with the equivalent functionality from date-fns we are going to end up with the following chunk size:

  • date-fns.js — 29.3 Kb
  • moment.js — 57.1 Kb

The repo with the example code is here.

7. Images

Accordingly to the HTTP Archive the images makes up around 30% of all requests. So let’s talk about how to optimize these resources.

7.1. Correct image dimensions

You should always use images which fully fill its container with no overflows with the exact dimensions. So for the container of 200px wide 200px high use a resized imaged with the same dimensions. With this approach the browser does not need to do extra work with the image resize.

7.2. Using srcset, sizes, and media attributes

Those attributes allows you to server different scaled images based on the size of the display. There is no need to serve the same image for a phone as for a computer.

srcset

The srcset attribute can be used in the <img> and <source> elements. In this case we rely solely on the browsers viewport.

sizes

The sizes attribute defines a set of media conditions which tells to the browser what image size to choose if certain conditions are true. You can combine it with the srcset.

media

The media attribute is similar to the sizes attribute. You can use it on <source> element when it is a child of the <picture> element.The difference is that with the media attribute you can define media conditions without the need to also define the viewport width. Then if condition is met the correct image size is used.

Pixel density descriptors

Besides the viewport it is also possible to to define what image is used based on the pixel density of the display. These descriptors are defined using 1x, 2x and 3x. So you can use larger images than the original while retaining the same dimensions.

7.3. Correct image formats

There are plenty of image formats which can be used. I am going to describe the most common ones.

JPEG

  • don’t support transparency
  • can support around 16 million colours
  • use it for all images that contain a natural scene or a photography where variation in colour and intensity is smooth

PNG

  • support transparency
  • PNG8 can support up to 256 colours
  • PNG24 can handle up to 16 million colours
  • use it for image that needs transparency or for images with objects with sharp contrast edges like logos

GIF

  • images support transparency
  • is limited to 256 colours
  • supports animations
  • use it for images that contain animations

WEBP

  • tries to combine the best parts from the already mentioned formats with a better compression ration
  • ~26% smaller than PNG
  • 25% — 34% smaller than JPEG

To serve the WEBP format with HTML you can use the <picture> and the <source> elements. The browsers which does not support those elements will skip them and will read only the <img> element.

AVIF

The AVIF image format is not really supported yet. This is because it is a really new technology you can be excited about. Accordingly to the ctrl.blog AVIF is:

  • ~20% smaller than WEBP
  • and ~50% smaller than JPEG

This format was developed by the Alliance for Open Media. It was create to be an open-source and royalty-free image format.

Even though the poor support, we can still use the format in native HTML with the <picture> element. The <picture> element allows for progressive support so from the provided list the browser will load the first that it supports.

Progressive JPEG

The baseline render of a JPEG image is to load it from up to bottom in a high quality.

The progressive render means that you first load a low quality JPEG image and then keep adding more pixels until the full quality image loads. This can significantly improve the user experience of your website.

You can achieve the progressive JPEG format by converting your standard JPEG images.

SVG

  • scalable vector format
  • works great for logos, icons, text, and simple images
  • The size of a simple SVG image (which can be converted to vectors) can be ~90% smaller compared to JPEG or PNG
example image where the saving can be up to 90%

7.5. Compress your images

Even after selecting the correct image format or the right dimensions there is still place for compression. One way is to remove all unnecessary meta data from the image without changing the image quality. This is called lossless compression.

On other hand also lossy compression can be used which removes almost all meta data and reduces the quality of the image. For the human eye this quality reduction is in most cases not noticeable and the saving on size can be up to 25%.

Some compression algorithms are:

7.6. Deliver images from cookie-free domain

Static images files should be delivered from a different domain or subdomain which does not use cookies (e.g. static.your-domain.com).

This approach gives you two benefits:

  • effective caching
  • less data transferred

7.7. Deliver images from a CDN

Content delivery networks (CDNs) are multiple servers around the world where your static files gets stored. On demand it delivers the files from the closest server available to the user.

Usually CDNs are paid services. Almost all of them also provides you an image processing option. This means that they automatically optimize your images for better performance.

For example on Cloudflare you can enable the polish option to compress your images.

7.8. Image modification tools

7.9. Webpack loaders

There are several tools which can be included into your building process. Let’s talk about some of them.

Image webpack loader

Put this loader in front of your url-loader or file-loader and it will compress and optimize your images.

SVG URL loader

The standard url-loader always uses base64 encoding for data-uri. The base64 encoded resources are on average 37% larger than original assets. The svg-url-loader encodes SVGs using UTF-8 encoding. The benefits are as it follows:

  • the result is smaller (up to 2 times smaller)
  • the result can be compressed better using gzip compression
  • browser parser UTF-8 encoded strings faster

8. Fonts

8.1. Custom web fonts

If you are serving fonts from your own server you are able to create a subset of characters you are using. It will help to load your fonts faster. For this purpose the following libraries can be used:

8.2. FOIT

Flash of invisible text (FOIT) may happen when it takes to long for a web font to load.

Font display property

To support the user experience and the first contentful paint use the font-display: swap; CSS property if you are using custom fonts. This will ensure to show default fonts while your custom font gets loaded. Google fonts also has support for this feature, simply add the &display=swap query parameter to the fonts URL. This will cause a flash of unstyled text (FOUT), but it is a small price to pay for better UX.

Font load events

There is another solution for this issue which is to detect when your fonts have been loaded and take control of the showing process. It can be done by using the CSS Font Loading API. As it is not supported in every browser you can use some alternatives:

To show some example I am going to use the fontfaceobserver library.

  • first — just load your fonts without any changes
  • second — create a CSS class:
  • third — set up Font Face Observer for each used font family and after they load set the body class:

Saving fonts into local storage

As the native cache gets flushed quite frequently, especially on mobile devices one good technique is to store your fonts into the clients localStorage.

We will use the same CSS classes as for the font face observer. Then we will use the following code:

Here we check for the support of localStorage and if is there a value for our custom key webFonts. If yes we inject a style tag with that value and if not we load our CSS with that font and insert that value to the local storage.

8.3. Fonts preloading

While you preload your fonts use the crossorigin=anonymous attribute for the link tag. Without that attribute, preloaded fonts will be ignored (https://github.com/w3c/preload/issues/32).

8.4. Google fonts preloading

If you are using google fonts better to self-host those fonts to load them faster — as the browser does not have to set up a new connection. You can use the google-fonts-webpack-plugin to download those fonts during the build time.

8.5. Font formats

TrueType Font (TTF) was developed in the late 80’s by Apple and Microsoft. It is the most common font format.

https://caniuse.com/ttf

Web Open Font Format (WOFF) was developed in 2009 for use in web pages. WOFF is basically OpenType or TrueType with compression and additional metadata.

https://caniuse.com/woff

Web Open Font Format 2 (WOFF2) has a better compression than WOFF.

https://caniuse.com/woff2

Embedded Open Type (EOT) is a compact form of OpenType fonts designed by Microsoft for use as embedded fonts on web pages.

https://caniuse.com/eot

What to choose?

The best option here is to provide the WOFF and WOFF2 formats with a fallback to web safe font. The web safe font is a preinstalled font on your users computer. You can check the coverage on cssfontstack.com.

The top 5 sans-serif web safe fonts are:

The top 5 serif web safe fonts are:

Providing the WOFF and WOFF2 formats with CSS :

And the fallback:

9. Layout recalculation

In the world of Firefox they call it DOM reflow and in the world of Chrome/Opera/IE/Safari it is Layout shift.

These terms refers to the same thing — layout recalculation. It is a user blocking operation which recalculates the dimensions and the position in document of an element.

It happens really often:

  • moving, animating, removing, inserting, updating a DOM element
  • modifying the content of the page — e.g. writing into an input box
  • changing a CSS style
  • scrolling, resizing the window
  • taking measurements of an element — e.g. offsetHeight

For more detailed list of javascript operations which triggers the layout recalculation check this list. Also there is a list of CSS properties.

9.1. Some tips how to avoid it

  • use fixed/absolute position for elements that change too often
  • change visibility instead of display
  • use flex box for layouts
  • use cssText for more than one layout change
  • use textContent instead of innerText

9.2. The contain CSS property

If there are a lot of style recalculations use for the recalculated element the container: content CSS property. It tells the browser that the element is isolated from the surrounding document so if something changes inside it there is no need to recalculate the whole layout just that element.

9.3. Cumulative layout shift — CLS

The following image describes the meaning of cumulative layout shift quite properly:

CLS is one of the Core Web Vitals defined by Google. It measures visual stability and the amount of unexpected layout shifts. Layout shifts happen whenever a visible entity changes its starting position between two frames.

As the image shows it can make the user experience really bad. It can be caused by:

  • FOIT — flash of invisible text
  • FOUT — flash of unstyled text
  • FOUC — flash of unstyled content
  • using images, embeds, banner ads, etc. without specific dimensions
  • any content that is injected dynamically

For improving the CLS or completely removing it from your first load, you can use tactics as:

  • showing white page until the application fully loads
  • implementing skeleton screens —page filled with boxes with predefined dimensions and some loading indicator
  • for the fonts use the display: swap; property and preload them by link tags
The implementation of the skeleton screen on Medium’s home page

10. External scripts

During the page parsing when the browser comes across a <script> element it starts to downloading and executing it. This stops the parsing process of the HTML page. This can lead to two major problems:

  • it is possible that the user see nothing but white page until those script loads and executes
  • the script which is executed cannot access the DOM elements below it as there are not parsed yet
  • the page is not interactive for a long period of time

On modern web pages usually the javascript is the heaviest thing — in the meaning of its size and the execution time. This is even more true for third party scripts like the GTM, the Adobe launch, marketing scripts, A/B testing libraries, embed videos and so on.

10.1. Defer and async script tags

To avoid the blockage of the HTML parsing it is possible to use the defer and the async attributes on external script tags. They are not working for inline scripts.

Normal (synchronous) execution

By default javascript files will interrupt the parsing of the HTML document in order for them to be fetched and executed.

One way to avoid issues with the white page and the DOM elements access, you can put your scripts right before the </body> tag, but in other hand the page loading time will still be longer.

Async

<script async src="script.js"></script>
The async attribute https://caniuse.com/script-async

The async attribute indicates to the browser that the script can be fetched in parallel with the HTML parsing, but after it is fetched start executing — during the execution the HTML parsing is paused.

https://flaviocopes.com/javascript-async-defer/

As the fetching and the execution is asynchronous the order of the async scripts are not preserved.

Defer

<script defer src="script.js"></script>
The defer attribute https://caniuse.com/script-defer

The defer attribute works similarly to the async attribute in the way of fetching, but the downloaded script gets executed only after the HTML parsing is finished, right after the domInteractive event. It also ensures the execution order of those scripts.

https://flaviocopes.com/javascript-async-defer/

What to choose?

My recommendation is to use the defer attribute for your own scripts. It ensures you faster page load and preserver the order of script execution.

Alternatives

There is a small library written by Kyle Simpson called LABjs.

This library provides more control over the loading process with full cross browser support and tries to download as much code in parallel as possible.

10.2. Custom defer script

To allowing the page to fully load and be interactive we can create a custom defer script for third parties lit the GTM, Adobe launch, etc.

The target is clear — load the third party script only after the page loads. This can be achieved by the following script:

10.3. Third party resource blocking

For more advanced manipulation with the script loading you can combine different techniques.

First you can block third party scripts using some library as yett then you can unblock those scripts when some user interaction is made or after the TTI (time to interactive).

TTI is calculated client-side by monitoring performance APIs and CPU active cycles. The advantage here is that Google Lighthouse is not monitoring the code executed after TTI. For the TTI events you can use the tti-polyfill library from google or some others, e.g. the time-to-interactive library.

10.4. Adaptive computation

One other method is the adaptive computation. Similarly to the TTI you can get the device capacity and the network speed. Accordingly to those metrics you can unblock the blocked third party resources.

https://caniuse.com/mdn-api_networkinformation

To obtain the needed metrics the Network Information API can be used. Unfortunately it has poor support in browsers. The solution for this is to use a polyfill:

There is also a library for React called react-adaptive-hooks. By this you can take control of loading your components on devices with slower connection or accordingly to the device speed.

Here you can check some examples of adaptive loading from google developers:

11. Pre — strategies

Let’s check how you can preload, prefetch some resources during parsing time or even whole pages where the user may navigate in the future to improve the user experience and the speed metrics for future pages.

11.1. Link tag pre — attributes

The easiest solution to apply a pre-strategy is to use specific values to the link element’s rel attribute.

DNS prefetch

<link rel="dns-prefetch" href="//example.com">

DNS prefetching is used to indicate an origin that will be used to fetch required resources and that the user agent should resolve as early as possible. This ensures that when the browser needs to start downloading some resources the DNS lookup will be finished or at least it will already be in progress.

https://caniuse.com/link-rel-dns-prefetch

Preconnect

<link rel="preconnect" href="https://example.com">

Similar to dns-prefetch but while preconnect it will do the DNS lookup, TLS negotiation, and the TCP handshake.

https://caniuse.com/link-rel-preconnect

Preload

<link rel="preload" href="image.png">

Tells the browser to preload specific static assets (js, css, images, etc.) which you know that it will need them but at that time during the parsing the browser knows nothing about that.

https://caniuse.com/link-rel-preload

There is a webpack plugin for generating the preload tags called webpack-preload-plugin.

Prefetch

<link rel="prefetch" href="image.png">

Prefetch also work only for static assets as the preload. The main difference here is that it prefetches the resources needed for a future page. It means that when the user navigates to a specific page where the previously prefetched asset is used, it gets delivered instantly.

The implementation of this feature may differ in different browsers. For example Firefox will only prefetch resources when the browser is idle. Also there is no same-origin restriction for link prefetching.

Be careful with the usage of this type of functionality. As it is downloading resources which are needed for some pages which may not be visited by the users browsing your site. It creates only extra bandwidth for them.

https://caniuse.com/link-rel-prefetch

Prerender

<link rel="prerender" href="https://example.com">

The prerender is more robust than the other mentioned options. You can imagine it as a hidden tab which pre-renders the whole page — the static assets are downloaded, the CSS is applied and the JS is executed. So when the user navigates to that page it appears instantly.

As for the prefetch the same rule applies here as well — the extra bandwidth is created sometimes unnecessarily. It can also be tricky when it comes to the google analytics — the events gets fired but the user may not visit that site.

https://caniuse.com/link-rel-prerender

11.2. Predictive prefetching

Predictive fetching is a technique to speed up navigations by cleverly predict pages where the user will go in the future and accordingly to that information download the next page resources. To achieve this functionality you can use a combination of some libraries we are going to dig into.

InstantClick

InstantClick is a library which is not maintained anymore. The main concept of it is to pre-load the link which is hovered by user. Usually between hovering and clicking is 200ms to 300ms.

Unfortunately if the server does not respond in 300ms, so the TTFB (time to first byte) is more than that, the user may not notice any difference.

Instant.page

Instant.page is doing the same thing as the InstantClick library, but this project is still maintained. It applies a 65ms delay — as they say, if the user hovers the page longer than 65ms there is a high chance that the user will also click on it. The rule with TTFB applies here as well.

Quicklink

Quicklink is a little bit different. It preloads all the links in the viewport, so the user does not have to hover any of the links. The functionality of Quicklink is as follows:

  • by using the Intersection Observer API it detects links within the viewport
  • waits until the browser is idle using requestIdleCallback
  • checks if the user isn’t on a slow connection (navigator.connection.effectiveType) or has data-saver enabled (navigator.connection.saveData)
  • prefetches URLs to the links (<link rel=prefetch> or XHR)

As this can be useful in some cases, be careful if you have a huge amount of links in the viewport, because Quicklink tells the browser to pre-fetch all the links at the same time. This will create a high server load.

Just imagine that you have 100 active users and let’s say 20 links in viewport. The server will load 2000 pages at that moment — if your server is not prepared to that it will crash and this is not what you want.

Also it is not pre-fetching the dynamically injected links as Quicklink finds all the link on the webpage only during the initial load.

For React you can use the react-quicklink library.

Flying Pages

Flying pages tries to combine the best from Quicklink and Instant.page. It detects all the links in the viewport then adds them to the queue. Links in the queue are processed by a limit of 3 requests per second — this prevents your server from crashing. Similarly to Instant.page it preloads the links on hover. When it detects a slow connection or crashed server it stops the preloading process.

Guess.js

Guess.js is a special library for automating the Predictive Prefetching developed by Google announced in 2018. The documentation says:

Guess.js provides a collection of libraries for enabling machine-learning driven experience for the Web.

It uses the Google Analytics API with machine learning models to predict the future pages where the users are likely to go from current page.

It has a webpack plugin as well:

11.3. Pre-heating

The concept is to separate code fetching from its execution. This tactic improves the initial paint and render time because the main thread is not requiring and executing code on the same tick.

This may get your paint and render times under 50ms.

12. Protocols

12.1. HTTP/1.1

This protocol is old and has newer alternatives. The downside are:

  • text protocol — files concatenation should be used to keep up
  • browsers open between four and eight connections per origin
  • one client-server request per connection
  • larger headers with no compression

12.2. HTTP/2

Prefer the usage of this protocol. The benefits against the HTTP/1.1 are:

  • binary protocol — no need to concatenate files
  • enables multiplexing — multiple requests and responses can be sent at the same time
  • uses more advanced header compression then HTTP/1.1 called HPACK
  • uses one connection per origin
  • enables server push
  • does not require the use of encryption (e.g. TLS), but there is no browser which supports HTTP/2 unencrypted
https://caniuse.com/http2

If you like to play around and see by your own what are the differences between HTTP/1.1 and HTTP/2 you can check it here:

12.3. QUIC over HTTP (draft of HTTP/3)

The main difference with the previous versions is that HTTP/3 uses QUIC instead of TCP. This increases the performance around fetching multiple objects simultaneously.

With HTTP/2, any packet loss in the TCP connection blocks all streams (it’s called Head of line blocking). Because HTTP/3 is UDP-based, if a packet gets dropped that only interrupts that one stream, not all of them.

It also offers 0 round trip (0-RTT) resumption, which means that subsequent connections are eliminating the TLS acknowledgement from the server when setting up the connection so they can start up much faster.

As the benefits of HTTP/3 may sound great, currently there is almost no support for it.

https://caniuse.com/http3

12.4. HTTPS and HSTS

As mentioned no browser supports the HTTP/2 with no encryption so it needs to be enabled.

In other hand the browser uses the HTTP protocol instead of the HTTPS as default. Usually this can be solved by redirecting the user server side to use the HTTPS protocol.

To boost the performance this redirect should be avoided. Rather that redirecting use HSTS, which stands for HTTP Strict-Transport-Security. You can implement it using response headers. This tells the browser that changing the protocol from HTTP to HTTPS works and asks the browser to do it for every request.

Also you can register your site to hstspreload.org which will ensure you that if a new user comes to your page it gets served using HTTPS.

https://caniuse.com/stricttransportsecurity

13. Caching

Caching is important for a few reasons:

  • helps to release the pressure on your database — server cache
  • provides better UX as the cached resources or content gets served immediately to the end user — browser cache

13.1. Server caching

Server caching is a technique used primarily for dynamic pages. You should cache the whole HTML page save it to some database and instead of rendering on a server and receiving all the needed information from database you load only the saved template and serve it to the user.

For this you can use different open source tools which supports this functionality. Some of them are:

13.2. HTTP Cache Headers

Using cache headers, you can control your caching strategy by establishing optimum cache policies that ensure the freshness of your content.

Cache-Control directives

  • public — the resource can be cached by any cache
  • private — indicates that the resource is user specific so still can be cached, but only on the user’s device
  • no-cache — the browser may cache a response, but must first submit a validation request
  • no-store — completely disables caching, the resource must be downloaded on every request
  • max-age=[seconds] — sets a time limit in seconds after what the cached resource needs to be refreshed
  • must-revalidate — indicates that the max-age and the Expires headers must be obeyed strictly — it is forbidden to serve a stale resource
  • no-transform—tells to any edge server (cache, proxy) to not make any modifications to the original asset — for example if the resource was received uncompressed the edge server is not allowed to send it compressed

Extension Cache-Control directives

  • immutable — once the resource gets cached the browser will never send a conditional request to check for changes
  • stale-while-revalidate=[seconds] — browser uses the cached files but updates them in the background after the expiration period in seconds pass
  • stale-if-error — similar to stale-while-revalidate but it only returns stale content if the origin server returns an error code
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control

Expires

Similar to Cache-Control: max-age, but here you need to set a concrete date instead of just seconds. It indicates the time of content expiration and removal. However the Expires header is ignored when a Cache-Control: max-age directive is provided.

ETag

Identifies the version of served content accordingly to a hash or token. If the token or hash changes the cache invalidates.

For example the Express library is automatically generating the ETag header.

Last-Modified

This header specifies the last time when the resource was modified. This may be used as part of the validation strategy to ensure fresh content.

Vary

This is most commonly used to tell caches to key by the Accept-Encoding header as well, so that the cache will know to differentiate between compressed and uncompressed content. When used properly, Vary can be a powerful tool for managing delivery of multiple file versions. For example, the header Vary: Accept-Language, User-Agent specifies that a cached version must exist for each combination of user agent and language.

13.3. What are the header usage recommendations?

  • do not use dynamically generated URLs for static assets
  • use CSS image sprites where possible — it reduces the roundtrips and allows the browser to cache that image for a long time
  • add unique identifiers to your files — like it is done when webpack generates chunks or modules, for which you can setup a file/chunk-name template. This helps you maximize the cache duration and when the content of file changes the identifier changes as well so the browser will download that new file
  • use the public cache control directive for all public assets
  • allow the browsers to cache also the user specific assets
  • if you have time sensitive content, like shopping cart, make exceptions for it as you don’t want to serve outdated content in critical situations. This can be achieved by no-cache or no-store directives.
  • always provide validators as the ETag or the Last-Modified headers

13.4. Service workers

Service workers are slightly related to progressive web applications as they provide a good caching functionality to pre-load content and serve it even if the user is offline. A service worker is actually a script which runs in the background of the browser so it does not affect the user experience while it is executed.

The following image describes how the service worker lifecycle works.

https://developers.google.com/web/fundamentals/primers/service-workers
  1. to install a service worker you need to register it — it is done by javascript
  2. after it is registered the browser starts the installing step — here you want to cache static assets
  3. if the caching fails, the worker will not be installed
  4. if everything is successfully cached the browser goes to activation step — here you want to revalidate your old cache
  5. after it is activated the service worker will control all the pages that fall under its scope — this occurs only after the second page load

To ease the process of configuring and generating service workers google came up with a collection of javascript libraries called Workbox.

It is mainly for creating a progressive web application, but you can use only a few packages to achieve your goals.

In other hand you can also use their webpack plugins package, the workbox-webpack-plugin, where you find two plugins. One (GenerateSW) is for generating a complete service worker to cache all your webpack assets and another one (InjectManifest) for generating only the assets list for pre-caching.

14. Useful tools

Performance and speed tests:

Thanks a lot for reading!

--

--

Leading my own software development company https://techmates.io. We focus on long term partnerships with our clients where we overtake part of the development.