Checklist for a better web application performance
Performance is crucial in today's web applications. A slow app feels buggy to the users and make 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 optimize too early
- Don't let performance ruin productivity
- Always check an optimization is efficient by measuring performance before and after it
- Define the performance metrics and objectives
- 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 really important in the content, what to load first and what to load later
- Make a performance budget
- Remember that this budget takes compression in account.
- If choosing between SPA frameworks, take in account features like server side rendering; these features will be hard to add later
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
- Optimize manually important images or script their optimization
- Lazy load images
- Minimize the source code
- If the bundled code file is too big, use code splitting to load only what's needed first and lazy load the rest
- Cache requests client side using service workers
- Bundle common images using CSS sprites
- 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 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.
- Defer scripts execution, especially social media buttons and ads
- Use server side rendering if making an SPA
- Use WOFF2 with fallback to WOFF and OTF
- If animating with JS, use requestAnimationFrame instead of
- Avoid animating during high network activity; for example wait for page is fully loaded
User perceived performance is often disregarded but can be more important than actual performance. These techniques allow 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 user's action, for example transition from a page to another
- 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 by a canvas filled with its main color.
- We can also simply use a low-quality version of it.
- When choosing a web framework / library / language, take in 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 an efficient resources management, e.g. using a connections pool or an event loop?
- Provide batch queries / transactions instead of making the client send multiple requests
- Identify & optimize slow resources
- Parallelize slow tasks
- Use relevant data structures
- Don't overuse serialization
- Generate static content when deploying so that it will be computed only once
- 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
- 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
- Fail fast by validating request inputs as soon as possible
- Use the circuit breaker pattern to avoid waiting 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
- 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.
- Identify & optimize critical and slow queries (e.g. code that produces n+1 queries)
- In most SQL databases,
EXPLAINcan help by showing the execution plan for a query
- In PostgreSQL,
EXPLAIN ANALYZEcan help further by executing the explained query
- Once we are sure the used DBMS is the good one for the 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 where there is less load on the database (at night for example) to save resources when needed
- 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
- Measure server side performance; this is usually already done by the web framework
- Measure client side performance by country; results may hugely differ from one to another
- Keep track of queries to the databases to ease slow queries discovery
- Keep the dependencies up-to-date as their performance is often improved by their maintainers