โšก

Performance & Optimization

๐Ÿ‘จโ€๐Ÿณ Chef

A slow restaurant loses customers

Imagine a restaurant with the best food in the world, but where every dish takes 45 minutes. No matter how good it is, customers leave.

Web applications are the same. Every second of load time can cost up to 7% in conversions.

Performance isn't premature optimization. It's respect for the user's time.


Core Web Vitals: The metrics that matter

Google measures 3 main metrics that affect SEO and experience:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   CORE WEB VITALS                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚      LCP        โ”‚      INP        โ”‚       CLS           โ”‚
โ”‚   < 2.5s        โ”‚    < 200ms      โ”‚      < 0.1          โ”‚
โ”‚                 โ”‚                 โ”‚                     โ”‚
โ”‚  Largest        โ”‚  Interaction    โ”‚  Cumulative         โ”‚
โ”‚  Contentful     โ”‚  to Next        โ”‚  Layout             โ”‚
โ”‚  Paint          โ”‚  Paint          โ”‚  Shift              โ”‚
โ”‚                 โ”‚                 โ”‚                     โ”‚
โ”‚  "Fast load"    โ”‚ "Responsive"    โ”‚ "Visually stable"   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
MetricWhat it measuresGoodBad
LCPHow long main content takes< 2.5s> 4s
INPResponse to interactions< 200ms> 500ms
CLSHow much layout shifts< 0.1> 0.25

The browser pipeline

Understanding how the browser renders helps you optimize.

HTML โ”€โ”€โ†’ DOM
          โ”‚
CSS  โ”€โ”€โ†’ CSSOM โ”€โ”€โ†’ Render Tree โ”€โ”€โ†’ Layout โ”€โ”€โ†’ Paint โ”€โ”€โ†’ Composite

What blocks rendering

  • Synchronous JavaScript: Blocks HTML parsing
  • CSS in <head>: Blocks render (but necessary)
  • External fonts: Can cause FOIT/FOUT

JavaScript: The bottleneck

The Event Loop

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  CALL STACK                      โ”‚
โ”‚    (Executes synchronous code, one at a time)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ–ฒ                         โ”‚
         โ”‚                         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   TASK QUEUE    โ”‚    โ”‚    MICROTASK QUEUE      โ”‚
โ”‚  (setTimeout,   โ”‚    โ”‚  (Promises, async/await)โ”‚
โ”‚   events)       โ”‚    โ”‚  (Higher priority)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Problem: Main thread blocking

// BAD: Blocks UI for 500ms
function processData(items) {
  items.forEach(item => heavyCalculation(item));
}

// GOOD: Divide into chunks
async function processDataAsync(items) {
  const chunkSize = 100;
  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize);
    chunk.forEach(item => heavyCalculation(item));
    await new Promise(r => setTimeout(r, 0)); // Yield to browser
  }
}

Bundle optimization

Code Splitting

// Without splitting: everything loads at start
import { Dashboard } from './Dashboard';
import { Admin } from './Admin';
import { Reports } from './Reports';

// With splitting: loads on demand
const Dashboard = lazy(() => import('./Dashboard'));
const Admin = lazy(() => import('./Admin'));
const Reports = lazy(() => import('./Reports'));

Tree Shaking

// BAD: Imports entire library (100KB)
import _ from 'lodash';
_.debounce(fn, 300);

// GOOD: Only what you need (2KB)
import debounce from 'lodash/debounce';
debounce(fn, 300);

Image optimization

Modern formats

FormatUseSavings vs JPEG
WebPGeneral, wide support25-35%
AVIFBest compression, less support50%+

Responsive images

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img
    src="hero.jpg"
    alt="Hero"
    loading="lazy"
    width="1200"
    height="600"
  >
</picture>

Next.js Image

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // For above the fold images
  placeholder="blur" // Loading effect
/>

Strategic caching

Cache levels

User โ”€โ”€โ†’ Browser Cache โ”€โ”€โ†’ CDN Cache โ”€โ”€โ†’ Server โ”€โ”€โ†’ DB
         (localStorage,     (Edge)        (Redis)
          sessionStorage)

HTTP Cache Headers

Cache-Control: public, max-age=31536000, immutable
                โ”‚       โ”‚                 โ”‚
                โ”‚       โ”‚                 โ””โ”€ Don't revalidate
                โ”‚       โ””โ”€ 1 year in seconds
                โ””โ”€ CDN can cache

Strategy by resource type

ResourceCache-ControlWhy
JS/CSS with hashmax-age=31536000, immutableHash changes if file changes
HTMLno-cacheAlways validate with server
Dynamic APIprivate, max-age=0Fresh data
Static imagesmax-age=864001 day, CDN

Database performance

Indexes: The key

-- Without index: Full table scan (slow)
SELECT * FROM users WHERE email = 'test@example.com';
-- Time: 500ms on 1M rows

-- With index: Index lookup (fast)
CREATE INDEX idx_users_email ON users(email);
-- Time: 2ms

EXPLAIN: Understand your queries

EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 123 AND created_at > '2024-01-01';

-- Look for:
-- - Seq Scan (bad on large tables)
-- - Index Scan (good)
-- - Estimated vs actual rows

The N+1 problem

// BAD: N+1 queries
const users = await User.findAll();
for (const user of users) {
  user.orders = await Order.findByUser(user.id); // 1 query per user
}
// 1 + N queries

// GOOD: Eager loading
const users = await User.findAll({
  include: [{ model: Order }]
});
// 1 query with JOIN

Profiling tools

Chrome DevTools

  1. Performance tab: Record load timeline
  2. Network tab: Request waterfall
  3. Lighthouse: Complete audit

What to look for in Performance tab

Timeline:
โ”œโ”€โ”€ Loading (blue): Parsing HTML
โ”œโ”€โ”€ Scripting (yellow): JavaScript
โ”œโ”€โ”€ Rendering (purple): Layout, style
โ””โ”€โ”€ Painting (green): Drawing pixels

If yellow dominates โ†’ Optimize JS
If purple dominates โ†’ Reduce reflows

Performance checklist

Before deploy

  • Lighthouse score > 90
  • LCP < 2.5s
  • Bundle size < 200KB (initial)
  • Images in WebP/AVIF
  • Lazy loading on below fold images
  • Code splitting active
  • Cache headers configured

In production

  • CDN configured
  • Gzip/Brotli active
  • HTTP/2 enabled
  • Indexes on slow queries
  • Core Web Vitals monitoring

Resources


Practice

-> Performance Audit - Optimize a slow app step by step