Introduction to Web Performance
Web performance is like the difference between a sports car and a compact sedan. Both will get you from point A to point B, but the sports car provides a smoother, faster, and more enjoyable experience. Similarly, a high-performing website delivers content quickly and responds promptly to user interactions, creating a satisfying experience that keeps users engaged.
In today's world, users expect websites to load almost instantly. Studies show that 53% of mobile site visits are abandoned if pages take longer than 3 seconds to load. Performance isn't just about user satisfaction—it directly impacts business metrics like conversion rates, bounce rates, and even search engine rankings.
By the end of this session, you'll understand:
- Why web performance matters from business and user experience perspectives
- Key performance metrics and how to measure them
- Common performance bottlenecks and how to identify them
- Practical techniques to optimize frontend performance
- Tools and methodologies for ongoing performance monitoring
The Restaurant Analogy
Think of a website like a restaurant. The performance of a website is similar to the service quality at a restaurant:
- Load time is like waiting to be seated - if it takes too long, customers might leave
- Rendering is like seeing your food arrive at the table - the visual confirmation that your order is being processed
- Time to Interactive is like having the food actually ready to eat - it's there, but can you interact with it yet?
- Optimization techniques are like the efficiency systems in the kitchen - they're not visible to customers but make everything run smoothly
Why Performance Matters
Impact on User Experience
Performance directly affects how users perceive your website or application. Here's how:
- Attention Span: Users have increasingly short attention spans. A delay of just a few seconds can cause frustration and abandonment.
- Perceived Quality: Performance issues are often equated with poor quality and low trustworthiness. A fast site feels professional and reliable.
- Accessibility: Not all users have high-speed connections or powerful devices. Performance optimizations make your site more accessible to a broader audience.
- User Satisfaction: Fast, responsive interfaces create a positive emotional response, leading to a better overall impression of your brand.
Real-World Impact
- Pinterest reduced perceived wait times by 40% and saw a 15% increase in SEO traffic and sign-ups
- BBC found they lose an additional 10% of users for every additional second their site takes to load
- Walmart observed a 2% conversion increase for every 1 second of improvement in page load time
Business Impact
Beyond user experience, performance has tangible business effects:
- Conversion Rates: Faster sites convert better. Every 100ms of improvement in load time can increase conversion rates.
- Bounce Rates: Slow sites see higher bounce rates as users leave before engaging with content.
- SEO Rankings: Page speed is a ranking factor for search engines like Google.
- Operating Costs: Efficient sites consume less bandwidth and server resources, potentially reducing hosting costs.
Typical Performance Impact on Business Metrics
| Performance Improvement | Typical Business Impact |
|---|---|
| 1 second faster | 7% increase in conversions |
| 50% faster load time | 12% increase in conversions |
| 30% faster site | 2-3x higher engagement rate |
Mobile Performance Considerations
With mobile traffic now exceeding desktop for many websites, mobile performance deserves special attention:
- Network Constraints: Mobile networks are often slower and less reliable than desktop connections.
- Device Limitations: Mobile devices typically have less processing power and memory than desktops.
- Context Sensitivity: Mobile users are often on the go and may be more sensitive to delays.
- Battery Impact: Inefficient websites can drain device batteries faster, creating a negative impression.
Detecting Network Conditions
// Using the Network Information API to adapt to network conditions
if (navigator.connection) {
const connection = navigator.connection;
if (connection.saveData) {
// User has requested reduced data usage
loadLowResolutionImages();
} else if (connection.effectiveType === '4g') {
// Fast connection, load high-quality assets
loadHighResolutionImages();
} else {
// Slower connection, load optimized assets
loadOptimizedImages();
}
}
Measuring Performance
Before you can improve performance, you need to measure it. Understanding key metrics and how to track them is essential for effective optimization.
Core Web Vitals and Other Key Metrics
Largest Contentful Paint (LCP)
Measures loading performance. To provide a good user experience, LCP should occur within 2.5 seconds of when the page first starts loading.
Imagine a newspaper gradually appearing on your doorstep. LCP is the moment when the main headline and featured image become visible.
First Input Delay (FID)
Measures interactivity. To provide a good user experience, pages should have a FID of 100 milliseconds or less.
Think of this as how long it takes a waiter to acknowledge you after you've raised your hand to order. If they take too long, you feel ignored and frustrated.
Cumulative Layout Shift (CLS)
Measures visual stability. To provide a good user experience, pages should maintain a CLS of 0.1 or less.
Imagine reading a physical book and the text and images suddenly jump around as you're trying to read. That's what a high CLS feels like to users.
Time to First Byte (TTFB)
Measures how long it takes for the browser to receive the first byte of response from the server.
Like waiting for the first word of an answer after asking a question. Even if the full answer takes time, hearing that first word quickly tells you someone's responding.
First Contentful Paint (FCP)
Measures how long it takes for the browser to render the first piece of DOM content.
Similar to seeing the first brush stroke appear on a previously blank canvas. It confirms that something is happening.
Time to Interactive (TTI)
Measures how long it takes for the page to become fully interactive.
Imagine a theme park ride that's visible but has a "Not Yet Operating" sign. TTI is when the ride is actually available for you to use.
Tools for Measuring Performance
Browser Developer Tools
Built-in tools in browsers like Chrome, Firefox, and Safari that provide detailed performance insights.
- Network Panel: Visualizes resource loading, timing, and size
- Performance Panel: Records and analyzes runtime performance
- Memory Panel: Identifies memory usage and leaks
Quick Tip: Using Chrome's Performance Panel
- Open DevTools (F12 or Right-click > Inspect)
- Go to the Performance tab
- Click the record button (circle)
- Interact with the page
- Stop recording to see detailed performance analysis
Lighthouse
An open-source, automated tool for improving the quality of web pages. It runs audits for performance, accessibility, progressive web apps, SEO, and more.
Using Lighthouse
- In Chrome DevTools, go to the Lighthouse tab
- Select the categories you want to audit
- Click "Generate report"
- Review the results and recommendations
WebPageTest
A free tool that allows you to test your website's performance from multiple locations around the world using real browsers.
WebPageTest API Example
// Using the WebPageTest API with Node.js
const WebPageTest = require('webpagetest');
const wpt = new WebPageTest('www.webpagetest.org', 'YOUR_API_KEY');
wpt.runTest('https://example.com', {
location: 'Sydney:Chrome',
connectivity: '3G',
runs: 3,
firstViewOnly: false
}, (err, data) => {
if (err) {
console.error('Error running test:', err);
return;
}
console.log('Test results:', data);
});
Google PageSpeed Insights
Analyzes the content of a web page and generates suggestions to make that page faster based on Google's PageSpeed rules.
Real User Monitoring (RUM)
Collects performance data from actual users as they interact with your site.
Using the Performance API for Basic RUM
// Basic Real User Monitoring using the Performance API
window.addEventListener('load', () => {
// Wait for the page to fully load to get accurate metrics
setTimeout(() => {
const perfData = window.performance.timing;
// Calculate key metrics
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
const domContentLoaded = perfData.domContentLoadedEventEnd - perfData.navigationStart;
// Send the data to your analytics server
navigator.sendBeacon('/analytics', JSON.stringify({
pageLoadTime,
domContentLoaded,
url: document.location.href,
timestamp: new Date().toISOString()
}));
}, 0);
});
Setting Performance Budgets
A performance budget is a set of limits on metrics that affect site performance. It's like a financial budget, but for user experience instead of money.
Types of Performance Budgets
- Quantity-based: Limits on the number of HTTP requests, total weight of a page, etc.
- Rule-based: Scores from tools like Lighthouse or WebPageTest
- Timing-based: Limits on metrics like TTI, LCP, etc.
Example Performance Budget
{
"resources": {
"total": 320, // Total weight in KB
"script": 120, // JS weight in KB
"image": 140, // Images weight in KB
"stylesheet": 40, // CSS weight in KB
"document": 20, // HTML weight in KB
"font": 20, // Fonts weight in KB
"thirdParty": 100 // Third-party scripts in KB
},
"requests": {
"total": 30, // Maximum number of requests
"script": 8, // JS requests
"image": 12, // Image requests
"stylesheet": 2, // CSS requests
"font": 2, // Font requests
"thirdParty": 6 // Third-party requests
},
"metrics": {
"LCP": 2500, // Largest Contentful Paint in ms
"FID": 100, // First Input Delay in ms
"CLS": 0.1, // Cumulative Layout Shift score
"TTI": 5000 // Time to Interactive in ms
}
}
Tools for Enforcing Performance Budgets
- Lighthouse CI: Integrates into CI/CD pipelines to ensure performance metrics stay within budget
- Webpack Bundle Analyzer: Visualizes bundle size and helps identify large dependencies
- bundlesize: Checks if your bundle size exceeds a specified threshold
Setting up bundlesize in package.json
{
"bundlesize": [
{
"path": "./dist/js/*.js",
"maxSize": "120 kB"
},
{
"path": "./dist/css/*.css",
"maxSize": "40 kB"
}
]
}
Common Performance Bottlenecks
Understanding common performance issues is crucial for effective optimization. Here are the main areas where bottlenecks typically occur:
Network and Asset Loading
Network-related issues are often the most significant contributors to slow page loads.
Large File Sizes
Oversized images, uncompressed assets, and bloated code all contribute to slow download times.
Solutions:
- Optimize and compress images
- Minify CSS and JavaScript
- Use modern image formats (WebP, AVIF)
- Implement code splitting
Image Optimization with Modern Formats
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
Too Many Requests
Each HTTP request adds latency, especially on high-latency connections.
Solutions:
- Bundle files appropriately
- Use CSS sprites for small images
- Inline critical CSS
- Implement domain sharding for concurrent downloads
- Use HTTP/2 or HTTP/3 to reduce the impact of multiple requests
Inlining Critical CSS
<!-- Inline critical CSS in the head -->
<head>
<style>
/* Critical CSS for above-the-fold content */
body { margin: 0; font-family: sans-serif; }
header { background: #f5f5f5; padding: 1rem; }
.hero { height: 50vh; background: linear-gradient(45deg, #1a2a6c, #b21f1f); color: white; display: flex; align-items: center; justify-content: center; }
</style>
<!-- Load non-critical CSS asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
Render-Blocking Resources
CSS and JavaScript that block page rendering until they're downloaded and processed.
Solutions:
- Load non-critical JavaScript asynchronously or defer it
- Inline critical CSS
- Prioritize above-the-fold content
Async and Defer Scripts
<!-- Regular script: blocks parsing -->
<script src="blocking.js"></script>
<!-- Async script: downloads in parallel, executes as soon as available -->
<script src="analytics.js" async></script>
<!-- Deferred script: downloads in parallel, executes after parsing -->
<script src="non-critical.js" defer></script>
Inefficient Resource Loading
Resources that aren't loaded in the optimal order or with the right priorities.
Solutions:
- Use resource hints (preload, prefetch, preconnect)
- Implement server push with HTTP/2
- Prioritize critical resources
Resource Hints
<!-- Preconnect to required origins -->
<link rel="preconnect" href="https://api.example.com">
<!-- Preload critical resources -->
<link rel="preload" href="critical-font.woff2" as="font" type="font/woff2" crossorigin>
<!-- Prefetch resources for the next page -->
<link rel="prefetch" href="next-page.html">
Rendering Performance
Once resources are loaded, how efficiently they're processed and rendered affects perceived performance.
Layout Thrashing
Causes the browser to recalculate layout multiple times unnecessarily.
Solutions:
- Batch DOM reads and writes
- Use CSS transforms instead of properties that trigger layout
- Avoid forcing synchronous layouts
Avoiding Layout Thrashing
// Bad practice - causes layout thrashing
function badLayout() {
const elements = document.querySelectorAll('.box');
elements.forEach(el => {
// Read (forces layout calculation)
const height = el.offsetHeight;
// Write (invalidates layout)
el.style.height = (height + 10) + 'px';
// Read again (forces another layout calculation)
const width = el.offsetWidth;
// Write again (invalidates layout again)
el.style.width = (width + 10) + 'px';
});
}
// Good practice - batch reads and writes
function goodLayout() {
const elements = document.querySelectorAll('.box');
const measurements = [];
// Batch all reads
elements.forEach(el => {
measurements.push({
height: el.offsetHeight,
width: el.offsetWidth
});
});
// Batch all writes
elements.forEach((el, i) => {
const { height, width } = measurements[i];
el.style.height = (height + 10) + 'px';
el.style.width = (width + 10) + 'px';
});
}
Expensive Styles and Animations
CSS properties and animations that are computationally expensive.
Solutions:
- Use CSS properties that only affect compositing (transform, opacity)
- Avoid expensive properties (box-shadow, filter, etc.) in animations
- Use will-change for elements that will animate (but use sparingly)
- Reduce paint areas
Performant Animations
/* Expensive animation (triggers layout and paint) */
.expensive {
animation: move-expensive 1s infinite;
}
@keyframes move-expensive {
from { left: 0; top: 0; }
to { left: 100px; top: 100px; }
}
/* Performant animation (only affects compositing) */
.performant {
animation: move-performant 1s infinite;
}
@keyframes move-performant {
from { transform: translate(0, 0); }
to { transform: translate(100px, 100px); }
}
Layout Shifts
Elements that change position after they're initially rendered, causing a jarring experience for users.
Solutions:
- Set size attributes on images and other media
- Reserve space for dynamic content like ads
- Avoid inserting content above existing content
- Use CSS containment where appropriate
Preventing Layout Shifts
<!-- Bad: image without dimensions -->
<img src="image.jpg" alt="Description">
<!-- Good: image with aspect ratio -->
<div style="aspect-ratio: 16/9; max-width: 800px;">
<img src="image.jpg" alt="Description" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<!-- Good: reserved space for dynamic content -->
<div class="ad-container" style="min-height: 250px;">
<!-- Ad will load here -->
</div>
JavaScript Performance
JavaScript execution can significantly impact the responsiveness of your web application.
Long-Running JavaScript
JavaScript that blocks the main thread for extended periods, causing unresponsiveness.
Solutions:
- Break up long tasks into smaller chunks
- Defer non-critical work
- Use Web Workers for CPU-intensive tasks
- Implement virtualization for long lists
Using Web Workers for CPU-Intensive Tasks
// In main.js
const worker = new Worker('processor.js');
// Send data to the worker
worker.postMessage({
data: largeDataSet,
config: processingConfig
});
// Handle the result
worker.onmessage = function(event) {
const result = event.data;
updateUI(result);
};
// In processor.js (the worker)
self.onmessage = function(event) {
const { data, config } = event.data;
// Perform CPU-intensive processing
const result = processData(data, config);
// Send the result back to the main thread
self.postMessage(result);
};
function processData(data, config) {
// Computationally expensive operation
// that doesn't block the main thread
return data.map(/* ... */);
}
Inefficient DOM Manipulation
Frequent or poorly optimized interactions with the DOM.
Solutions:
- Minimize DOM updates
- Use DocumentFragment for batch insertions
- Consider using a virtual DOM library for complex UIs
- Remove event listeners when no longer needed
Efficient DOM Updates
// Inefficient - causes multiple reflows
function addItemsInefficiently(items) {
const list = document.getElementById('list');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
list.appendChild(li); // Causes reflow each time
});
}
// Efficient - uses DocumentFragment for batch update
function addItemsEfficiently(items) {
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
list.appendChild(fragment); // Single reflow
}
Memory Leaks
JavaScript that continues to consume memory, eventually causing performance degradation or crashes.
Solutions:
- Remove event listeners when elements are removed
- Watch for circular references in objects
- Be cautious with closures that capture large objects
- Use Chrome DevTools' Memory panel to identify leaks
Preventing Memory Leaks
// Memory leak example
function setupLeakyHandler() {
const largeData = new Array(1000000).fill('potentially large data');
const element = document.getElementById('button');
// This creates a closure that captures largeData
element.addEventListener('click', function() {
console.log(largeData.length);
});
// Even if element is removed from DOM, the event listener
// keeps largeData in memory
}
// Fixed example
function setupEfficientHandler() {
const element = document.getElementById('button');
// Reference only what you need
element.addEventListener('click', processClick);
}
function processClick() {
// Generate data only when needed
const data = generateData();
console.log(data.length);
}
// Clean up when no longer needed
function cleanup() {
const element = document.getElementById('button');
element.removeEventListener('click', processClick);
}
Key Optimization Techniques
Now that we understand common bottlenecks, let's explore practical optimization techniques. These strategies will help you improve the performance of your web applications.
Asset Optimization
Image Optimization
- Choose the right format: JPEG for photos, PNG for transparency, SVG for vector graphics, WebP/AVIF for modern browsers
- Responsive images: Use srcset and sizes attributes to serve different image sizes based on viewport
- Lazy loading: Only load images when they're close to the viewport
- Modern compression: Use tools like sharp, imagemin, or squoosh
Responsive and Lazy Loaded Images
<img
src="small.jpg"
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1600px"
alt="Responsive image"
loading="lazy"
decoding="async"
width="800"
height="600"
>
CSS Optimization
- Minification: Remove unnecessary whitespace, comments, and characters
- Critical CSS: Inline critical styles to reduce render-blocking
- Remove unused CSS: Tools like PurgeCSS can eliminate unused rules
- CSS modularization: Only load the CSS needed for each page
Using PurgeCSS with a Build Process
// webpack.config.js with PurgeCSS (simplified)
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
// ...webpack config...
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
purgecss({
content: ['./src/**/*.html', './src/**/*.js'],
// Safelist patterns if needed
safelist: ['whitelisted-class', /^modal-/]
})
]
}
}
}
]
}
]
}
};
JavaScript Optimization
- Minification: Reduce file size by removing unnecessary characters
- Code splitting: Break your JavaScript into smaller chunks
- Tree shaking: Eliminate dead code
- Differential loading: Serve modern JavaScript to modern browsers, and transpiled code to older browsers
Code Splitting with Dynamic Imports
// Without code splitting - loads everything upfront
import { heavyFeature } from './heavy-feature';
// With code splitting - loads on demand
document.getElementById('feature-button').addEventListener('click', async () => {
try {
// Dynamic import - loads the module only when needed
const { heavyFeature } = await import('./heavy-feature');
heavyFeature();
} catch (error) {
console.error('Failed to load feature:', error);
}
});
Font Optimization
- WOFF2 format: Most efficient compression for web fonts
- Font subsetting: Only include the characters you need
- Font display strategies: Use font-display property to control rendering behavior
- Preload critical fonts: Especially for key typographic elements
Optimized Font Loading
<!-- Preload the font file -->
<link rel="preload" href="fonts/my-font.woff2" as="font" type="font/woff2" crossorigin>
<style>
/* Define the font with optimal settings */
@font-face {
font-family: 'MyFont';
src: url('fonts/my-font.woff2') format('woff2'),
url('fonts/my-font.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap; /* Show fallback font until custom font is loaded */
}
/* Apply the font */
body {
font-family: 'MyFont', sans-serif;
}
</style>
Delivery Optimization
Caching Strategies
- Browser caching: Set appropriate cache headers for different resource types
- Content fingerprinting: Include content hashes in filenames for cache busting
- Service Workers: Implement offline caching for PWAs
- Cache-Control policies: Configure fine-grained caching behavior
Setting Cache Headers in Various Environments
// Express.js (Node.js) example
app.use('/static', express.static('public', {
maxAge: '1y',
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
// Don't cache HTML files
res.setHeader('Cache-Control', 'no-cache');
} else if (path.includes('critical.css')) {
// Short cache for critical CSS
res.setHeader('Cache-Control', 'public, max-age=3600');
} else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
// Long cache for static assets
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}
}));
// Nginx configuration
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~* \.(html)$ {
add_header Cache-Control "no-cache";
}
Content Delivery Networks (CDNs)
- Distribute content: Serve assets from geographically distributed servers
- Edge caching: Cache content at the network edge, closer to users
- Advanced features: Many CDNs offer image optimization, minification, and more
- HTTP/2 or HTTP/3 support: Benefit from modern protocol optimizations
Integrating with a CDN
// Configure your build tool to output to your CDN
// webpack.config.js example
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/assets/'
: '/'
}
};
Compression
- Gzip/Brotli: Enable HTTP compression for text-based resources
- Precompression: Compress files at build time rather than on-demand
- Appropriate compression levels: Balance compression ratio vs. CPU usage
Enabling Compression in Nginx
# Nginx configuration for Brotli and Gzip
# Enable Brotli
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml application/xml+rss;
# Enable Gzip as fallback
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml+rss;
HTTP/2 and HTTP/3
- Multiplexing: Send multiple requests and responses in parallel over a single connection
- Header compression: Reduce overhead of HTTP headers
- Server push: Proactively send resources the client will need
- HTTP/3: Uses QUIC protocol for reduced latency and improved connection migration
Setting Up HTTP/2 in Nginx
# Nginx configuration for HTTP/2
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HTTP/2 server push
location / {
http2_push /styles/main.css;
http2_push /scripts/main.js;
}
}
Runtime Optimization
Debouncing and Throttling
- Debouncing: Group multiple sequential calls into a single one after a period of inactivity
- Throttling: Limit the number of times a function can be called over time
- Common use cases: Search inputs, scroll listeners, resize events, etc.
Implementing Debounce and Throttle
// Debounce function - executes after a delay of inactivity
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Throttle function - executes at most once per specified time period
function throttle(func, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
func.apply(this, args);
}
};
}
// Usage examples
const debouncedSearch = debounce(function(query) {
// Search implementation - called 300ms after user stops typing
performSearch(query);
}, 300);
const throttledScroll = throttle(function() {
// Scroll handler - called at most once every 100ms
updateScrollIndicator();
}, 100);
// Event listeners
document.getElementById('search').addEventListener('input', function(e) {
debouncedSearch(e.target.value);
});
window.addEventListener('scroll', throttledScroll);
Virtualization
- Virtual scrolling/windowing: Only render visible items in long lists
- Common libraries: react-window, vue-virtual-scroller, etc.
- Use cases: Infinite scrolling lists, data tables, etc.
Basic Virtual List Implementation
// Simplified virtual list using vanilla JS
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2;
this.scrollTop = 0;
this.startIndex = 0;
this.init();
}
init() {
// Create a wrapper that will have the height of all items
this.wrapper = document.createElement('div');
this.wrapper.style.position = 'relative';
this.wrapper.style.height = `${this.items.length * this.itemHeight}px`;
this.container.appendChild(this.wrapper);
// Handle scroll events
this.container.addEventListener('scroll', this.handleScroll.bind(this));
// Initial render
this.render();
}
handleScroll() {
this.scrollTop = this.container.scrollTop;
this.render();
}
render() {
// Calculate visible range
this.startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = Math.min(
this.startIndex + this.visibleItems,
this.items.length
);
// Clear current content
this.wrapper.innerHTML = '';
// Render only visible items
for (let i = this.startIndex; i < endIndex; i++) {
const itemEl = document.createElement('div');
itemEl.style.position = 'absolute';
itemEl.style.top = `${i * this.itemHeight}px`;
itemEl.style.height = `${this.itemHeight}px`;
itemEl.style.width = '100%';
itemEl.textContent = this.items[i];
this.wrapper.appendChild(itemEl);
}
}
}
// Usage
const container = document.getElementById('list-container');
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
new VirtualList(container, items, 40);
Passive Event Listeners
- Purpose: Indicate that a listener will not call preventDefault()
- Benefit: Browser can optimize scrolling and other operations
- Common use cases: Scroll, touchstart, touchmove listeners
Using Passive Event Listeners
// Without passive option - browser doesn't know if preventDefault() will be called
window.addEventListener('scroll', function(event) {
// Scroll handler
});
// With passive option - browser knows preventDefault() won't be called
window.addEventListener('scroll', function(event) {
// Same scroll handler
}, { passive: true });
// Feature detection for passive support
let passiveSupported = false;
try {
const options = {
get passive() {
passiveSupported = true;
return false;
}
};
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (err) {
passiveSupported = false;
}
// Use passive if supported
window.addEventListener('scroll', handleScroll,
passiveSupported ? { passive: true } : false);
Efficient Animations
- Use requestAnimationFrame: Synchronize animations with the browser's refresh cycle
- Promote to GPU: Use transform and opacity for hardware acceleration
- Reduce paint area: Limit the region that needs repainting
- Consider Web Animations API: More efficient than CSS animations in some cases
Efficient Animation with requestAnimationFrame
// Inefficient animation using setTimeout
function badAnimation() {
let position = 0;
const element = document.getElementById('moving-element');
function update() {
position += 2;
element.style.left = position + 'px';
if (position < 500) {
setTimeout(update, 16); // ~60fps, but not synchronized with screen refresh
}
}
update();
}
// Efficient animation using requestAnimationFrame
function goodAnimation() {
let position = 0;
const element = document.getElementById('moving-element');
function update() {
position += 2;
// Use transform instead of left for GPU acceleration
element.style.transform = `translateX(${position}px)`;
if (position < 500) {
requestAnimationFrame(update); // Synchronized with screen refresh
}
}
requestAnimationFrame(update);
}
Ongoing Performance Monitoring
Web performance isn't a one-time effort—it requires ongoing monitoring and optimization. Here's how to set up a sustainable performance monitoring system.
Real User Monitoring (RUM)
Real User Monitoring collects performance data from actual users of your website, giving you insights into real-world conditions.
Implementing Basic RUM
Use the Performance API to collect key metrics from real users:
// Basic implementation of Real User Monitoring
window.addEventListener('load', () => {
// Ensure all the resources are loaded before measuring
setTimeout(() => {
// Get performance entries
const perfData = performance.getEntriesByType('navigation')[0];
const paintEntries = performance.getEntriesByType('paint');
// Extract key metrics
const metrics = {
// Navigation timing
TTFB: perfData.responseStart - perfData.requestStart,
// Page load time
loadTime: perfData.loadEventEnd - perfData.startTime,
// DOM content loaded
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.startTime,
// First Paint
firstPaint: getFirstPaint(paintEntries),
// First Contentful Paint
firstContentfulPaint: getFirstContentfulPaint(paintEntries),
// Page URL and timestamp
url: window.location.href,
timestamp: new Date().toISOString()
};
// Send metrics to analytics endpoint
sendMetricsToAnalytics(metrics);
}, 0);
});
// Helper functions
function getFirstPaint(entries) {
const fp = entries.find(entry => entry.name === 'first-paint');
return fp ? fp.startTime : 0;
}
function getFirstContentfulPaint(entries) {
const fcp = entries.find(entry => entry.name === 'first-contentful-paint');
return fcp ? fcp.startTime : 0;
}
function sendMetricsToAnalytics(metrics) {
// Use Navigator.sendBeacon for reliability
navigator.sendBeacon('/analytics/performance', JSON.stringify(metrics));
}
Web Vitals Monitoring
Google's Web Vitals library makes it easy to collect Core Web Vitals metrics:
// Using the web-vitals library
import {getLCP, getFID, getCLS} from 'web-vitals';
function sendToAnalytics({name, delta, id}) {
// Send metric to your analytics service
const analyticsData = {
metric: name,
value: delta,
id: id,
page: window.location.pathname,
timestamp: Date.now()
};
navigator.sendBeacon('/analytics/vitals', JSON.stringify(analyticsData));
}
// Monitor Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
RUM Tools and Services
Several services provide comprehensive RUM capabilities:
- Open source: Boomerang, Perfume.js
- Commercial: New Relic, Datadog, Dynatrace, SpeedCurve
- Analytics platforms: Google Analytics also provides some performance insights
Synthetic Testing
While RUM gives you data from real users, synthetic testing allows for consistent, controlled measurements over time.
Continuous Performance Testing
Integrate performance testing into your CI/CD pipeline:
Using Lighthouse CI
// .lighthouserc.js configuration
module.exports = {
ci: {
collect: {
// Number of runs
numberOfRuns: 3,
// URLs to test
url: ['https://example.com/', 'https://example.com/about'],
// Chrome flags
chromeFlags: '--headless',
// Simulated throttling settings
settings: {
throttlingMethod: 'simulate',
throttling: {
cpuSlowdownMultiplier: 4,
networkDownloadSpeed: 1638400,
networkUploadSpeed: 675840,
networkRTT: 150
}
}
},
assert: {
// Performance budget assertions
assertions: {
'categories:performance': ['error', {minScore: 0.9}],
'first-contentful-paint': ['error', {maxNumericValue: 2000}],
'interactive': ['error', {maxNumericValue: 3500}],
'largest-contentful-paint': ['error', {maxNumericValue: 2500}],
'cumulative-layout-shift': ['error', {maxNumericValue: 0.1}],
'max-potential-fid': ['error', {maxNumericValue: 100}]
}
},
upload: {
// Upload results to a Lighthouse CI server
target: 'lhci',
serverBaseUrl: 'https://your-lhci-server.example.com'
}
}
};
Scheduled Performance Tests
Regular tests help identify performance regressions over time:
- WebPageTest API: Schedule regular tests from different locations
- Lighthouse on a schedule: Run via cron jobs or scheduled workflows
- Puppeteer/Playwright scripts: Custom performance measurement scripts
Performance Data Visualization
Visualizing performance data helps identify trends and communicate issues to stakeholders.
Performance Dashboards
Create dashboards to monitor key metrics over time:
- Time-series graphs: Track metrics over time to identify trends and regressions
- Heatmaps: Visualize distributions of performance across users
- Geographic visualizations: Understand performance differences by region
Performance Alerts
Set up alerts for performance regressions:
- Static thresholds: Alert when metrics exceed specific values
- Statistical anomaly detection: Alert on unusual changes compared to historical data
- Integration with communication tools: Send alerts to Slack, email, etc.
Building a Performance Culture
Technical solutions are only part of the equation. Creating a culture that values performance is equally important.
Performance Budgets
Establish and enforce performance budgets:
- Set clear limits: Define acceptable thresholds for key metrics
- Enforce in CI/CD: Block deployments that breach budgets
- Create visibility: Make performance metrics visible to all team members
Performance Reviews
Regularly review performance as part of the development process:
- Pre-release reviews: Check performance before major releases
- Cross-functional reviews: Include designers, product managers, and developers
- Learning reviews: Analyze what worked and what didn't after releases
Performance Champions
Designate team members to advocate for performance:
- Expertise development: Invest in training and tools for champions
- Best practice sharing: Regular knowledge sharing sessions
- Recognition: Acknowledge and reward performance improvements
Real-World Performance Case Studies
Learning from real examples can provide valuable insights and inspiration. Here are a few case studies showcasing effective performance optimizations:
E-commerce Site Optimization
Challenge: An e-commerce site was experiencing high bounce rates on product pages, particularly on mobile devices. Analysis showed that the page load time was averaging 8 seconds on 3G connections.
Key Optimizations:
- Image optimization: Implemented responsive images and WebP format with JPEG fallbacks, reducing image weight by 60%
- Route-based code splitting: Only loaded JavaScript needed for the current page
- Critical CSS inlining: Delivered above-the-fold CSS inline and deferred the rest
- Lazy loading: Implemented for product images below the fold
- CDN implementation: Distributed static assets globally to reduce latency
Results:
- Page load time reduced from 8 seconds to 2.5 seconds on 3G connections
- Mobile bounce rate decreased by 28%
- Conversion rate improved by 15%
Key Implementation: Responsive Images with WebP
<!-- Before optimization -->
<img src="product-large.jpg" alt="Product">
<!-- After optimization -->
<picture>
<source type="image/webp"
srcset="product-small.webp 400w,
product-medium.webp 800w,
product-large.webp 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw">
<source type="image/jpeg"
srcset="product-small.jpg 400w,
product-medium.jpg 800w,
product-large.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw">
<img src="product-medium.jpg" alt="Product"
loading="lazy"
width="800" height="600">
</picture>
News Website Performance Improvement
Challenge: A news website was struggling with poor Core Web Vitals scores, particularly Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS). This was affecting both user experience and search rankings.
Key Optimizations:
- Font optimization: Preloaded custom fonts and used font-display: swap
- Layout stability: Set explicit dimensions for all ads, embeds, and images
- Resource prioritization: Used resource hints (preconnect, preload) for critical resources
- JavaScript optimization: Deferred non-critical scripts and reduced third-party impact
- Server optimization: Improved TTFB with server-side caching and database query optimization
Results:
- LCP improved from 4.2s to 1.9s
- CLS reduced from 0.25 to 0.05
- Pages passing Core Web Vitals increased from 22% to 87%
- User engagement (time on site) increased by 17%
Key Implementation: Preventing Layout Shifts
<!-- Before - ad causing layout shift -->
<div class="ad-container">
<!-- Ad script inserts content here -->
</div>
<!-- After - reserved space prevents layout shift -->
<div class="ad-container" style="min-height: 250px; min-width: 300px;">
<!-- Ad script inserts content here -->
</div>
/* CSS for images to prevent layout shifts */
.article-image {
aspect-ratio: 16 / 9;
width: 100%;
background-color: #f0f0f0; /* Placeholder color */
}
/* CSS for web fonts to prevent layout shifts */
html {
font-display: optional; /* Or use swap if necessary */
}
/* Ensure consistent spacing even before custom fonts load */
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
Single Page Application (SPA) Optimization
Challenge: A React-based dashboard application was taking over 6 seconds to become interactive, leading to poor user experience, especially for users with slower devices.
Key Optimizations:
- Bundle analysis and reduction: Identified and removed unused dependencies
- Component-level code splitting: Loaded components on demand
- Server-side rendering (SSR): Implemented initial server rendering for faster First Contentful Paint
- Runtime performance: Optimized React rendering with memoization and virtualizing long lists
- Progressive enhancement: Showed usable content before JavaScript loaded completely
Results:
- Time to Interactive reduced from 6.2s to 2.3s
- JavaScript bundle size reduced by 45%
- CPU usage during interaction reduced by 30%
- User-reported satisfaction increased by 40%
Key Implementation: Component-Level Code Splitting
// Before optimization - importing everything upfront
import Dashboard from './components/Dashboard';
import Reports from './components/Reports';
import Analytics from './components/Analytics';
import Settings from './components/Settings';
// After optimization - dynamic imports
import React, { lazy, Suspense } from 'react';
// Only the initial route component is loaded eagerly
import Dashboard from './components/Dashboard';
// Other components are loaded on demand
const Reports = lazy(() => import('./components/Reports'));
const Analytics = lazy(() => import('./components/Analytics'));
const Settings = lazy(() => import('./components/Settings'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/" exact component={Dashboard} />
<Route path="/reports" component={Reports} />
<Route path="/analytics" component={Analytics} />
<Route path="/settings" component={Settings} />
</Switch>
</Suspense>
</Router>
);
}
Web Performance Checklist
Use this comprehensive checklist to evaluate and improve the performance of your web applications. This can serve as a guide during development or as an audit tool for existing projects.
Initial Loading Performance
Resource Optimization
- ☐ Minify and compress HTML, CSS, and JavaScript
- ☐ Optimize images using modern formats and proper sizing
- ☐ Implement responsive images with srcset and sizes
- ☐ Use lazy loading for below-the-fold images and iframes
- ☐ Optimize fonts with proper format selection and font-display
- ☐ Remove unused CSS and JavaScript from the bundle
Critical Rendering Path
- ☐ Minimize render-blocking resources by deferring or asynchronously loading non-critical scripts
- ☐ Inline critical CSS for above-the-fold content
- ☐ Use server-side rendering or pre-rendering where appropriate
- ☐ Implement resource hints (preload, preconnect, prefetch) for critical resources
- ☐ Ensure efficient order of loading for critical resources
Delivery Optimization
- ☐ Enable compression (Gzip or Brotli) for text-based resources
- ☐ Implement proper caching with appropriate headers
- ☐ Use a CDN for static assets
- ☐ Enable HTTP/2 or HTTP/3 for multiplexing and reduced latency
- ☐ Optimize server response time (TTFB)
JavaScript Delivery
- ☐ Split code into smaller bundles
- ☐ Implement tree shaking to remove unused code
- ☐ Use modern script loading attributes (async/defer) appropriately
- ☐ Prioritize critical JavaScript execution
- ☐ Consider module/nomodule pattern for differential serving
Runtime Performance
JavaScript Efficiency
- ☐ Optimize long-running JavaScript tasks
- ☐ Use Web Workers for CPU-intensive operations
- ☐ Implement virtualization for long lists
- ☐ Throttle and debounce event listeners appropriately
- ☐ Use passive event listeners for scroll and touch events
- ☐ Avoid memory leaks by cleaning up event listeners and references
Rendering Performance
- ☐ Minimize layout thrashing by batching DOM reads and writes
- ☐ Use CSS transforms and opacity for animations
- ☐ Avoid forced synchronous layouts
- ☐ Use requestAnimationFrame for visual updates
- ☐ Reduce paint complexity and paint areas
- ☐ Use will-change appropriately (but sparingly)
Layout Stability
- ☐ Set explicit dimensions for images and media
- ☐ Reserve space for dynamic content like ads and embeds
- ☐ Avoid inserting content above existing content
- ☐ Use transform animations instead of animating properties that trigger layout
Performance Monitoring
Metrics and Measurement
- ☐ Track Core Web Vitals (LCP, FID, CLS)
- ☐ Implement Real User Monitoring (RUM) for field data
- ☐ Set up synthetic testing for lab data
- ☐ Create performance dashboards for visibility
- ☐ Establish performance budgets and enforce them
Continuous Improvement
- ☐ Include performance testing in CI/CD pipeline
- ☐ Set up performance regression alerts
- ☐ Review performance metrics regularly
- ☐ Conduct performance audits before major releases
- ☐ Foster a performance culture within the development team
Mobile-First Performance Strategy
With mobile traffic dominating for many websites, a mobile-first approach to performance is essential. This means optimizing for mobile constraints first, then enhancing the experience for more capable devices and connections.
Understanding Mobile Constraints
Mobile devices face unique challenges:
- Network variability: Mobile connections can range from fast 5G to spotty 2G or worse
- CPU limitations: Mobile processors are typically less powerful than desktop CPUs
- Memory constraints: Less RAM means more aggressive background tab handling
- Battery concerns: Performance-intensive websites drain batteries faster
- Input methods: Touch interfaces have different interaction patterns than mouse/keyboard
Progressive Enhancement
Start with a minimal but functional experience, then enhance it for more capable devices:
/* Base styles for all devices */
.card {
padding: 1rem;
margin-bottom: 1rem;
background-color: #fff;
border: 1px solid #ddd;
}
/* Enhanced styles for devices with hover capability */
@media (hover: hover) {
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
/* Enhanced styles for larger screens with more capabilities */
@media (min-width: 768px) and (min-resolution: 2dppx) {
.card {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
}
Adaptive Loading
Adjust the experience based on the user's device capabilities and network conditions:
// Detect network conditions and capabilities
function getExperienceLevel() {
// Start with the assumption of a constrained experience
let level = 'lite';
// Check for device memory
if (navigator.deviceMemory > 4) {
level = 'medium';
}
// Check for network conditions
if (navigator.connection) {
const connection = navigator.connection;
// Low data mode
if (connection.saveData) {
return 'lite'; // Override with lite experience
}
// Fast connection
if (connection.effectiveType === '4g' && !connection.saveData) {
level = level === 'medium' ? 'full' : 'medium';
}
}
return level;
}
// Adjust the experience based on capabilities
function loadAppropriateExperience() {
const experienceLevel = getExperienceLevel();
switch (experienceLevel) {
case 'lite':
// Load minimal assets
loadLiteAssets();
break;
case 'medium':
// Load standard assets
loadMediumAssets();
break;
case 'full':
// Load enhanced assets
loadFullAssets();
break;
}
}
// Example implementations
function loadLiteAssets() {
// Load low-resolution images
document.querySelectorAll('[data-src-lite]').forEach(img => {
img.src = img.getAttribute('data-src-lite');
});
// Load essential scripts only
import('./essential.js');
}
function loadMediumAssets() {
// Load standard resolution images
document.querySelectorAll('[data-src-medium]').forEach(img => {
img.src = img.getAttribute('data-src-medium');
});
// Load standard features
import('./essential.js').then(() => import('./standard.js'));
}
function loadFullAssets() {
// Load high-resolution images
document.querySelectorAll('[data-src-full]').forEach(img => {
img.src = img.getAttribute('data-src-full');
});
// Load all features
import('./essential.js')
.then(() => import('./standard.js'))
.then(() => import('./enhanced.js'));
}
Offline Support with Service Workers
Service workers enable offline functionality and can significantly improve perceived performance:
// Register a service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registered:', registration.scope);
})
.catch(error => {
console.error('ServiceWorker registration failed:', error);
});
});
}
// In sw.js (Service Worker file)
const CACHE_NAME = 'v1-cache';
const URLS_TO_CACHE = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/main.js',
'/offline.html',
'/images/logo.svg'
];
// Install event - cache critical assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(URLS_TO_CACHE);
})
);
});
// Fetch event - serve from cache or network
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return the response from cache
if (response) {
return response;
}
// No cache match - fetch from network
return fetch(event.request)
.then(response => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response as it can only be used once
const responseToCache = response.clone();
// Open the cache and store the response
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// Network failed - serve offline page for HTML requests
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
Touch Optimization
Optimize for touch interactions to improve perceived performance:
- Make touch targets at least 44×44 pixels for easy interaction
- Implement 'instant' visual feedback for touch actions
- Minimize touch delay by avoiding unnecessary event listeners
- Ensure smooth scrolling with optimized event handlers
/* CSS for touch-friendly targets */
.button {
min-width: 44px;
min-height: 44px;
padding: 12px 16px;
/* Visual padding can be smaller than the actual touch target */
}
/* CSS for instant touch feedback */
.button {
transition: background-color 0.1s;
}
.button:active {
background-color: #e0e0e0;
/* Fast transition for active state */
}
/* JavaScript for smoother scrolling */
// Use passive listeners for scroll events
document.addEventListener('scroll', function() {
// Scroll handling code
}, { passive: true });
Future Trends in Web Performance
The web performance landscape continually evolves. Here are some trends to watch and prepare for:
Core Web Vitals Evolution
Google's Core Web Vitals will continue to evolve, with new metrics focusing on different aspects of user experience:
- Interaction to Next Paint (INP): Measures overall responsiveness to user interactions
- Smoothness: Measures animation and scrolling fluidity
- Visibility metrics: Focusing on actual visibility of content to users
Staying ahead means preparing your site for these metrics before they become ranking factors.
Machine Learning for Performance Optimization
ML and AI are becoming more prevalent in performance optimization:
- Predictive preloading: ML models that predict which resources a user will need next
- Adaptive delivery: Automatically optimizing content based on device, network, and user patterns
- Intelligent caching: Beyond simple rules to learning-based caching strategies
WebAssembly Adoption
WebAssembly (Wasm) enables near-native performance for CPU-intensive tasks:
- Image and video processing on the client side
- Complex calculations and simulations
- Games and interactive applications with high performance requirements
- Porting existing C/C++/Rust applications to the web
// Loading and using a WebAssembly module
async function loadWasmModule() {
try {
// Fetch and instantiate the WebAssembly module
const response = await fetch('image_processing.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
// Access the exported functions
const { grayscale, blur, sharpen } = instance.exports;
// Example: Process an image with WebAssembly
function processImage(imageData) {
// Get raw pixel data
const pixels = new Uint8Array(imageData.data.buffer);
// Call WebAssembly function for image processing
// This will be much faster than JavaScript equivalent
grayscale(pixels.byteOffset, pixels.length);
return imageData;
}
return {
processImage
};
} catch (err) {
console.error('WebAssembly module loading failed:', err);
// Fallback to JavaScript implementation
return {
processImage: processImageJS
};
}
}
New Browser APIs and Capabilities
Emerging APIs will enable new optimization strategies:
- Portals: For seamless navigations with iframe-like previews
- Scheduler API: For more granular control over task scheduling
- Display Locking: To prevent rendering of off-screen content
- Lazy loading for iframes and JavaScript modules: To defer loading non-critical resources
// Example of the Scheduler API (proposed)
scheduler.postTask(() => {
// Non-urgent work here
}, {
priority: 'background',
delay: 0
});
// Example of Portals API for seamless navigation (experimental)
// Create a portal to the target page
const portal = document.createElement('portal');
portal.src = 'https://example.com/next-page';
document.body.appendChild(portal);
// When ready to navigate, activate the portal
someButton.addEventListener('click', () => {
portal.activate().then(() => {
console.log('Navigation completed');
});
});
Edge Computing
Moving computation closer to the user through edge functions and CDN-integrated computing:
- Dynamic edge caching: Tailoring cached content at the edge
- Personalization at the edge: Customizing content without round-trips to origin servers
- Edge rendering: Server-side rendering at edge locations for faster delivery
Practical Exercise: Performance Audit and Optimization
Let's apply what we've learned with a hands-on exercise. In this exercise, you'll audit a web page for performance issues and implement optimizations to improve its speed and user experience.
Step 1: Initial Performance Analysis
- Choose a web page to optimize (your own project or a sample page)
- Run a Lighthouse audit in Chrome DevTools (Lighthouse tab)
- Note the key performance metrics and opportunities for improvement
- Use the Network panel to identify slow-loading resources
- Use the Performance panel to identify JavaScript and rendering bottlenecks
Step 2: Create a Performance Improvement Plan
Based on your analysis, create a prioritized list of optimizations to implement. Focus on:
- Quick wins with high impact
- Critical rendering path optimizations
- Resource optimizations (images, scripts, styles)
- Runtime performance improvements
Step 3: Implement Optimizations
Implement the planned optimizations. Here are some examples:
Image Optimization
<!-- Before -->
<img src="large-image.jpg" alt="Description">
<!-- After -->
<img src="large-image.jpg"
srcset="small-image.jpg 400w, medium-image.jpg 800w, large-image.jpg 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
alt="Description"
loading="lazy"
width="800"
height="600">
Critical CSS Extraction
<!-- Before -->
<link rel="stylesheet" href="styles.css">
<!-- After -->
<style>
/* Critical CSS inlined */
body { font-family: sans-serif; margin: 0; }
.header { background: #f5f5f5; padding: 1rem; }
.hero { height: 50vh; background: linear-gradient(#1a2a6c, #b21f1f); color: white; display: flex; align-items: center; justify-content: center; }
/* End critical CSS */
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
JavaScript Optimization
<!-- Before -->
<script src="app.js"></script>
<!-- After -->
<script src="critical.js"></script>
<script src="app.js" defer></script>
// In app.js - use dynamic imports for non-critical features
document.getElementById('feature-button').addEventListener('click', async () => {
const { initFeature } = await import('./feature.js');
initFeature();
});
Step 4: Measure Improvement
- Run another Lighthouse audit after implementing your optimizations
- Compare before and after metrics
- Identify remaining opportunities for improvement
- Document what worked best and what didn't make a significant difference
Step 5: Advanced Optimizations
If time permits, implement more advanced optimizations:
- Set up a simple service worker for caching
- Implement resource hints (preconnect, preload) for critical resources
- Add performance monitoring with the Performance API
- Optimize third-party script loading
Simple Performance Monitoring
// Add to your main JavaScript file
window.addEventListener('load', () => {
// Wait for all resources to finish loading
setTimeout(() => {
// Get performance metrics
const perfEntries = performance.getEntriesByType('navigation')[0];
const paintEntries = performance.getEntriesByType('paint');
// Calculate key metrics
const metrics = {
// Time to First Byte (TTFB)
ttfb: perfEntries.responseStart - perfEntries.requestStart,
// DOM Content Loaded
domContentLoaded: perfEntries.domContentLoadedEventEnd - perfEntries.startTime,
// Load Time
loadTime: perfEntries.loadEventEnd - perfEntries.startTime,
// First Paint
firstPaint: findMetric(paintEntries, 'first-paint'),
// First Contentful Paint
firstContentfulPaint: findMetric(paintEntries, 'first-contentful-paint')
};
// Log metrics
console.table(metrics);
// Send to analytics if needed
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', JSON.stringify(metrics));
}
}, 0);
});
function findMetric(entries, name) {
const entry = entries.find(entry => entry.name === name);
return entry ? entry.startTime : 0;
}
Conclusion
Web performance is a cornerstone of exceptional user experience. By understanding the principles, measuring the right metrics, and implementing appropriate optimizations, you can create web applications that load quickly, respond immediately, and provide a smooth experience across all devices and networks.
Key Takeaways
- Performance matters for user experience, business metrics, and SEO
- Measure before optimizing to identify the most impactful improvements
- Focus on the critical rendering path to improve perceived performance
- Optimize resource loading with proper sizing, formats, and delivery strategies
- Consider all users, especially those on mobile and slow connections
- Establish a performance culture with ongoing monitoring and maintenance
Continuing Your Performance Journey
Web performance is a deep field with much to explore. Here are resources to continue your learning:
Remember, performance optimization is a continuous process, not a one-time task. As web technologies evolve and user expectations increase, the pursuit of better performance will remain a worthy investment of your time and effort. The skills you've learned in this session form a foundation that will serve you well throughout your career as a web developer.