We see performance as an integral part of the quality of the software that we build. There's no need to re-emphasize the importance of performance in the modern web. We strongly belive that it is our duty to provide to our users a slick and fast UX, so we try to include performance optimizations in our regular working process.
Here are a couple of areas that revolve around web performance. They will be later grouped into front-end, back-end and tooling, but for now let's look at where we can actually improve the performance of our web applications.
- Optimizing Critical Rendering Path
- Keeping the bundle size to a minimum
- Image Optimization
- Interactivity and Animations
- Backend & Server Optimizations
- Performance Tests and Tools
All these are considered in the realm of modern web development with frontend frameworks rendering complex applications and thin APIs serving the data.
When we talk about the Critical Rendering Path, or CRP, we refer to the initial load time of a web application. We ask a few questions:
- How fast does the user see something?
- How fast does the user see something relevant?
- How fast can the user interact with the page?
All these are important but if we focus on the rendering part and on the moment when the users sees the relevant content, then optimizing CRP is about shortening the delay of the first meaningful paint. A few points to consider are:
Modern JS frameworks (Angular, React, Vue) support server side rendering, allowing the application to become universal. Here's a guide to implement server side rendering in React. SSR minimizes the time you wait for the first meaningul paint because your components render on the server and the client only needs to load HTML and CSS. This also ensures that we can defer loading javascript files.
If the user doesn't need JavaScript to see the initial render, we can use the defer
attribute on script tags.
<script type="text/javascript" src="/app.bundle.js" defer/>
Further reading on defer
and async
.
Defering resource execution is one thing, but in certain scenarios you may want to preload resources that you know will be used in the initial render. Link Preload is a nice feature that you can include in your <head>
tag. Normally you would preload: styles, fonts or even scripts, based on how fast you need them during the initial render phase.
An example would be
<link rel="preload" href="/app.bundle.css" as="style">
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/app.bundle.js" as="script" />
Also make sure you keep all your critical resources on your domain/servers, so you don't depend on 3rd parties for your critical render.
In order to avoid a roundtrip to fetch the initial css, there are certain scenarios in which you can inline the styles needed to render the initial screen. The browser only needs the styles which are above-the-fold, or inside the user viewport. There are multiple tools that help you extract above the fold css:
When you're using webfonts, by default, browsers will not show the text until the webfonts are loaded. Using font-display: swap
or optional
can signal browsers to use a system font until webfonts are there. More about font-display
and the possible values on css-tricks.
Make sure you don't introduce too many nodes in your HTML, try to keep it clean and neat. Google Lighthouse suggests limiting your DOM Nodes to ~1500 per page.
Anything from Google Analytics to Helpdesk or other 3rd party services you are using in your website tend to delay the initial render if they are not properly delayed. Ideally, you should make sure all 3rd parties have the lowest priority for downloading and execution.
Further reading on 3rd party JS optimization
Make sure you also minimize CSS, especially since that CSS will always delay the initial render.
When using webpack, make sure you disable module transpilation, so webpack can automatically do tree shaking based on ES Modules syntax.
If you bundle size is still large, one option is to split chunks of your code and lazy load them only when you need them. This is very easily done with the dynamic import syntax and webpack. Further more, when using React, you can use react-loadable to abstract the complexity of the dynamic module import.
Make sure you're using a good chunk of a library if you import it in your code. Otherwise you will end up with a lot of unused code in your final bundle that might not be easily tree shaken because of the way in which the library is built. A few examples:
- Prefer the native array functions instead of adding lodash for a few operations
- Avoid using moment.js (69 kB minified + gziped!) or make sure you don't load all locales for it.
- Create your custom components if your use case is very limited compared to a fully featured component.
- Use babel-preset-env + browserslist to specify browser support and limit polyfills
- Use bundlesize in your CI to define hard size limits for your bundle.
- Generate and inspect bundle size with webpack bundle analyzer or other similar tools
- Measure code coverage with Chrome DevTools
A few practices to improve the performance on website that rely heavily on images
- Defer image loading based on their importance / visibility
- Serve images according to screen size - you don't need a 1080p image on a mobile device.
- Use modern encoding formats when possible, like Webp
- Inline small images as SVGs
- Don’t render offscreen images, use IntersectionObserver to determine if the image should be fetched
After optimizing the critical rendering path and the initial render, it's time to look at how the user interacts with your application. Using the following practices will help you maintain your rendering at 60fps:
- Use passive event listeners
- Use opacity and transforms to create css animations, avoiding unnecessary reflows.
- Use will-change when you know the element will be animated
- Constantly check performance with the Chrome DevTools Audit tab.
And a few tips to use on the backend and the server to help the web app performance overall:
- Don’t rely on ORMs for performant queries
- Use indexes on all columns you query
- Use pagination for long lists
- Rely on server caching when possible
- Use http/2
- Use gzip/deflate
- Rely CDNs to deliver assets (js, css, images) based on region and availability
Finally, some tools that help you on the way:
- Google lighthouse (performance, seo, accessibility, etc.)
- Google devtools / performance tab
- GTMetrix (loading times, performance charts)
- Locust.io (load testing, requires some python knowledge)
- jmeter (load testing)
- Apache Benchmark (load and perf testing)