Building High-performance Apps: A Checklist To Get It Right
An app is only as good as the problem it solves. But your app’s performance can be extremely critical to its success as well. A slow-loading web app can make users quit and try out an alternative in no time. Testing an app’s performance should thus be an integral part of your development process and not an afterthought.
In this article, we will talk about how you can proactively monitor and boost your app’s performance as well as fix common issues that are slowing down the performance of your app.
I’ll use the following tools for this blog.
- Lighthouse — A performance audit tool, developed by Google
You can find similar tools online, both free and paid. So let’s give our Vue a new Angular perspective to make our apps React faster.
First, we need to understand which metrics play an important role in determining an app’s performance. Lighthouse helps us calculate a score based on a weighted average of the below metrics:
- First Contentful Paint (FCP) — 15%
- Speed Index (SI) — 15%
- Largest Contentful Paint (LCP) — 25%
- Time to Interactive (TTI) — 15%
- Total Blocking Time (TBT) — 25%
- Cumulative Layout Shift (CLS) — 5%
By taking the above stats into account, Lighthouse gauges your app’s performance as such:
- 0 to 49 (slow): Red
- 50 to 89 (moderate): Orange
- 90 to 100 (fast): Green
I would recommend going through Lighthouse performance scoring to learn more. Once you understand Lighthouse, you can audit websites of your choosing.
I gathered audit scores for a few websites, including Walmart, Zomato, Reddit, and British Airways. Almost all of them had a performance score below 30. A few even secured a single-digit.
To attract more customers, businesses fill their apps with many attractive features. But they ignore the most important thing: performance, which degrades with the addition of each such feature.
As I said earlier, it’s all about the user experience. You can read more about why performance matters and how it impacts the overall experience.
Now, with that being said, I want to challenge you to conduct a performance test on your favorite app. Let me know if it receives a good score. If not, then don’t feel bad.
Follow along with me.
Let’s get your app fixed!
If you’re still reading this blog, I expect that your app received a low score, or maybe, you’re just curious.
Whatever the reason, let’s get started.
Below your scores are the possible opportunities suggested by Lighthouse. Fixing these affects the performance metrics above and eventually boosts your app’s performance. So let’s check them out one-by-one.
Here are all the possible opportunities listed by Lighthouse:
- Eliminate render-blocking resources
- Properly size images
- Defer offscreen images
- Serve images in the next-gen formats
- Enable text compression
- Preconnect to required origins
- Avoid multiple page redirects
- Use video formats for animated content
A few other opportunities won’t be covered in this blog, but they are just an extension of the above points. Feel free to read them under the further reading section.
Eliminate Render-blocking Resources
This section lists down all the render-blocking resources. The main goal is to reduce their impact by:
- removing unnecessary resources,
- deferring non-critical resources, and
- in-lining critical resources.
To do that, we need to understand what a render-blocking resource is.
Render-blocking resource and how to identify
As the name suggests, it’s a resource that prevents a browser from rendering processed content. Lighthouse identifies the following as render-blocking resources:
- A <script> tag in <head> that doesn’t have a defer or async attribute
- A <link rel=”stylesheet”> tag that doesn’t have a media attribute to match a user’s device or a disabled attribute to hint browser to not download if unnecessary
- A <link rel=”import”> that doesn’t have an async attribute
To reduce the impact, you need to identify what’s critical and what’s not. You can read how to identify critical resources using the Chrome dev tool.
Classify resources as critical and non-critical based on the following color code:
- Green (critical): Needed for the first paint.
- Red (non-critical): Not needed for the first paint but will be needed later.
Now, to eliminate render-blocking resources:
Extract the critical part into an inline resource and add the correct attributes to the non-critical resources. These attributes will indicate to the browser what to download asynchronously. This can be done manually or by using a JS bundler.
Webpack users can use the libraries below to do it in a few easy steps:
- For extracting critical CSS, you can use html-critical-webpack-plugin or critters-webpack-plugin. It’ll generate an inline <style> tag in the <head> with critical CSS stripped out of the main CSS chunk and preloading the main file
- For extracting CSS depending on media queries, use media-query-splitting-plugin or media-query-plugin
- And finally, for the main chunk, vendor chunk, or any other external scripts (included in index.html), you can defer them using script-ext-html-webpack-plugin
There are many more libraries for inlining CSS and deferring external scripts. Feel free to use as per the use case.
Use Properly Sized Images
This section lists all the images used in a page that aren’t properly sized, along with the stats on potential savings for each image.
How Lighthouse Calculate Oversized Images?
Lighthouse calculates potential savings by comparing the rendered size of each image on the page with its actual size. The rendered image varies based on the device pixel ratio. If the size difference is at least 25 KB, the image will fail the audit.
DO NOT serve images that are larger than their rendered versions! The wasted size just hampers the load time.
- Use responsive images. With this technique, create multiple versions of the images to be used in the application and serve them depending on the media queries, viewport dimensions, etc
- Use image CDNs to optimize images. These are like a web service API for transforming images
- Use vector images, like SVG. These are built on simple primitives and can scale without losing data or change in the file size
You can resize images online or on your system using tools. Learn how to serve responsive images.
Learn more about replacing complex icons with SVG. For browsers that don’t support SVG format, here’s A Complete Guide to SVG fallbacks.
Defer Offscreen Images
An offscreen image is an image located outside of the visible browser viewport.
The audit fails if the page has offscreen images. Lighthouse lists all offscreen or hidden images in your page, along with the potential savings.
Load offscreen images only when the user focuses on that part of the viewport. To achieve this, lazy-load these images after loading all critical resources.
There are many libraries available online that will load images depending on the visible viewport. Feel free to use them as per the use case.
Lighthouse identifies all the CSS and JS files that are not minified. It will list all of them along with potential savings.
Do as the heading says!
Minifiers can do it for you. Webpack users can use mini-css-extract-plugin and terser-webpack-plugin for minifying CSS and JS, respectively.
Serve Images in Next-gen Formats
Following are the next-gen image formats:
- JPEG 2000
- JPEG XR
The image formats we use regularly (i.e., JPEG and PNG) have inferior compression and quality characteristics compared to next-gen formats. Encoding images in these formats can load your website faster and consume less cellular data.
Lighthouse converts each image of the older format to Webp format and reports those which ones have potential savings of more than 8 KB.
Convert all, or at least the images Lighthouse recommends, into the above formats. Use your converted images with the fallback technique below to support all browsers.
Enable Text Compression
This technique of compressing the original textual information uses compression algorithms to find repeated sequences and replace them with shorter representations. It’s done to further minimize the total network bytes.
Lighthouse lists all the text-based resources that are not compressed.
It computes the potential savings by identifying text-based resources that do not include a content-encoding header set to br, gzip or deflate and compresses each of them with gzip.
If the potential compression savings is more than 10% of the original size, then the file fails the audit.
Webpack users can use compression-webpack-plugin for text compression.
The best part about this plugin is that it supports Google’s Brotli compression algorithm which is superior to gzip. Alternatively, you can also use brotli-webpack-plugin. All you need to do is configure your server to return Content-Encoding as br.
Brotli compresses faster than gzip and produces smaller files (up to 20% smaller). As of June 2020, Brotli is supported by all major browsers except Safari on iOS and desktop and Internet Explorer.
Don’t worry. You can still use gzip as a fallback.
Preconnect to Required Origins
This section lists all the key fetch requests that are not yet prioritized using <link rel=”preconnect”>.
Establishing connections often involves significant time, especially when it comes to secure connections. It encounters DNS lookups, redirects, and several round trips to the final server handling the user’s request.
Establish an early connection to required origins. Doing so will improve the user experience without affecting bandwidth usage.
To achieve this connection, use preconnect or dns-prefetch. This informs the browser that the app wants to establish a connection to the third-party origin as soon as possible.
Use preconnect for most critical connections. For non-critical connections, use dns-prefetch. Check out the browser support for preconnect. You can use dns-prefetch as the fallback.
Avoid Multiple Page Redirects
This section focuses on requesting resources that have been redirected multiple times. One must avoid multiple redirects on the final landing pages.
A browser encounters this response from a server in case of HTTP-redirect:
A typical example of a redirect looks like this:
example.com → www.example.com → m.example.com — very slow mobile experience.
This eventually makes your page load more slowly.
Don’t leave them hanging!
Point all your flagged resources to their current location. It’ll help you optimize your pages’ Critical Rendering Path.
Use Video Formats for Animated Content
This section lists all the animated GIFs on your page, along with the potential savings.
Large GIFs are inefficient when delivering animated content. You can save a significant amount of bandwidth by using videos over GIFs.
Consider using MPEG4 or WebM videos instead of GIFs. Many tools can convert a GIF into a video, such as FFmpeg.
Use the code below to replicate a GIF’s behavior using MPEG4 and WebM. It’ll be played silent and automatically in an endless loop, just like a GIF. The code ensures that the unsupported format has a fallback.
Note: Do not use video formats for a small batch of GIF animations. It’s not worth doing it. It comes in handy when your website makes heavy use of animated content.
I found a great result in my app’s performance after trying out the techniques above.
While they may not all fit your app, try it and see what works and what doesn’t. I have compiled a list of some resources that will help you enhance performance. Hopefully, they help.
Do share your starting and final audit scores with me.
Happy optimized coding!
Learn more — web.dev
Other opportunities to explore:
- Remove unused CSS
- Efficiently encode images
- Reduce server response times (TTFB)
- Preload key requests
- Reduce the impact of third-party code