Security & Performance

Performance Optimization Project (Part 1)

20 min Lesson 32 of 35

Performance Optimization Project (Part 1)

Performance optimization is a systematic process of identifying bottlenecks and implementing targeted improvements. In this hands-on project, we'll audit a slow application, identify performance issues, and implement comprehensive frontend optimizations. This two-part project will transform a sluggish application into a high-performance system.

Project Overview

We'll optimize a real-world e-commerce application that's experiencing significant performance issues. The application has a 7-second load time, poor Lighthouse scores, and frustrated users. Our goal is to achieve sub-2-second load times and excellent performance metrics.

Project Goals:
- Reduce initial page load from 7s to <2s
- Achieve Lighthouse score >90
- Implement comprehensive caching
- Optimize images and assets
- Improve perceived performance

Initial Performance Audit

Before making any changes, establish baseline metrics. Use multiple tools to get a complete picture:

<!-- Performance Audit Checklist -->

1. Google Lighthouse Audit:
# Chrome DevTools > Lighthouse
# Run audit in incognito mode
# Test on 3G and 4G connections

Initial Results (Before Optimization):
Performance: 42/100
First Contentful Paint: 3.2s
Largest Contentful Paint: 7.1s
Time to Interactive: 8.5s
Total Blocking Time: 2,140ms
Cumulative Layout Shift: 0.18

2. WebPageTest Analysis:
# Visit webpagetest.org
# Test from multiple locations
# Use 3G connection profile

Key Findings:
- 127 HTTP requests
- 4.2 MB total page weight
- No text compression
- Unoptimized images (3.1 MB)
- Render-blocking resources
- No browser caching

3. Chrome DevTools Network Panel:
# Open DevTools > Network
# Disable cache
# Record full page load

Observations:
- 12 CSS files (320 KB total)
- 15 JavaScript files (890 KB total)
- 45 images (3.1 MB total)
- No CDN usage
- Synchronous script loading

4. Chrome DevTools Performance Panel:
# DevTools > Performance
# Record page load
# Analyze main thread activity

Bottlenecks Identified:
- Long JavaScript execution (2.1s)
- Excessive layout thrashing
- Unoptimized third-party scripts
- Large DOM size (1,842 nodes)

Prioritizing Optimizations

Based on the audit, prioritize optimizations by impact and effort:

<!-- Optimization Priority Matrix -->

HIGH IMPACT, LOW EFFORT:
✓ Enable text compression (gzip/brotli)
✓ Implement browser caching
✓ Minify CSS and JavaScript
✓ Optimize images (modern formats, compression)
✓ Eliminate render-blocking resources

HIGH IMPACT, MEDIUM EFFORT:
✓ Implement code splitting
✓ Lazy load images and components
✓ Optimize critical rendering path
✓ Reduce third-party script impact

MEDIUM IMPACT, LOW EFFORT:
✓ Remove unused CSS/JS
✓ Optimize web fonts
✓ Implement resource hints
✓ Reduce DOM complexity

Implementation Order:
1. Frontend optimizations (Part 1)
2. Server and database optimizations (Part 2)

Step 1: Enable Text Compression

Compress all text-based assets (HTML, CSS, JS, JSON) to reduce transfer size:

<!-- Apache .htaccess Configuration -->

# Enable gzip compression
<IfModule mod_deflate.c>
# Compress HTML, CSS, JavaScript, Text, XML
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE text/plain
</IfModule>

<!-- Nginx Configuration -->

# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
text/xml;

# Enable brotli (if module available)
brotli on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
text/javascript
application/javascript
application/json;

Results:
- CSS: 320 KB → 58 KB (82% reduction)
- JavaScript: 890 KB → 246 KB (72% reduction)
- HTML: 42 KB → 12 KB (71% reduction)
Total Savings: 936 KB

Step 2: Implement Browser Caching

Configure aggressive caching for static assets to eliminate redundant requests:

<!-- Apache Caching Configuration -->

<IfModule mod_expires.c>
ExpiresActive On

# Images
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"

# CSS and JavaScript
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"

# Fonts
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"

# HTML (shorter cache)
ExpiresByType text/html "access plus 1 hour"
</IfModule>

<!-- Cache-Control Headers -->

<IfModule mod_headers.c>
# Versioned assets - aggressive caching
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|css|js|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

# HTML - validate on each request
<FilesMatch "\.html$">
Header set Cache-Control "public, max-age=0, must-revalidate"
</FilesMatch>
</IfModule>

Results:
- Returning visitors: 0 asset requests (all cached)
- Page load time for return visits: 7s → 1.2s

Step 3: Image Optimization

Images account for 3.1 MB of the 4.2 MB page weight. Comprehensive image optimization is critical:

<!-- Image Optimization Strategy -->

1. Convert to Modern Formats:
# Install sharp for Node.js
npm install sharp

# Conversion script (convert-images.js)
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

async function convertToWebP(inputPath, outputPath) {
await sharp(inputPath)
.webp({ quality: 85 })
.toFile(outputPath);
}

async function convertToAVIF(inputPath, outputPath) {
await sharp(inputPath)
.avif({ quality: 75 })
.toFile(outputPath);
}

// Process all product images
const imagesDir = './public/images/products';
fs.readdirSync(imagesDir).forEach(async (file) => {
if (file.match(/\.(jpg|jpeg|png)$/)) {
const inputPath = path.join(imagesDir, file);
const baseName = file.replace(/\.(jpg|jpeg|png)$/, '');

await convertToWebP(inputPath,
path.join(imagesDir, baseName + '.webp'));
await convertToAVIF(inputPath,
path.join(imagesDir, baseName + '.avif'));
}
});

2. Implement Responsive Images:
<!-- Modern responsive image markup -->
<picture>
<source
type="image/avif"
srcset="product-320.avif 320w,
product-640.avif 640w,
product-1280.avif 1280w"
sizes="(max-width: 640px) 100vw, 640px">
<source
type="image/webp"
srcset="product-320.webp 320w,
product-640.webp 640w,
product-1280.webp 1280w"
sizes="(max-width: 640px) 100vw, 640px">
<img
src="product-640.jpg"
srcset="product-320.jpg 320w,
product-640.jpg 640w,
product-1280.jpg 1280w"
sizes="(max-width: 640px) 100vw, 640px"
alt="Product name"
loading="lazy"
decoding="async">
</picture>

3. Optimize Original JPEGs:
# Using imagemagick
mogrify -strip -quality 85 -sampling-factor 4:2:0 *.jpg

4. Generate Multiple Sizes:
# Automated resize script
for img in *.jpg; do
convert $img -resize 320x product-320-${img}
convert $img -resize 640x product-640-${img}
convert $img -resize 1280x product-1280-${img}
done

Results:
- AVIF: 3.1 MB → 420 KB (86% reduction)
- WebP: 3.1 MB → 680 KB (78% reduction)
- Optimized JPEG fallback: 3.1 MB → 1.1 MB (65% reduction)
- With lazy loading: Initial load only 180 KB
Pro Tip: Use the `<picture>` element with multiple sources. Modern browsers will automatically select the best format they support (AVIF > WebP > JPEG).

Step 4: Implement Lazy Loading

Defer loading of off-screen images and components until needed:

<!-- Native Lazy Loading -->

<!-- Simple lazy loading for images -->
<img src="product.jpg" alt="Product" loading="lazy">

<!-- Advanced lazy loading with Intersection Observer -->
<script>
// Lazy load images
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.srcset = img.dataset.srcset;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
}, {
rootMargin: '50px' // Start loading 50px before visible
});

document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});

// Lazy load components
const componentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const component = entry.target;
// Load component code
import(component.dataset.component).then(module => {
module.init(component);
});
componentObserver.unobserve(component);
}
});
});

document.querySelectorAll('[data-component]').forEach(el => {
componentObserver.observe(el);
});
</script>

<!-- HTML markup for lazy loading -->
<img
class="lazy"
data-src="product-full.jpg"
data-srcset="product-320.jpg 320w, product-640.jpg 640w"
src="placeholder.jpg"
alt="Product name">

Results:
- Initial images loaded: 45 → 6
- Initial page weight: 4.2 MB → 850 KB
- LCP improved: 7.1s → 3.2s

Step 5: Eliminate Render-Blocking Resources

Remove resources that block initial page rendering:

<!-- Original (Render-Blocking) -->
<head>
<link rel="stylesheet" href="normalize.css">
<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="custom.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
</head>

<!-- Optimized (Non-Blocking) -->
<head>
<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold CSS (~14KB) */
body { margin: 0; font-family: system-ui; }
.header { /* header styles */ }
.hero { /* hero section styles */ }
</style>

<!-- Preload critical resources -->
<link rel="preload" href="main.css" as="style">
<link rel="preload" href="main.js" as="script">

<!-- Async load non-critical CSS -->
<link rel="preload" href="main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="main.css"></noscript>
</head>
<body>
<!-- Content -->

<!-- Defer JavaScript -->
<script src="main.js" defer></script>
</body>

<!-- Extract Critical CSS (using critical package) -->
# Install critical
npm install critical

# Generate critical CSS
const critical = require('critical');

critical.generate({
inline: true,
base: 'public/',
src: 'index.html',
dest: 'index-critical.html',
width: 1300,
height: 900
});

Results:
- Render-blocking resources: 5 → 0
- First Contentful Paint: 3.2s → 1.1s
- Largest Contentful Paint: 3.2s → 2.1s

Step 6: Minify and Bundle Assets

Reduce file sizes and HTTP requests through minification and bundling:

<!-- Webpack Configuration -->

// webpack.config.js
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
vendor: ['jquery', 'bootstrap']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: { drop_console: true },
format: { comments: false }
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendor',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};

Results:
- JavaScript files: 15 → 3 (main, vendor, common)
- CSS files: 12 → 1
- Total JS size: 246 KB (compressed) → 198 KB
- Total CSS size: 58 KB (compressed) → 42 KB
- HTTP requests: 127 → 38

Step 7: Implement Code Splitting

Split code into smaller chunks that load on-demand:

<!-- Route-Based Code Splitting (React example) -->

import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));

function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/products" component={Products} />
<Route path="/product/:id" component={ProductDetail} />
<Route path="/cart" component={Cart} />
<Route path="/checkout" component={Checkout} />
</Switch>
</Suspense>
</Router>
);
}

<!-- Dynamic Import for Features -->

// Load modal component only when needed
button.addEventListener('click', async () => {
const { Modal } = await import('./components/Modal');
const modal = new Modal();
modal.show();
});

Results:
- Initial bundle: 890 KB → 156 KB
- Home page chunk: 156 KB
- Products page chunk: 89 KB (loaded on demand)
- Checkout flow: 124 KB (loaded on demand)
- Time to Interactive: 8.5s → 3.1s
Hands-On Exercise: Optimize a sample application:

1. Download the slow-ecommerce-starter repository
2. Run Lighthouse audit and document baseline metrics
3. Enable text compression on your server
4. Convert 5 product images to WebP and AVIF
5. Implement lazy loading for all product images
6. Extract and inline critical CSS
7. Run Lighthouse again and compare results
8. Document performance improvements

Part 1 Results Summary

After implementing all frontend optimizations:

<!-- Before vs. After Metrics -->

BEFORE OPTIMIZATION:
- Lighthouse Performance: 42/100
- First Contentful Paint: 3.2s
- Largest Contentful Paint: 7.1s
- Time to Interactive: 8.5s
- Total Blocking Time: 2,140ms
- Page Weight: 4.2 MB
- HTTP Requests: 127

AFTER FRONTEND OPTIMIZATION:
- Lighthouse Performance: 78/100 ✓
- First Contentful Paint: 1.1s ✓ (66% improvement)
- Largest Contentful Paint: 2.1s ✓ (70% improvement)
- Time to Interactive: 3.1s ✓ (64% improvement)
- Total Blocking Time: 420ms ✓ (80% improvement)
- Page Weight: 850 KB ✓ (80% reduction)
- HTTP Requests: 38 ✓ (70% reduction)

REMAINING ISSUES (Part 2):
- Slow server response time (1.2s)
- Database query inefficiencies
- No server-side caching
- Unoptimized API endpoints

Excellent progress! We've achieved significant frontend improvements. In Part 2, we'll tackle server-side and database optimizations to reach our goal of sub-2-second load times and a Lighthouse score above 90.