Performance Checklist Checklist for a better web application performance
Performance is crucial in today's web applications. A slow app feels buggy to the users and makes them flee it. Although performance is such an important topic, there are so many optimization vectors that it's easy to forget some of them. The intent of this checklist is to gather performance practices to apply when developing a web application.
While we apply these practices, we should always keep the following rules in mind:
Don't let performance ruin productivity
Always check an optimization is efficient by measuring performance before and after it
Objectives
Define the performance metrics and objectives
The RAIL model is generally a good model to start with.
If speed is an advantage we want to have against competitors, know that users usually will feel we are faster if we are at least 20% faster than them.
Plan out a loading sequence ; this way we can define early what is important in the content, what to load first and what to load later
Make a performance budget
Remember that this budget takes compression into account.
Frontend
If choosing between SPA frameworks, take in account features like server side rendering ; these features will be hard to add later
Take in account how much every library / framework will take on the performance budget ; don't use too much of them
Bundlephobia can help we estimate the size of a new dependency.
Make sure we need custom fonts before using them
Consider technologies like AMP and Instant Articles , but be aware of their pros and cons
Keep also in mind these solutions are not mandatory to obtain correct performance
Optimize images
Images represent in average ~60% of a page's weight, thus it's an important part to optimize.
Use WebP compression format for browsers that accept it
Use responsive images with img
's' srcset
and size
attributes
Optimize manually important images or script their optimization
Replacing animated gifs by videos can reduce their size dramatically (details here )
Reduce code size
Use tree shaking (e.g. with webpack ) to remove unused code
If the bundled code file is too big, use code splitting to load only what's needed first and lazy load the rest
Serving ES2015 code to browsers supporting it and ES5 code to browsers that don't, using type="module"
and nomodule
, can improve the bundle size and parsing time (details here )
Reduce number of requests
Replace third parties components (like sharing buttons, maps...) by static components
Cache requests client side using service workers
Bundle common images using CSS sprites
Prepare next requests
We can use prefetching to prepare the browser to next requests and make them faster or even instant. This article is a few old but explains well the following techniques.
Use dns-prefetch to resolve the domain of services we may need
Use preconnect to do DNS lookup, TCP handshake and TLS negotiation with services we know we will need soon
Use prefetch to request specific resources that are likely to be needed soon, like images and scripts
This technique makes an especially good combination with lazy loading.
Use preload to request specific resources that will be needed in the current page, e.g. <script>
tags at the end of the body
The difference between prefetch
and preload
is explained here .
Use prerender to request and prerender pages that are very likely to be visited soon, like homepage or user dashboard
Be aware that this technique is quite heavy, make sure we know what we are doing before applying it.
Optimize time to rendering
Ideally the critical code should fit in 14KB in order to be server in the first TCP roundtrip (why 14KB? ). These techniques help to achieve this goal.
Inline critical CSS in the <head>
of the pages
If critical CSS is inlined, we can then defer the CSS files loading
When the CSS files are loaded, set a cookie to avoid using inlined critical CSS anymore ; this way it will benefit from browser cache
More explanations on this technique here
Defer scripts execution , especially social media buttons and ads
Defer fonts loading ; the technique is explained here
Use server side rendering if making an SPA
Optimize fonts
Use WOFF2 with fallback to WOFF and OTF
Make animations smooth
Prefer animating using CSS' transform and opacity ; more explanations here
If animating with JS, use requestAnimationFrame instead of setInterval
Avoid animating during high network activity ; for example, wait till the page is fully loaded.
User perceived performance
User-perceived performance is often disregarded but can be more important than actual performance. These techniques allow us to improve it.
We should use a loader only on "long" / "heavy" tasks , i.e. tasks the user can imagine they are heavy (e.g. account creation)
Instead, we can use animations to illustrate the transitions following the user's action, for example, the transition from one page to another
Show app shell before content if needed; more explanations here
If using JPG images, we can use progressive JPGs to improve their loading perception
If not using especially JPG, we can replace the images by cheaper components until they are loaded
We can replace an image with a canvas filled with its main colour.
We can replace an image with a very lightweight, blurred version of it. This efficient technique is explained in this article from Facebook.
We can also simply use a low-quality version of it.
Make an optimistic UI to make some interactions feel instant; a quick explanation can be found here
Backend
When choosing a web framework/library/language, take into account the following points:
How fast is the library / underlying language (but be aware that benchmarks are usually biased)?
How easy will it be to handle concurrency ?
Does it allow efficient resource management , e.g. using a connections pool or an event loop?
General
Provide batch queries/transactions instead of making the client send multiple requests
Identify & optimize slow resources
Use relevant data structures
Don't overuse serialization
Generate static content when deploying so that it will be computed only once
If possible, use jemalloc to improve memory allocation
Cache
Use HTTP cache ; the different caching techniques are explained in this guide
Consider using ESI if the app is not a SPA
Cache calls to other services using Redis, a reverse proxy...
Cache data slow to compute and memoize slow functions
Don’t loose time with non-urgent tasks
Defer tasks to workers using a queue or use event-based patterns like Event Sourcing
Use UDP for immediate but not vital tasks like logging
Don’t lose time with errors
Fail fast by validating request inputs as soon as possible
Use the circuit breaker pattern to avoid waiting for timeout when needing another service
On sensible resources, detect suspicious requests as attacks before actually handling them ; attacks can cause heavy resources consumption
For example, detect a login request as part of a brute force attack before fetching user data from the database
Database
Make sure we use the right DBMS for the needs
Usually, a relational database will cover most needs, but in some cases, a NoSQL database may be a better fit.
First, use indexes smartly
Identify & optimize critical and slow queries (e.g. code that produces n+1 queries)
In most SQL databases, EXPLAIN
can help by showing the execution plan for a query
In PostgreSQL, EXPLAIN ANALYZE
can help further by executing the explained query
If using pagination, use the last row instead of offset
as a starting point; more explanations here
Once we are sure the used DBMS is the good one for our needs, take advantage of its advanced features (e.g. materialized views in Oracle, hyperloglogs in Redis...)
Don’t use ORM for complex queries , unless you know what you’re doing
If possible, defer heavy tasks to moments of the day when there is less load on the database (at night for example) to save resources when needed
If possible, enable jemalloc to improve memory allocation
If using UUIDs, reorder them before storing them; more explanations here
Try different storage engines
On many DBMSs RocksDB often gives interesting results.
Network & Infrastructure
Serve static content using a CDN to shorten the distance between the client and the server
When using the CDN take into consideration features like HTTP/2 support, compression...
Deploy the app on several datacenters , also to shorten the distance between the client and the server
Serve resources compressed using Brotli if it's supported, Gzip otherwise
Compress resources that are rarely changed using Zopfli
Use HTTP/2 and its features like server push and enable HPACK to compress HTTP headers
Use OCSP stapling to fasten TLS shaking
Avoid redirects as they increase the number of needed requests
If using a microservices architecture, bring services needing each other often closer , ideally in the same machine
Kubernetes' pods can help achieve this goal
Measuring
Measure server-side performance; this is usually already done by the web framework
Measure client-side performance; tools like Web Page Test can help
Measure client-side performance by country ; results may hugely differ from one to another
Load test the servers as they probably won't have the same perfs under 10rps and 1000rps
Keep track of queries to the databases to ease slow queries discovery
Miscellaneous
Keep the dependencies up-to-date as their performance is often improved by their maintainers
Take into account the Save-Data
request header to serve lighter assets to clients with limited resources