Performance is a critical aspect of frontend development that directly impacts user experience, engagement, and conversion rates. Studies have shown that:
53% of mobile users abandon sites that take longer than 3 seconds to load
Every 100ms of latency costs Amazon 1% in sales
Google uses page speed as a ranking factor for both desktop and mobile searches
Performance optimization is not just about making your site fast—it’s about creating a smooth, responsive experience that keeps users engaged and satisfied.
Before optimizing, it’s important to understand what to measure. Here are key performance metrics to track:
First Contentful Paint (FCP) Time until the browser renders the first bit of content from the DOM. Good target: Under 1.8 seconds
Largest Contentful Paint (LCP) Time until the largest text or image element is rendered. Good target: Under 2.5 seconds
First Input Delay (FID) Time from when a user first interacts with your site to when the browser responds. Good target: Under 100 milliseconds
Cumulative Layout Shift (CLS) Measures visual stability and unexpected layout shifts. Good target: Under 0.1
Time to Interactive (TTI) Time until the page is fully interactive. Good target: Under 3.8 seconds
Total Blocking Time (TBT) Sum of all time periods between FCP and TTI when the main thread was blocked. Good target: Under 200 milliseconds
Lighthouse
Chrome’s built-in auditing tool that provides performance scores and suggestions. Access it through Chrome DevTools > Lighthouse tab or run it from the command line: npm install -g lighthouse
lighthouse https://example.com --view
WebPageTest
Provides detailed performance analysis from multiple locations and browsers. Visit WebPageTest.org to run tests.
Chrome DevTools Performance Panel
Offers detailed runtime performance analysis including CPU usage, rendering, and network activity.
Open Chrome DevTools (F12)
Go to the Performance tab
Click Record and interact with your site
Stop recording and analyze the results
Core Web Vitals Report
Google’s report on real-user performance metrics. Access it through Google Search Console.
Real User Monitoring (RUM)
While lab testing is valuable, measuring performance with real users provides the most accurate data:
// Using the Web Vitals library
import { getLCP , getFID , getCLS } from 'web-vitals' ;
function sendToAnalytics ({ name , delta , id }) {
// Send metrics to your analytics service
console . log ( `Metric: ${ name } | Value: ${ delta } | ID: ${ id } ` );
}
// Monitor Core Web Vitals
getCLS ( sendToAnalytics );
getFID ( sendToAnalytics );
getLCP ( sendToAnalytics );
Optimizing Asset Delivery
Image Optimization
Images often account for the largest portion of page weight. Here’s how to optimize them:
Compress and resize images
Use tools like ImageOptim, TinyPNG, or Sharp to compress images without significant quality loss. # Using Sharp in Node.js
const sharp = require ( 'sharp' );
sharp( 'input.jpg' )
.resize(800 ) // Resize to 800px width
.jpeg( { quality: 80 } ) // Compress with 80% quality
.toFile( 'output.jpg' );
Always serve images at the appropriate size for their display dimensions.
Only load images when they’re about to enter the viewport. <!-- Native lazy loading -->
< img src = "image.jpg" alt = "Description" loading = "lazy" >
For broader browser support, use a library like lazysizes: < img data-src = "image.jpg" class = "lazyload" alt = "Description" >
Serve different image sizes based on the device’s screen size. < img
srcset = "small.jpg 500w, medium.jpg 1000w, large.jpg 1500w"
sizes = "(max-width: 600px) 500px, (max-width: 1200px) 1000px, 1500px"
src = "medium.jpg"
alt = "Description"
>
JavaScript Optimization
Split your JavaScript into smaller chunks that load on demand. // Using dynamic imports in modern JavaScript
button . addEventListener ( 'click' , async () => {
const module = await import ( './heavy-feature.js' );
module . initFeature ();
});
With webpack: // webpack.config.js
module . exports = {
entry: './src/index.js' ,
output: {
filename: '[name].[contenthash].js' ,
chunkFilename: '[name].[contenthash].js' ,
},
optimization: {
splitChunks: {
chunks: 'all' ,
},
},
};
Minification and compression
Reduce file size by removing unnecessary characters and compressing the code. # Using Terser for minification
npx terser script.js -o script.min.js -c -m
Enable Gzip or Brotli compression on your server: # Nginx configuration for Gzip
gzip on ;
gzip_types text/plain text/css application/javascript;
gzip_min_length 1000 ;
Defer non-critical JavaScript
Prevent JavaScript from blocking the page render. <!-- Defer loading until HTML parsing is complete -->
< script src = "non-critical.js" defer ></ script >
<!-- Load after everything else is done -->
< script src = "analytics.js" async ></ script >
Remove unused code from your bundles. // webpack.config.js
module . exports = {
mode: 'production' , // Enables tree shaking
optimization: {
usedExports: true ,
},
};
Use ES modules to enable tree shaking: // Import only what you need
import { Button } from 'ui-library' ;
// Instead of
// import * from 'ui-library';
CSS Optimization
Inline critical styles in the <head>
and load the rest asynchronously. < head >
< style >
/* Critical CSS for above-the-fold content */
header { /* styles */ }
.hero { /* styles */ }
</ style >
< link rel = "preload" href = "styles.css" as = "style" onload = " this . onload = null ; this . rel = 'stylesheet'" >
< noscript >< link rel = "stylesheet" href = "styles.css" ></ noscript >
</ head >
Tools like Critical or CriticalCSS can automate this process.
Remove unused styles to reduce file size. # Using PurgeCSS
npx purgecss --css style.css --content index.html --output style.purged.css
Minimize render-blocking CSS
Load non-critical CSS asynchronously. < link rel = "preload" href = "non-critical.css" as = "style" onload = " this . onload = null ; this . rel = 'stylesheet'" >
Use efficient selectors to improve rendering performance. /* Avoid deeply nested selectors */
.header .navigation .list .item a { /* slow */ }
/* Better approach */
.nav-link { /* fast */ }
Fonts Optimization
Use system fonts when possible
System fonts load instantly because they’re already installed. body {
font-family : -apple-system , BlinkMacSystemFont, "Segoe UI" , Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue" , sans-serif ;
}
Optimize web fonts loading
Use font-display
to control how fonts are displayed while loading. @font-face {
font-family : 'CustomFont' ;
src : url ( 'custom-font.woff2' ) format ( 'woff2' );
font-weight : 400 ;
font-style : normal ;
font-display : swap ; /* Show fallback font until custom font loads */
}
Preload important fonts: < link rel = "preload" href = "custom-font.woff2" as = "font" type = "font/woff2" crossorigin >
Only include the characters you need. <!-- For Latin characters only -->
< link href = "https://fonts.googleapis.com/css2?family=Roboto&subset=latin" rel = "stylesheet" >
Use tools like glyphhanger to create custom subsets.
Limit font weights and styles
Each font weight and style is a separate download. /* Instead of loading many weights */
body {
/* Only load what you need */
font-family : 'Roboto' ;
font-weight : 400 ; /* Regular */
}
h1 , h2 , h3 {
font-weight : 700 ; /* Bold */
}
Optimizing the Critical Rendering Path
Minimize render-blocking resources
Move non-critical CSS and JavaScript out of the critical rendering path. <!-- For CSS -->
< link rel = "preload" href = "non-critical.css" as = "style" onload = " this . onload = null ; this . rel = 'stylesheet'" >
<!-- For JavaScript -->
< script src = "non-critical.js" defer ></ script >
Optimize DOM size
Keep the DOM tree small and shallow. <!-- Avoid -->
< div >
< div >
< div >
< div >
< p > Deeply nested content </ p >
</ div >
</ div >
</ div >
</ div >
<!-- Better -->
< p class = "content" > Flatter DOM structure </ p >
Avoid layout thrashing
Batch DOM reads and writes to prevent forced reflows. // Bad: Interleaving reads and writes
const width = element . offsetWidth ; // Read
element . style . width = ( width + 10 ) + 'px' ; // Write
const height = element . offsetHeight ; // Read (forces reflow)
element . style . height = ( height + 10 ) + 'px' ; // Write
// Good: Batch reads, then writes
const width = element . offsetWidth ; // Read
const height = element . offsetHeight ; // Read
element . style . width = ( width + 10 ) + 'px' ; // Write
element . style . height = ( height + 10 ) + 'px' ; // Write
Use efficient CSS animations
Prefer properties that only affect compositing. /* Expensive (triggers layout) */
.expensive {
animation : move 1 s infinite ;
}
@keyframes move {
from { width : 100 px ; height : 100 px ; }
to { width : 200 px ; height : 200 px ; }
}
/* Efficient (only compositing) */
.efficient {
animation : slide 1 s infinite ;
}
@keyframes slide {
from { transform : translateX ( 0 ); }
to { transform : translateX ( 100 px ); }
}
Preventing Layout Shifts
Layout shifts create a poor user experience and negatively impact your CLS score.
<!-- Bad: Image without dimensions -->
< img src = "image.jpg" alt = "Description" >
<!-- Good: Image with dimensions -->
< img src = "image.jpg" alt = "Description" width = "800" height = "600" >
/* Reserve space for dynamic content */
.comments-container {
min-height : 200 px ;
}
/* Use content-visibility for off-screen content */
.below-fold-section {
content-visibility : auto ;
contain-intrinsic-size : 1000 px ; /* Estimate height */
}
Network Optimization
Caching Strategies
Configure proper cache headers to reduce server requests. # Nginx configuration for static assets
location /static/ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" ;
}
# For HTML files
location / {
add_header Cache-Control "no-cache, must-revalidate" ;
}
Implement offline caching with service workers. // Register a service worker
if ( 'serviceWorker' in navigator ) {
navigator . serviceWorker . register ( '/sw.js' );
}
// In sw.js
self . addEventListener ( 'install' , ( event ) => {
event . waitUntil (
caches . open ( 'v1' ). then (( cache ) => {
return cache . addAll ([
'/' ,
'/styles.css' ,
'/script.js' ,
'/offline.html'
]);
})
);
});
self . addEventListener ( 'fetch' , ( event ) => {
event . respondWith (
caches . match ( event . request ). then (( response ) => {
return response || fetch ( event . request );
})
);
});
Use browser memory cache for frequently accessed data. // Simple in-memory cache
const cache = new Map ();
async function fetchWithCache ( url ) {
if ( cache . has ( url )) {
return cache . get ( url );
}
const response = await fetch ( url );
const data = await response . json ();
cache . set ( url , data );
return data ;
}
Resource Hints
Use resource hints to inform the browser about resources it should load or connect to:
<!-- Preconnect to important third-party domains -->
< link rel = "preconnect" href = "https://api.example.com" >
<!-- DNS prefetch for older browsers -->
< link rel = "dns-prefetch" href = "https://api.example.com" >
<!-- Preload critical resources -->
< link rel = "preload" href = "critical-script.js" as = "script" >
< link rel = "preload" href = "hero-image.jpg" as = "image" >
<!-- Prefetch resources needed for the next page -->
< link rel = "prefetch" href = "next-page.html" >
<!-- Prerender the next page (use with caution) -->
< link rel = "prerender" href = "likely-next-page.html" >
Framework-Specific Optimizations
React
// Use React.memo for component memoization
const MemoizedComponent = React . memo ( function MyComponent ( props ) {
// Only re-renders if props change
return < div > { props . name } </ div > ;
});
// Use useMemo for expensive calculations
function SearchResults ({ query , data }) {
const filteredData = React . useMemo (() => {
return data . filter ( item => item . name . includes ( query ));
}, [ query , data ]);
return (
< ul >
{ filteredData . map ( item => < li key = { item . id } > { item . name } </ li > ) }
</ ul >
);
}
// Use useCallback for stable function references
function Parent () {
const [ count , setCount ] = useState ( 0 );
const handleClick = React . useCallback (() => {
console . log ( 'Clicked!' );
}, []); // Empty dependency array = stable reference
return < Child onClick = { handleClick } /> ;
}
Vue.js
< template >
< div >
<!-- Use v-once for content that never changes -->
< header v-once >
< h1 > {{ title }} </ h1 >
</ header >
<!-- Use v-show instead of v-if for elements that toggle frequently -->
< div v-show = " isVisible " > Toggled content </ div >
<!-- Use key with v-for -->
< ul >
< li v-for = " item in items " : key = " item . id " > {{ item . name }} </ li >
</ ul >
</ div >
</ template >
< script >
export default {
data () {
return {
title: 'My App' ,
isVisible: true ,
items: []
};
} ,
// Use computed properties for derived values
computed: {
filteredItems () {
return this . items . filter ( item => item . isActive );
}
}
} ;
</ script >
Angular
// Use OnPush change detection strategy
@ Component ({
selector: 'app-item' ,
template: `<div>{{ item.name }}</div>` ,
changeDetection: ChangeDetectionStrategy . OnPush
})
export class ItemComponent {
@ Input () item : any ;
}
// Use trackBy with ngFor
@ Component ({
selector: 'app-list' ,
template: `
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
`
})
export class ListComponent {
items : any [] = [];
trackByFn ( index : number , item : any ) : number {
return item . id ;
}
}
// Lazy load modules
const routes : Routes = [
{
path: 'admin' ,
loadChildren : () => import ( './admin/admin.module' ). then ( m => m . AdminModule )
}
];
Set performance budgets to maintain performance standards as your project evolves:
// webpack.config.js
module . exports = {
performance: {
maxAssetSize: 250000 , // 250 KB
maxEntrypointSize: 250000 ,
hints: 'error'
}
};
In your CI/CD pipeline, you can use tools like Lighthouse CI to enforce performance budgets:
// lighthouserc.json
{
"ci" : {
"collect" : {
"url" : [ "https://example.com" ],
"numberOfRuns" : 3
},
"assert" : {
"assertions" : {
"first-contentful-paint" : [ "warn" , { "minScore" : 0.8 }],
"interactive" : [ "error" , { "maxNumericValue" : 3000 }],
"max-potential-fid" : [ "error" , { "maxNumericValue" : 100 }],
"cumulative-layout-shift" : [ "error" , { "maxNumericValue" : 0.1 }],
"largest-contentful-paint" : [ "error" , { "maxNumericValue" : 2500 }]
}
}
}
}
Use this checklist to ensure you’ve covered the most important performance optimizations:
Measure current performance
Run Lighthouse audits
Set up real user monitoring
Identify the biggest performance bottlenecks
Optimize asset delivery
Compress and optimize images
Minify and compress JavaScript and CSS
Implement code splitting
Use tree shaking
Optimize fonts
Improve rendering performance
Minimize render-blocking resources
Optimize the critical rendering path
Prevent layout shifts
Use efficient animations
Implement caching strategies
Configure HTTP caching
Implement service workers
Use resource hints
Apply framework-specific optimizations
Use memoization techniques
Implement lazy loading
Optimize rendering cycles
Set up performance monitoring
Establish performance budgets
Integrate performance testing in CI/CD
Monitor real user metrics
Next Steps
Now that you understand frontend performance optimization, you can:
Learn about Accessibility to make your sites usable by everyone
Explore Security best practices to protect your applications
Study Testing strategies to ensure your optimizations don’t break functionality