Frontend Performance Handbook — Best Practices and Strategies

Frontend performance optimization is one of those tricky areas that even senior developers often struggle with. In this blog, I’ll break it down in-depth, covering everything from fundamentals to advanced strategies. Whether you’re a junior or a seasoned developer, there’s something here for you to learn.
The three key areas for optimizing a web app’s performance are:
- Client-Side — Optimizing how the browser processes and renders content.
- Network Performance (File Transfer) — Reducing load times through efficient data transfer.
- Frontend Infrastructure & Build Optimization — Enhancing performance with CDNs, caching, and efficient build processes.
Client Side Performance Optimization
There are several client-side optimization techniques:
1. Asset Optimization (Optimizing media and static files)
Below are some assets optimizing techniques:
a) Use Modern Image Formats: WebP and AVIF are lightweight alternatives to traditional formats like JPG and PNG. These formats can achieve smaller file sizes without compromising image quality, making them ideal choices for faster loading times. Additionally, consider compressing your images to reduce file size without sacrificing quality. For images with fewer colors, SVG is an excellent option since it’s extremely lightweight.
b) Implement Responsive Images (srcset, sizes): The srcset attribute allows you to define multiple image sizes for different screen resolutions, so the browser can select the best one based on the device’s display. The sizes attribute helps the browser understand how much space the image will occupy, allowing it to choose the right size accordingly. Together, these techniques ensure that images load faster and look sharper across different devices.
<img src="image.jpg"
srcset="image-small.jpg 600w, image-large.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="A responsive image" />
c) Font Optimization: Use modern font formats like WOFF2 for better compression and faster loading compared to older formats like TTF or OTF. Set the font-display property to swap or fallback to avoid the “flash of invisible text” (FOIT), which ensures that text is displayed with a fallback font while the custom font loads. Additionally, use the <link rel=”preload”> tag to preload essential fonts, so they start downloading earlier, reducing the time before they’re available for rendering.
d) Deliver Assets via a CDN: A Content Delivery Network (CDN) distributes your files (images, scripts, styles) across multiple global servers. This allows users to download assets from a server closer to their location, boosting load speed and enhancing global performance.
2. Code Optimization
Below are some code optimization techniques:
a) Code Splitting: Implement code splitting to load only the necessary JavaScript for each route. This reduces the amount of JavaScript the browser has to download, improving load times. In React, you can use React.lazy to split your code.
const About = React.lazy(() => import('./About'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<About />
</Suspense>
);
}
In this example, the About component will only load when it’s needed (i.e., when the user navigates to it), reducing the initial load time.
b) Lazy Loading: Implement lazy loading for components and images below the first fold. This means only the content visible in the user’s viewport will be loaded initially, and the rest will load as the user scrolls. This improves performance and reduces initial page load time.
const App = () => (
<div>
<img src="image.jpg" alt="Lazy loaded image" loading="lazy" />
</div>
);
This ensures that the image is only loaded when it comes into view. For a particular component or section to lazy-load, you can create your custom component like “LazyLoad”, which uses the Intersection Observer to detect if the component is in the visible area or close to it, and then renders it. I see many developers relying on third-party packages to lazy-load sections; however, you can achieve this yourself, saving the need to add extra libraries or unwanted chunks.
c) Avoid Unnecessary Re-renders: Hooks like useRef()
are often underused but can significantly improve performance. When tracking state that doesn't need to trigger re-renders (e.g., form validation or DOM references), use useRef()
instead of useState()
. useState()
should only be used when you need to update the UI. Always ask yourself, "Do I need to update the UI?" before using useState()
. If the answer is yes, then use it; otherwise, opt for a simple variable or useRef()
.
import React, { useRef } from 'react';
const FormComponent = () => {
const inputRef = useRef();
const handleSubmit = () => {
// Access value without causing a re-render
console.log(inputRef.current.value);
};
return (
<div>
<input ref={inputRef} placeholder="Enter text" />
<button onClick={handleSubmit}>Submit</button>
</div>
);
};
export default FormComponent;
d) Optimize React Components: Use performance-focused hooks like useMemo()
, useCallback()
, and React.memo()` to prevent unnecessary re-renders and optimize rendering performance. Each of them has its unique use case, which you can read about in the articles I have provided.
e) Minify HTML, CSS, and JavaScript: Use bundlers like Webpack or Vite to minify your code, reducing file sizes and speeding up delivery over the network. Minification removes unnecessary spaces, and comments, and simplifies variable names. I will discuss more about it in the Build Optimization section.
module.exports = {
mode: 'production',
};
If you’re using Webpack 5 in production mode, the minification process will automatically be handled unless you override the configuration.
f) Optimize API Calls: Whenever you integrate an API, make it a habit to open the Network tab in Chrome DevTools to check if the API is being called more than once unnecessarily. If you notice multiple redundant calls, you can use AbortController
to cancel the unwanted request. Additionally, if you're making multiple independent API calls, use Promise.allSettled()
to run them concurrently and handle all responses at once.
Example using AbortController
to cancel redundant API calls:
const controller = new AbortController();
const signal = controller.signal;
// Start the API request
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Request failed', error));
// Abort the request if needed (e.g., before making a new one)
controller.abort();
Example using Promise.allSettled()
to make concurrent API calls:
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
];
const results = await Promise.allSettled(urls.map(url => fetch(url)));
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
const data = await result.value;
console.log(`Data from URL ${index + 1}:`, data);
} else {
console.error(`Error fetching from URL ${index + 1}:`, result.reason);
}
});
g) Semantic HTML: Using appropriate semantic tags not only provides SEO benefits but also eliminates the need for excessive <div>
and <span>
, making the DOM tree shallower. A less complex DOM improves rendering speed and simplifies JavaScript DOM manipulation. This video demonstrates it well.
3. Rendering Patterns
A rendering pattern refers to the way in which HTML, CSS, and JavaScript code are processed and rendered in a web application.
When building a web app, one of the key decisions you’ll face is: “Where and how should I render the content?” Should it be done on the web server, during the build process, at the edge, or directly in the client’s browser? Should the content load all at once, in parts, or progressively?
Choosing the right rendering pattern can optimize build times, enhance loading performance, and keep processing costs low. Each rendering approach serves different goals, balancing performance and user experience in distinct ways.
Server-Side Rendering (SSR): With SSR, the web server creates the HTML for a page and sends it to the browser. This method can improve initial load times and help with SEO, but it can be slower when dealing with dynamic content that changes often.
Client-Side Rendering (CSR): In CSR, the browser builds the HTML using JavaScript. This can lead to a faster, more interactive experience for the user, but the page may take longer to load initially, and it’s not the best for SEO.
Static Site Generation (SSG): With SSG, the HTML is generated during the build process and served as a static file to the client. This provides fast performance and better security but isn’t as flexible for pages that need to change frequently.
Watch this video for a complete explanation of rendering patterns.
4. Critical Rendering Path (CRP):
The Critical Rendering Path (CRP) refers to the steps the browser takes to turn HTML, CSS, and JavaScript into visible content on the screen. By optimizing the critical render path, you can improve how quickly the page loads. The CRP includes processes like building the Document Object Model (DOM), the CSS Object Model (CSSOM), creating the render tree, and performing the layout.
Below are different strategies to Optimize the Critical Rendering Path:
HTML Optimization:
i) Load <style> in <head>: This ensures that the styles are applied before the content is visible, preventing a flash of unstyled content (FOUC).
ii) Load <script> right before </body>: This minimizes render blocking and allows the HTML content to load before JavaScript runs.
CSS Optimization:
i) Load only what is needed: Only include the necessary CSS for the page being loaded to avoid excess styles that slow down rendering.
ii) Use less specificity: Simplify your CSS selectors to make it easier for the browser to apply styles, speeding up the rendering process.
JavaScript Optimization:
i) Load scripts asynchronously: This ensures that JavaScript is loaded in parallel without blocking the page from rendering.
ii) Defer loading of scripts: Delays the execution of non-essential scripts until the page has finished loading, improving the initial render time.
Third-Party Optimization: Third-party services like Google Analytics, GTag Manager, or Microsoft Clarity can slow down the page load and impact user experience. Here are a few ways to optimize them:
a) Load their scripts with defer: This ensures the scripts load after the main content is rendered.
b) Load them when the user scrolls: This technique delays loading third-party scripts until the user interacts with the page, reducing the initial load time.
c) Use setTimeout to load after a delay: Load third-party scripts after a specified time (e.g., 5 seconds), ensuring they don’t delay the initial render.
d) Web Worker: Use a Web Worker to load third-party scripts and offload their execution from the main thread. It prevents UI blocking and improving responsiveness. This is especially useful for heavy analytics or tracking scripts like Google Tag Manager and MS Clarity.
5. Performance Monitoring
Performance monitoring can be divided into two types of metrics: Browser-Centric and User-Centric (User Perception).
Browser-Centric Metrics: These focus on the technical aspects of page loading and rendering.
a) Time to First Byte (TTFB): The time it takes for the browser to receive the first byte of data from the server after making a request.
b) Network Request and Load Time: The time it takes for the network to send a request and receive a response, including all assets required to load the page.
c) DNS Resolution / DNS Lookup: The time it takes for the browser to translate the domain name into an IP address. You can implement caching to improve this.
d) Connection Time: The time it takes to establish a connection between the client and the server.
e) DOM Content Loaded: The time when the HTML is fully loaded and parsed, excluding images and stylesheets.
f) Page Load: The total time it takes for the page to load completely, including all assets like images, scripts, and styles.
User-Centric Metrics (Mainly Web Vitals): These focus on how users perceive the performance of a page.
a) First Contentful Paint (FCP): Measures the time from when the page starts loading to when any part of the page’s content (like text or images) is rendered on the screen.
b) Largest Contentful Paint (LCP): Measures the time from when the page starts loading to when the largest content element (text block or image) is rendered.
c) Interaction to Next Paint (INP): Measures the responsiveness of the page by tracking the latency of interactions (tap, click, or keyboard action). It records the worst interaction latency as a representative value of the page’s overall responsiveness.
d) Total Blocking Time (TBT): Measures the total time between FCP and Time to Interactive (TTI) where the main thread was blocked, preventing the page from responding to user input.
e) Cumulative Layout Shift (CLS): Measures the total score of unexpected layout shifts that occur while the page is loading, affecting visual stability.
f) Time to First Byte (TTFB): Measures how long it takes for the network to respond to a user request with the first byte of the resource.
To measure performance, you can use various tools such as PageSpeed Insights, Chrome DevTools (Network and Performance tabs), Lighthouse, Microsoft Clarity, etc
Network Optimization
Below are the various network optimization techniques we will cover:
- Minimize the HTTP Request
- Loading of JS: async / defer
- Avoid Redirection
- Resource Hinting
- Fetch Priority
- Early Hints
- HTTP upgrade methods (http 1.1 VS http2 VS http3)
- Compression (brotli / gzip)
- Optimizing third-party Scripts like Google Tag Manager, Analytics, MS Clarity (Already discussed in the Client Side Optimization Section)
- HTTP caching: Cache Control
- Caching using a Service Worker
Optimize the Network Request
When optimizing for faster page loads, optimizing network requests can greatly improve site speed. Here are some key challenges and solutions:
Challenges:
- Connection Overhead: Each new connection requires setup time, including steps like TCP handshakes and SSL Handshake.
- Browser Limitations: Browsers limit the number of parallel requests per domain (usually 6–10), meaning too many requests can block loading.
Solutions:
1. Images: Compress images. Use images like Base64, and SVG for icons and small images. This reduces extra requests and allows images to load directly within HTML or CSS.
2. Minimize (Reduce) external requests: Limit the number of external libraries and services you use. Whenever possible, bundle dependencies to avoid multiple network calls.
3. Load scripts asynchronously: Use async
or defer
attributes for scripts to prevent them from blocking page rendering.
4. Avoid long running JavaScript: Ensure your code doesn’t block the main thread with tasks that take longer time to execute, which can cause pages to feel unresponsive. Load them in the background using a service worker if possible.
5. Avoid Redirection: Reduce unnecessary redirects like going from HTTP to HTTPS (http://www.flipkart.com/ to https://www.flipkart.com/). Instead, preload HTTPS in browsers using HSTS preload to avoid redirect delays.
6. Resource Hinting: Resource hints are directives that help browsers pre-optimize resource loading. Use them as follows:
Preconnect: Pre-establish early connections to important domains (e.g., CDNs) to reduce latency. Avoid handshake.
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
DNS Prefetch: Resolve DNS for domains that will be accessed later. Avoid using DNS prefetch if preconnect is already in place, as preconnect also handles DNS resolution.
<link rel="dns-prefetch" href="https://cdn.example.com" />
Preload: Preload assets critical to page rendering, like fonts or hero images, for improved performance on initial load.
<link
rel="preload"
href="https:images.flower.avif"
as="image"
type="image/avif"
crossorigin
/>
<link
rel="preload"
href="https://example.com/fonts/font.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Prefetch: Needed in the near future navigation like prefetch of image or a js chunk.
<link rel="prefetch" href="https://example.com/scripts/next-page.js" />
7. Fetch Priority / Resource hinting priority: Use the fetchpriority attribute to prioritize image loading. Set it to high for above-the-fold content and low for less important images.
<img src="hero.jpg" fetchpriority="high" alt="Hero Image" />
<img src="footer-logo.jpg" fetchpriority="low" alt="Footer Logo" />
8. Early Hint: Early Hints (HTTP status code 103) allow servers to send hints about critical resources to browsers before the main response, optimizing load times. This allows a browser to preconnect to sites or start preloading resources even before the server has prepared and sent a final response. Preloaded resources indicated by early hints are fetched by the client as soon as the hints are received.
Example HTTP Response with Early Hints: Servers can send a preliminary 103 response with hints about critical resources like stylesheets or scripts.
HTTP/1.1 103 Early Hints
Link: </main.abcd100.css>; rel=preload; as=style
Link: </script.abcd100.js>; rel=preload; as=script
HTTP/1.1 200 OK
Content-Type: text/html
<html>
<head>
<link rel="stylesheet" href="/main.abcd105.css">
</head>
<body>
<script src="/script.abcd105.js"></script>
</body>
</html>
Note: Early Hints require server support, such as HTTP/2 or HTTP/3, and are typically implemented at the CDN or reverse proxy level.
8. HTTP Upgrade (http 1.1 VS http2 VS http3):
- Speed: HTTP/3 > HTTP/2 > HTTP/1.1
- Security: HTTP/3 > HTTP/2 > HTTP/1.1
- Compatibility: HTTP/1.1 > HTTP/2 > HTTP/3
- Ease of Debugging: HTTP/1.1 > HTTP/2 > HTTP/3
9. Compression (brotli / gzip):
- GZIP: A widely-used text compression algorithm since 1992. It reduces file size to speed up web page load times and is supported by nearly all browsers and servers.
- Brotli: A newer compression algorithm (2013) with better compression rates than GZIP, reducing file sizes further while maintaining speed.
Benefits of GZIP and Brotli
- Smaller Files: Reduces text file sizes (e.g., HTML, CSS, JS) to save bandwidth.
- Faster Load Times: Smaller files transfer faster, improving page load speed and user experience.
- Compatibility: Brotli achieves up to 70% compression; GZIP achieves around 65%.
- Fallback Options: Brotli is supported by 96% of browsers; GZIP acts as a fallback for older browsers like IE11.
10. HTTP caching: HTTP caching helps reduce page load times by storing copies of resources (e.g., HTML, CSS, JavaScript) locally in a browser or at intermediary servers (e.g., CDNs). This avoids redundant requests and improves performance.
Types of Caching
- Browser Cache
- Stores resources locally in the user’s browser.
- Reduces load time for repeat visits by serving cached files.
2. CDN Cache
- Distributed cache used by CDNs to store resources closer to users.
- Reduces latency by avoiding direct requests to the origin server.
11. Service worker caching: A service worker intercepts network-type HTTP requests and uses a caching strategy to determine what resources should be returned to the browser. The service worker cache and the HTTP cache serve the same general purpose, but the service worker cache offers more caching capabilities, such as fine-grained control over exactly what is cached and how caching is done.
Benefits
- Offline Access: Serve cached resources when offline.
- Faster Load Times: Cache static assets and serve them directly.
- Customizable Caching: Define cache policies for different resources.
const CACHE_NAME = 'my-app-cache-v1';
const FILES_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/script.js',
'/logo.png',
];
// Install event: Cache files
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(FILES_TO_CACHE);
})
);
});
// Fetch event: Serve from cache or network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
// Activate event: Clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
)
);
});
Build Optimization
Bundler
A bundler is a tool that processes and optimizes frontend code, transforming it into a browser-compatible format. It takes various inputs like TypeScript, SCSS, and modern JavaScript and outputs an optimized bundle with features like transpilation, minification, and fallback support for older browsers.
Some different types of bundlers:
- Esbuild
- Webpack
- Vite
- Rollup
- Parcel
Build Optimization can be generally categorized into ways:
1. User Experience:
User Experience optimizations done during build times:
- Code Splitting
- Tree Shaking
- Minification
- Cross Browser Compatibility
- Code Obfuscation: Obfuscation means to make something difficult to understand. This is often done to protect intellectual property or make it harder for attackers to reverse-engineer the code. It also helps in inspecting and rewriting the code to reduce its size.
// Programmer Code
function getUserData() {
return "Sensitive Data";
}
console.log(getUserData());
// After Code Obfuscation
function a() {
return "Sensitive Data";
}
console.log(a());
// Programmer Code
function greet(name) {
console.log(`Hello, ${name}!`);
}
greet("Alice");
// After Code Obfuscation
var _0x1234 = ["log", "Hello, ", "!"];
(function (_0x5678, _0x9abc) {
var _0xdef0 = function (_0x1357) {
while (--_0x1357) {
_0x5678["push"](_0x5678["shift"]());
}
};
_0xdef0(++_0x9abc);
})(_0x1234, 0x2);
var _0x2345 = function (_0x5678, _0x9abc) {
_0x5678 = _0x5678 - 0x0;
var _0xdef0 = _0x1234[_0x5678];
return _0xdef0;
};
function greet(_0x6789) {
console[_0x2345("0x0")](_0x2345("0x1") + _0x6789 + _0x2345("0x2"));
}
greet("Alice");
- Pruning and Optimizing CSS
- Compression: Reducing the file size of JS, CSS, etc. It can be done using Brotli or GZIP. It needs to be enabled/applied during the bundling phase and then can be shipped via the network layer.
- Optimizing Images and assets
- Remove Source Maps in Production. Keep it only in the Development mode.
- Profiling and Analyzing bundles
- Pre-rendering (SSG)
- Cache using asset hashing: Asset hashing is a technique where filenames of bundled assets (JS, CSS, images, etc.) include a unique hash (based on file content). This ensures that browsers only fetch new versions when the file changes, preventing unnecessary re-downloads and improving caching efficiency. When a file changes, its hash changes, and the browser downloads the new version. If the file stays the same, it uses the cached version.
Example: A file named app.js → after hashing → app.a1b2c3.js
Next time you modify app.js, a new hashed file gets generated: app.d4e5f6.js
- Vendor Chunk Splitting: It is a bundling optimization technique where third-party dependencies (like Carousel Package, Lodash, or Axios) are separated from your application’s code into a dedicated “vendor” bundle. This helps improve caching, load times, and overall performance. With vendor chunk splitting, a third-party dependency used in a particular route will be downloaded only when that route is loaded. Without it, the dependency will be downloaded on all pages.
2. Developer Experience:
- Faster Builds
- Parallelization: Parallelization in bundling refers to the process of splitting tasks across multiple CPU cores or threads to speed up the build process. Instead of processing files sequentially, modern bundlers utilize parallel processing to optimize performance. Most bundlers, such as Webpack, Vite, Rollup, Parcel, and esbuild, leverage worker threads or multiple processes to speed up bundling.
- Cache Management: It is a mechanism that stores intermediate build results to avoid redundant computations and speed up subsequent builds. Instead of rebuilding everything from scratch, bundlers like Webpack reuse previously compiled modules and assets, significantly improving performance.
- Incremental Compilation: Incremental Compilation is a build optimization technique where only the changed files are recompiled instead of rebuilding everything from scratch. This significantly speeds up the development and build process, especially for large projects.
- HMR (Hot Module Replacement): HMR is a feature in modern bundlers that updates modules in real-time without requiring a full page reload. This drastically improves development speed and experience by keeping the state intact while applying code changes.
- Monorepos with tools like Lerna or Nx: A monorepo (monolithic repository) is a single Git repository that stores multiple related projects, often sharing dependencies and configurations. Tools like Lerna and Nx help manage monorepos efficiently, improving code sharing, dependency management, and scalability.
Thank you for reading this far! I hope you now have a clear understanding of frontend optimization techniques and strategies. This blog was designed as a handbook or checklist that you can revisit whenever needed.
Performance optimization is an ongoing process, and there’s always more to explore — so keep learning, experimenting, and refining your approach!
If you want more web development content then follow me on Medium and subscribe to my YouTube channel
Thank you for being a part of the community
Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast | Differ | Twitch
- Check out CoFeed, the smart way to stay up-to-date with the latest in tech 🧪
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com