Duration: 1.5 hours (90 minutes)
Level: Intermediate
Last Updated: March 2026
- Introduction
- Frontend Performance Metrics
- Frontend Optimization Techniques
- Network Optimization
- Framework-Level Optimization
- Practical Optimization Workflow
- Conclusion
View recorded workshop on YouTube: https://youtu.be/9weMJYLVwmI
By the end of this workshop, you will be able to:
- Understand why frontend optimization is critical for business success
- Measure and interpret Core Web Vitals metrics
- Apply multiple optimization techniques to reduce page load time
- Implement effective caching and CDN strategies
- Monitor real user performance in production
- Follow a systematic optimization workflow
Frontend optimization is not just about making websites faster—it directly impacts your bottom line and user satisfaction.
Frontend optimization improves:
✅ Website loading speed - Faster pages keep users engaged
✅ User experience - Smooth interactions increase satisfaction
✅ SEO ranking - Google prioritizes fast sites
✅ Conversion rate - Speed directly correlates to sales
✅ Mobile performance - Critical for mobile-first users
mindmap
root((Frontend
Optimization))
🚀 Loading Speed
Faster page loads
Lower TTFB
Smaller assets
🎯 User Experience
Smooth interactions
No layout shifts
Instant responses
📈 SEO Ranking
Google PageSpeed
Core Web Vitals
Mobile-first index
💰 Conversion Rate
1s delay = 7% loss
Fewer abandoned carts
Higher engagement
📱 Mobile Performance
Reduced data usage
Battery efficiency
Offline support
The numbers speak for themselves:
📊 1 second delay → up to 7% conversion loss
📊 53% of users leave if page loads >3 seconds
📊 100ms improvement → 1% revenue increase (Amazon study)
📊 0.1s faster → 8% more conversions (Walmart study)
💡 Example: If your e-commerce site makes $100,000/day, a 1-second delay could cost you $7,000 daily—over $2.5 million annually.
Before we dive into solutions, let's identify the usual culprits:
Problem: Modern web apps often ship megabytes of JavaScript.
// Common issue: importing entire libraries
import _ from 'lodash'; // Imports ALL of lodash (~70KB)
// Better approach: import only what you need
import debounce from 'lodash/debounce'; // Only ~2KBImpact:
- Longer download times
- Slower parsing and execution
- Delayed interactivity
Problem: Each request adds latency, especially on mobile networks.
Typical unoptimized page:
- 50+ JavaScript files
- 30+ CSS files
- 100+ images
- Multiple font files
= 200+ requests 🔴
Problem: Images often account for 50-80% of page weight.
Common mistakes:
❌ 5MB raw camera photo for thumbnail
❌ PNG when JPEG would suffice
❌ No lazy loading for below-the-fold images
❌ Same image for mobile and desktop
Problem: Resources that block rendering delay content visibility.
<!-- BAD: Blocks rendering -->
<head>
<script src="large-analytics.js"></script>
<link rel="stylesheet" href="entire-bootstrap.css">
</head>
<!-- GOOD: Non-blocking -->
<head>
<script defer src="large-analytics.js"></script>
<style>/* Critical CSS inline */</style>
<link rel="preload" href="main.css" as="style">
</head>Problem: Users re-download unchanged files on every visit.
Without caching:
Every visit = Full download
With caching:
First visit = Full download
Subsequent visits = Only changed files
📝 Key Principle: "You can't optimize what you don't measure."
Google's Core Web Vitals are three critical metrics that measure real-world user experience.
What it measures: Loading performance—when the largest visible element appears.
Good LCP: < 2.5 seconds
Needs Improvement: 2.5 - 4.0 seconds
Poor: > 4.0 seconds
graph LR
A[Page Load Starts] -->|LCP < 2.5s| B[✅ Good]
A -->|LCP 2.5-4s| C[⚠️ Needs Improvement]
A -->|LCP > 4s| D[🔴 Poor]
What counts as "largest contentual element"?
- Large images
- Video thumbnail
- Block-level text elements
- Background images loaded via CSS
Common causes of slow LCP:
-
Slow server response times
TTFB (Time to First Byte) > 600ms Solution: Optimize backend, use CDN -
Render-blocking JavaScript and CSS
// Bad: Blocks rendering <script src="huge-bundle.js"></script> // Good: Deferred loading <script defer src="huge-bundle.js"></script>
-
Large image files
5MB image = ~8 seconds on 3G Solution: Compress, use WebP, lazy load -
Client-side rendering delays
Issue: Content generated by JavaScript after download Solution: Server-side rendering (SSR) or static generation
What it measures: Responsiveness—how quickly page reacts to user interactions.
Good INP: < 200 milliseconds
Needs Improvement: 200 - 500 ms
Poor: > 500 ms
Example scenarios:
- User clicks a button → how long until visual feedback?
- User types in a search box → when does the interface update?
- User opens a dropdown menu → delay before it appears?
Common problems:
-
Heavy JavaScript execution
// Bad: Blocking main thread function processLargeData() { for (let i = 0; i < 1000000; i++) { // Expensive computation } } // Good: Break into chunks async function processLargeData() { for (let i = 0; i < 1000; i++) { await new Promise(resolve => setTimeout(resolve, 0)); // Process 1000 items at a time } }
-
Long event handlers
// Bad: Synchronous expensive operations button.addEventListener('click', () => { const result = expensiveCalculation(); updateUI(result); }); // Good: Async operations button.addEventListener('click', async () => { const result = await expensiveCalculation(); updateUI(result); });
-
Large DOM size
DOM with 5000+ nodes = slow interactions Solution: Virtualization, pagination
What it measures: Visual stability—unexpected layout shifts during page load.
Good CLS: < 0.1
Needs Improvement: 0.1 - 0.25
Poor: > 0.25
Classic problem:
User scenario:
1. User sees "Buy Now" button
2. User moves cursor to click
3. Ad loads above, pushes button down
4. User accidentally clicks ad 😤
Common causes and solutions:
-
Images without dimensions
<!-- BAD: Causes layout shift --> <img src="hero.jpg"> <!-- GOOD: Reserve space --> <img src="hero.jpg" width="800" height="600"> <!-- BETTER: Aspect ratio --> <img src="hero.jpg" style="aspect-ratio: 16/9; width: 100%;">
-
Dynamic content insertion
<!-- BAD: Banner appears after content --> <div id="banner"></div> <main>Content here</main> <!-- GOOD: Reserve space --> <div id="banner" style="min-height: 100px"></div> <main>Content here</main>
-
Web fonts causing text reflow
/* BAD: Invisible text, then shift */ @font-face { font-family: 'CustomFont'; src: url('font.woff2'); } /* GOOD: Show fallback, swap when ready */ @font-face { font-family: 'CustomFont'; src: url('font.woff2'); font-display: swap; }
What it measures: When the first piece of DOM content renders.
Good: < 1.8 seconds
sequenceDiagram
participant Browser
participant Server
Browser->>Server: Request page
Server->>Browser: HTML response
Note over Browser: FCP: First text/image visible
Browser->>Server: Request CSS/JS
Note over Browser: LCP: Main content visible
What it measures: When the page becomes fully interactive and responsive.
Good: < 3.8 seconds
Requirements for TTI:
- Page displays useful content (FCP)
- Event handlers registered
- Page responds to interactions within 50ms
What it measures: Total time the main thread was blocked (preventing user input).
Good: < 200 milliseconds
// Blocking task example
function blockingTask() {
const start = Date.now();
while (Date.now() - start < 1000) {
// Blocks for 1 second! 🔴
}
}🎬 Live Demo — run this before starting the tools walkthrough
Open
scripts/demo/demo.htmlin Chrome using a local server:# Option A — Node.js npx serve series/web-optimization/w1-frontend-optimization/scripts/demo -p 8080 # Option B — Python python3 -m http.server 8080 --directory series/web-optimization/w1-frontend-optimization/scripts/demoThen navigate to
http://localhost:8080/demo.htmland run a Lighthouse audit. The page is intentionally built with the following issues:
Metric Intentional Issue LCP Large unoptimised hero image loaded from an external URL CLS <img>has no explicitwidth/height; late-injected banner demoINP / TBT Synchronous ~600 ms spin-loop on interaction; 300 ms blocking task on load TTI Render-blocking script runs before content is interactive Walk students through each failing metric in the Lighthouse report, then show them the corresponding section of the demo page that caused it.
Access: Chrome DevTools → Lighthouse tab
Features:
- Overall performance score (0-100)
- Core Web Vitals measurements
- Specific optimization suggestions
- Accessibility and SEO audits
How to use:
# Run Lighthouse from command line
npm install -g lighthouse
lighthouse https://example.com --viewWhen to use:
- Initial performance audit
- Quick score check
- Development environment testing
URL: https://www.webpagetest.org/
Features:
- Real device testing
- Network waterfall visualization
- Multiple location testing
- Connection speed simulation
- Filmstrip view of loading
Example test:
Test Settings:
- Location: London, UK
- Browser: Chrome
- Connection: 3G
- Runs: 3 (for consistency)
When to use:
- Detailed network analysis
- Real-world device testing
- Comparison testing
Access: DevTools → Performance → Record
Features:
- JavaScript execution timeline
- Rendering bottleneck identification
- Frame rate analysis
- Memory usage tracking
How to use:
- Open DevTools (F12)
- Go to Performance tab
- Click Record
- Interact with page
- Stop recording
- Analyze flame chart
When to use:
- Debugging specific interactions
- Finding JavaScript bottlenecks
- Analyzing rendering performance
JavaScript is often the biggest performance bottleneck in modern web apps.
1MB JavaScript file:
- Download: 2-3 seconds (3G)
- Parse: 0.5-1 second
- Compile: 0.5-1 second
- Execute: 0.5-2 seconds
= 3.5-7 seconds before interactive! 🔴
Definition: Eliminates unused code during the build process.
Example:
// utils.js - Library with many functions
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }
// app.js - Your code only uses one function
import { add } from './utils.js';
console.log(add(2, 3));
// With tree shaking: Only 'add' is bundled
// Without tree shaking: All functions bundledConfiguration (Webpack):
// webpack.config.js
module.exports = {
mode: 'production', // Enables tree shaking
optimization: {
usedExports: true,
sideEffects: false
}
};Configuration (Vite):
Vite uses Rollup under the hood — tree shaking is enabled by default in production builds. No extra config is required for basic usage:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
// Tree shaking is on by default; these are optional fine-tuning options
rollupOptions: {
treeshake: {
// Remove functions marked as side-effect-free
preset: 'recommended',
// Treat property reads on modules as side-effect-free
propertyReadSideEffects: false,
},
},
},
});💡 Tip: Mark individual packages as side-effect-free in
package.jsonso both Vite and Webpack skip unused exports:{ "sideEffects": false }Or allowlist files that do have side effects (e.g., global CSS imports):
{ "sideEffects": ["*.css", "./src/polyfills.js"] }
Definition: Split your code into smaller chunks loaded on demand.
Route-based splitting (Vue Router):
// Bad: Everything loaded upfront
import Dashboard from './Dashboard.vue';
import Profile from './Profile.vue';
import Settings from './Settings.vue';
const routes = [
{ path: '/dashboard', component: Dashboard },
{ path: '/profile', component: Profile },
{ path: '/settings', component: Settings }
];
// Good: Load routes on demand
const routes = [
{
path: '/dashboard',
component: () => import('./Dashboard.vue') // Lazy loaded!
},
{
path: '/profile',
component: () => import('./Profile.vue')
},
{
path: '/settings',
component: () => import('./Settings.vue')
}
];Component-based splitting (React):
import React, { lazy, Suspense } from 'react';
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}Results:
Before code splitting:
- main.js: 800KB
After code splitting:
- main.js: 200KB ✅
- dashboard.chunk.js: 150KB (loaded on demand)
- profile.chunk.js: 100KB (loaded on demand)
- settings.chunk.js: 100KB (loaded on demand)
Audit your dependencies:
# Check bundle size
npm install -g webpack-bundle-analyzer
webpack-bundle-analyzer dist/stats.json
# Find unused dependencies
npm install -g depcheck
depcheckCommon bloat examples:
// Bad: Importing entire library (70KB)
import _ from 'lodash';
const result = _.debounce(fn, 300);
// Good: Import specific function (2KB)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// Bad: Using moment.js (67KB gzipped)
import moment from 'moment';
moment().format('YYYY-MM-DD');
// Good: Using date-fns (2KB gzipped)
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');Principle: Load resources only when needed, not upfront.
Native lazy loading:
<!-- Browser handles lazy loading automatically -->
<img src="below-fold.jpg" loading="lazy" alt="Description">
<!-- Eager loading (default) -->
<img src="hero.jpg" loading="eager" alt="Hero">Browser support: Chrome 77+, Firefox 75+, Edge 79+, Safari 15.4+
Advanced lazy loading with Intersection Observer:
// For older browsers or more control
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Load image
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));<!-- HTML -->
<img data-src="actual-image.jpg"
src="placeholder.jpg"
alt="Description"
class="lazy-image">Vue 3 example:
<template>
<div>
<!-- Heavy component loaded on demand -->
<Suspense>
<template #default>
<AsyncHeavyComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncHeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
</script>Next.js example:
// pages/dashboard.js
import dynamic from 'next/dynamic';
// Lazy load with custom loading component
const DashboardWidget = dynamic(
() => import('../components/DashboardWidget'),
{
loading: () => <p>Loading dashboard...</p>,
ssr: false // Skip server-side rendering
}
);
export default function Dashboard() {
return <DashboardWidget />;
}Images typically represent 50-80% of total page weight.
Format comparison:
Same 1920x1080 image:
- JPEG: 250KB
- PNG: 850KB
- WebP: 150KB (40% smaller than JPEG!)
- AVIF: 100KB (60% smaller than JPEG!)
HTML with fallbacks:
<picture>
<!-- Modern browsers: Use AVIF -->
<source srcset="image.avif" type="image/avif">
<!-- Fallback: WebP -->
<source srcset="image.webp" type="image/webp">
<!-- Fallback: JPEG -->
<img src="image.jpg" alt="Description" loading="lazy">
</picture>Tools:
- Squoosh (web): https://squoosh.app/
- ImageOptim (Mac): Free compression tool
- TinyPNG (web): PNG compression
Command-line compression:
# Install imagemagick
brew install imagemagick
# Compress JPEG (quality 85)
convert input.jpg -quality 85 output.jpg
# Convert to WebP
cwebp input.jpg -q 80 -o output.webp
# Batch processing
for img in *.jpg; do
cwebp "$img" -q 80 -o "${img%.jpg}.webp"
doneProblem: Sending 4K images to mobile phones.
Solution: srcset and sizes attributes.
<img
src="image-800w.jpg"
srcset="
image-400w.jpg 400w,
image-800w.jpg 800w,
image-1200w.jpg 1200w,
image-1600w.jpg 1600w
"
sizes="
(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px
"
alt="Responsive image"
loading="lazy"
>Breakdown:
srcset: Available image sizessizes: When to use each size- Browser automatically picks the best option
Cloudinary example:
<!-- Original: 2000x1500 -->
<img src="https://res.cloudinary.com/demo/image/upload/sample.jpg">
<!-- Auto-optimized: width=800, auto format, auto quality -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800,f_auto,q_auto/sample.jpg">JavaScript minification (Terser):
// Before minification (readable)
function calculateTotalPrice(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}
// After minification
function calculateTotalPrice(t){let e=0;for(let l=0;l<t.length;l++)e+=t[l].price*t[l].quantity;return e}CSS minification:
/* Before: 2.5KB */
.header {
background-color: #ffffff;
padding: 20px;
margin-bottom: 30px;
}
/* After: 0.5KB */
.header{background-color:#fff;padding:20px;margin-bottom:30px}Gzip vs Brotli:
1MB JavaScript file:
- Uncompressed: 1000KB
- Gzip: 250KB (75% reduction)
- Brotli: 200KB (80% reduction) ✅
NGINX configuration:
# Enable Gzip
gzip on;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
image/svg+xml;
# Enable Brotli (better compression)
brotli on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
text/javascript
application/javascript
application/json
image/svg+xml;Apache configuration:
# .htaccess
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
</IfModule>CDN (Content Delivery Network): Distributes static assets across global servers.
graph TB
A[User in Tokyo] -->|Fast: 20ms| B[CDN Edge - Tokyo]
C[User in London] -->|Fast: 15ms| D[CDN Edge - London]
E[User in New York] -->|Fast: 10ms| F[CDN Edge - New York]
B --> G[Origin Server - US West]
D --> G
F --> G
✅ Lower latency: Content served from nearest server
✅ Faster global access: Consistent performance worldwide
✅ Reduced origin load: Less traffic to your server
✅ Better availability: Multiple points of presence
✅ DDoS protection: Many CDNs include security features
Cloudflare (Free tier available)
# Setup: Change DNS nameservers
# Auto-caching, auto-minificationAWS CloudFront
# Create distribution
aws cloudfront create-distribution \
--origin-domain-name example.com \
--default-root-object index.htmlFastly (Edge compute capabilities) Vercel (Optimized for Next.js) Netlify (Integrated with Git)
Goal: Avoid downloading unchanged files repeatedly.
Cache-Control: max-age=31536000, immutable
Breakdown:
max-age=31536000: Cache for 1 yearimmutable: Never revalidate (perfect for versioned files)
1. Versioned assets (Aggressive caching)
# Cache JS/CSS with hash in filename forever
location ~* \.(?:js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Example files:
# app.abc123.js
# styles.def456.css2. HTML (Always fresh)
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}3. Images (Moderate caching)
location ~* \.(?:jpg|jpeg|gif|png|ico|svg|webp)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 days
}Advanced caching with Service Workers:
// service-worker.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js'
];
// Install event: Cache assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Fetch event: Serve from cache, fallback to network
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});Problem: CSS and JavaScript block page rendering.
<!-- BAD: Blocks parsing and rendering -->
<script src="app.js"></script>
<!-- GOOD: Parse HTML first, execute after -->
<script defer src="app.js"></script>
<!-- ALSO GOOD: Don't block parsing -->
<script async src="analytics.js"></script>Difference:
defer: Execute after HTML parsing, in orderasync: Execute as soon as downloaded, may be out of order
gantt
title Script Loading Comparison
dateFormat X
axisFormat %s
section Normal
Download :0, 2
Block Parse :0, 2
Execute :2, 3
section Defer
Download :0, 2
Parse HTML :0, 3
Execute :3, 4
section Async
Download :0, 2
Execute Immediately :2, 3
Technique: Inline CSS needed for above-the-fold content.
<head>
<!-- Critical CSS inline -->
<style>
/* Only styles for hero, header, above-fold */
.header { background: #333; color: white; padding: 1rem; }
.hero { height: 100vh; display: flex; align-items: center; }
</style>
<!-- Non-critical CSS loaded async -->
<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 for extracting critical CSS:
# Using Critical
npm install -g critical
critical src/index.html --base dist --inline > dist/index.htmlWeb fonts can delay text rendering significantly.
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when ready */
}Options:
block: Invisible text up to 3s (avoid!)swap: Show fallback immediately, swap when ready ✅fallback: Show fallback after 100ms, swap for 3s windowoptional: Browser decides based on connection speed
<head>
<!-- Load critical font immediately -->
<link rel="preload"
href="/fonts/main-font.woff2"
as="font"
type="font/woff2"
crossorigin>
</head>Problem: Full font files include all characters (Latin, Cyrillic, Greek, etc.)
Solution: Include only needed characters.
<!-- Full Google Fonts: ~50KB -->
<link href="https://fonts.googleapis.com/css2?family=Roboto" rel="stylesheet">
<!-- Subset (Latin only): ~12KB -->
<link href="https://fonts.googleapis.com/css2?family=Roboto&subset=latin" rel="stylesheet">
<!-- Further subset (specific characters): ~5KB -->
<link href="https://fonts.googleapis.com/css2?family=Roboto&text=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" rel="stylesheet">Self-hosting with subsetting:
# Install glyphhanger
npm install -g glyphhanger
# Create subset
glyphhanger https://example.com --subset=font.ttf --formats=woff2Every request has overhead:
Single HTTP request:
- DNS lookup: 20-120ms
- TCP connection: 20-100ms
- TLS handshake: 50-200ms
- Actual transfer: Varies
= 90-420ms minimum!
CSS bundling:
# Before: 5 separate CSS files = 5 requests
header.css
menu.css
content.css
sidebar.css
footer.css
# After: 1 combined file = 1 request
styles.css (combined)Icon sprites (traditional):
/* Instead of 20 icon files */
.icon {
background: url('sprite.png') no-repeat;
}
.icon-home { background-position: 0 0; }
.icon-user { background-position: -32px 0; }
.icon-settings { background-position: -64px 0; }Modern alternative: SVG sprites
<svg style="display: none;">
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</symbol>
<symbol id="icon-user" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</symbol>
</svg>
<!-- Use icons -->
<svg><use xlink:href="#icon-home"></use></svg>
<svg><use xlink:href="#icon-user"></use></svg>HTTP/1.1 limitations:
Request 1 → Wait → Response 1
Request 2 → Wait → Response 2
Request 3 → Wait → Response 3
(Sequential, slow)
HTTP/2 benefits:
Request 1 ──┐
Request 2 ──┤→ Multiplexing → All responses in parallel
Request 3 ──┘
(Parallel, fast!)
Additional HTTP/2 features:
- Header compression: Reduces overhead
- Server push: Server sends resources before requested
- Binary protocol: More efficient than text
Enable HTTP/2 (NGINX):
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}Definition: High-priority fetch for resources needed soon.
<!-- Tell browser to fetch immediately -->
<link rel="preload" href="critical-font.woff2" as="font" crossorigin>
<link rel="preload" href="hero-image.jpg" as="image">
<link rel="preload" href="main.js" as="script">Use cases:
- Critical fonts
- Hero images
- Key JavaScript/CSS
Definition: Low-priority fetch for resources needed later.
<!-- Hint: User might navigate here next -->
<link rel="prefetch" href="/dashboard.js">
<link rel="prefetch" href="/next-page.html">Use cases:
- Next page in flow
- Likely user navigation
- Background data loading
Definition: Resolve DNS early.
<!-- Prepare connections to external domains -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://analytics.example.com">Definition: Establish early connection (DNS + TCP + TLS).
<!-- Full connection to external domain -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com">Comparison:
dns-prefetch: [DNS]
preconnect: [DNS][TCP][TLS]
prefetch: [DNS][TCP][TLS][GET resource]
preload: [DNS][TCP][TLS][GET resource] (high priority!)
Modern frameworks offer powerful optimization techniques.
Traditional SPA problem:
<!-- Client receives empty shell -->
<div id="app"></div>
<script src="app.js"></script>
<!-- Steps:
1. Download HTML (empty)
2. Download JavaScript
3. Execute JavaScript
4. Render content
= Users see content after ALL stepsSSR solution:
<!-- Client receives fully rendered HTML -->
<div id="app">
<h1>Welcome</h1>
<p>Content is already here!</p>
</div>
<script src="app.js"></script>
<!-- JavaScript hydrates existing content
= Users see content immediately// nuxt.config.js
export default {
ssr: true, // Enable SSR
// Or choose per-page
generate: {
routes: ['/about', '/contact']
}
}// pages/index.js
export default function Home({ data }) {
return <div>{data}</div>;
}
// This runs on server
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}Even better: Pre-render at build time.
// Next.js static generation
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 3600 // Rebuild every hour
};
}Benefits:
- ⚡ Instant page loads
- 💰 Cheap hosting (static files)
- 🔒 More secure (no server)
Best for:
- Blogs
- Documentation
- Marketing sites
- E-commerce product listings
Automatic in modern frameworks:
// Vue Router
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue') // Auto code-split!
}
];
// Next.js
// Each file in pages/ is automatically code-split
pages/
index.js → Route: /
about.js → Route: /about
contact.js → Route: /contactResult:
Visit / → Load: index.bundle.js
Visit /about → Load: about.bundle.js
Visit /contact → Load: contact.bundle.js
VS all at once in SPA
<template>
<!-- Use v-once for static content -->
<div v-once>
<h1>{{ staticTitle }}</h1>
</div>
<!-- Use v-memo for expensive lists -->
<div v-for="item in list" v-memo="[item.id]">
{{ expensiveComputation(item) }}
</div>
</template>// Memoize expensive computations
import { useMemo } from 'react';
function Component({ items }) {
const expensiveResult = useMemo(
() => computeExpensiveValue(items),
[items]
);
return <div>{expensiveResult}</div>;
}
// Memoize components
import { memo } from 'react';
const MemoizedComponent = memo(function MyComponent({ data }) {
return <div>{data}</div>;
});Follow this systematic process:
# Run Lighthouse
lighthouse https://example.com --output html --output-path ./report.html
# Run WebPageTest
# Visit webpagetest.org and test from multiple locationsDocument baseline:
Before optimization:
- LCP: 4.2s 🔴
- FID: 350ms 🔴
- CLS: 0.25 🔴
- Total page size: 3.5MB
- Requests: 87
- Load time: 6.8s
Analyze Lighthouse suggestions:
Top opportunities:
1. Eliminate render-blocking resources (save 2.1s)
2. Properly size images (save 1.8s)
3. Reduce unused JavaScript (save 1.2s)
Check network waterfall:
- Identify slow resources
- Find sequential loading chains
- Spot oversized assets
Prioritize by impact:
1. High impact, low effort:
- Image compression (saves 1.8s)
- Enable Gzip/Brotli (saves 1.2s)
2. High impact, medium effort:
- Code splitting (saves 1.0s)
- Lazy loading (saves 0.8s)
3. High impact, high effort:
- Implement SSR (saves 1.5s)
- Complete redesign (saves 2.0s)
Start with quick wins!
# Run Lighthouse again
lighthouse https://example.com --output htmlCompare results:
After optimization:
- LCP: 2.1s ✅ (50% improvement)
- FID: 150ms ✅ (57% improvement)
- CLS: 0.08 ✅ (68% improvement)
- Total page size: 850KB (76% reduction)
- Requests: 32 (63% reduction)
- Load time: 2.9s (57% improvement)
// Set up monitoring
setupRUM({
service: 'my-app',
trackWebVitals: true,
alertThresholds: {
LCP: 2500,
FID: 100,
CLS: 0.1
}
});Create dashboards:
- Real user Core Web Vitals
- Performance by country/device
- Error rates and slow transactions
graph LR
A[Measure] --> B[Identify]
B --> C[Optimize]
C --> D[Test]
D --> E[Monitor]
E --> A
Remember:
- 📊 Measure → Optimize → Measure again
- 🎯 Focus on biggest impact first
- 🔄 Performance is ongoing, not one-time
- 📈 Track real user metrics
Frontend optimization is not just about speed—it's about:
✅ Better user experience - Happy users stay longer
✅ Higher conversions - Fast sites sell more
✅ Improved SEO - Google rewards fast sites
✅ Lower infrastructure costs - Efficient delivery
✅ Competitive advantage - Stand out from competitors
You can't optimize what you don't measure.
Use Lighthouse, WebPageTest, Chrome DevTools.
- Tree shaking
- Code splitting
- Remove unused dependencies
Impact: Often 50-70% size reduction
- Use WebP/AVIF
- Compress aggressively
- Implement lazy loading
- Serve responsive sizes
Impact: Images are usually 50%+ of page weight
- Aggressive caching for static assets
- Use CDN for global delivery
- Service Workers for offline support
Impact: Repeat visits 80% faster
- Lab data ≠ Real users
- Track Core Web Vitals in production
- Set performance budgets
- Alert on regressions
Impact: Catch issues before users complain
┌─────────────────────────────────────────────────┐
│ Frontend Optimization Cheat Sheet │
├─────────────────────────────────────────────────┤
│ 🎯 Core Web Vitals Goals: │
│ • LCP < 2.5s │
│ • INP < 200ms │
│ • CLS < 0.1 │
│ │
│ ⚡ Quick Wins: │
│ 1. Enable Gzip/Brotli compression │
│ 2. Lazy load images (loading="lazy") │
│ 3. Defer non-critical JavaScript │
│ 4. Use CDN for static assets │
│ 5. Compress images to WebP │
│ │
│ 📊 Measurement Tools: │
│ • Lighthouse (DevTools) │
│ • WebPageTest │
│ • Chrome DevTools Performance Tab │
│ │
│ 🔄 Workflow: │
│ Measure → Identify → Optimize → Test │
│ → Monitor → Repeat │
└─────────────────────────────────────────────────┘
- Audit your site - Run Lighthouse right now
- Implementation strategy - Focus on quick wins first
- Set up monitoring - Track real user performance
- Continuous improvement - Make optimization part of your workflow
- Share knowledge - Teach your team these techniques
Tools:
Documentation:
Books:
- "High Performance Browser Networking" by Ilya Grigorik
- "Designing for Performance" by Lara Hogan
Courses:
- web.dev/learn → Performance section
- Frontend Masters - Performance courses
"Fast is not a feature—it's a requirement."
Performance optimization is an investment that pays dividends in:
- User satisfaction
- Conversion rates
- SEO rankings
- Infrastructure costs
- Competitive advantage
Start optimizing today! 🚀
Time for questions and optional live demos:
- Running Lighthouse audit
- Analyzing network waterfall
- Implementing lazy loading
- Image compression comparison
- Code splitting demonstration
Thank you for attending! Let's make the web faster together! 🎉

