Skip to main content

Web Performance 101: Making Your Site Fast Without Losing Your Mind

5 min read

Page speed matters. Users bounce if your site is slow. Google ranks you lower. But performance optimization can feel overwhelming. Let’s focus on the wins that actually matter.

The Metrics That Actually Matter

Forget about all the metrics. Focus on these:

Core Web Vitals:

  • LCP (Largest Contentful Paint): < 2.5s
  • FID (First Input Delay): < 100ms
  • CLS (Cumulative Layout Shift): < 0.1

These are what Google cares about, and they correlate with user experience.

Check them:

Quick Win #1: Optimize Images

Images are usually 50%+ of page weight.

Use Modern Formats

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Fallback">
</picture>

WebP is 30% smaller than JPEG. AVIF is even better.

Lazy Load

<img src="hero.jpg" alt="Hero" loading="eager">
<img src="footer.jpg" alt="Footer" loading="lazy">

Only load images when needed. Works in all modern browsers.

Responsive Images

<img
  srcset="
    image-400.jpg 400w,
    image-800.jpg 800w,
    image-1200.jpg 1200w
  "
  sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
  src="image-800.jpg"
  alt="Responsive"
>

Serve appropriate size based on screen.

Tools

  • Squoosh.app - Compress images
  • imgix or Cloudinary - CDN with automatic optimization
  • next/image - Automatic optimization in Next.js

Quick Win #2: Code Splitting

Don’t ship code users don’t need.

Route-Based Splitting

// Instead of this
import Dashboard from './Dashboard';
import Settings from './Settings';

// Do this
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

Only load the route they visit.

Component-Based Splitting

// Heavy component used conditionally
const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {showChart && (
        <Suspense fallback={<div>Loading...</div>}>
          <Chart />
        </Suspense>
      )}
    </>
  );
}

Quick Win #3: Reduce JavaScript

Less JS = faster page.

Check Your Bundle Size

npm install -g webpack-bundle-analyzer

# In your project
npx webpack-bundle-analyzer dist/stats.json

Visualize what’s taking up space.

Tree Shaking

//  Imports entire library
import _ from 'lodash';

//  Imports only what you need
import debounce from 'lodash/debounce';

Use Smaller Alternatives

  • date-fns instead of moment.js
  • preact instead of react (for simple projects)
  • Native APIs instead of jQuery

Quick Win #4: Caching Strategy

Make the browser do the work.

Set Cache Headers

# Immutable assets (with hash in filename)
location /static/ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

# HTML (always revalidate)
location / {
  add_header Cache-Control "no-cache";
}

Service Worker (PWA)

// Cache-first strategy
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

Quick Win #5: Fonts

Web fonts can block rendering.

Font Display

@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately */
}

Preload Critical Fonts

<link
  rel="preload"
  href="/fonts/custom.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

Use System Fonts

font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;

Zero network request, instant rendering.

Quick Win #6: Critical CSS

Inline critical CSS in <head> to avoid render-blocking.

<head>
  <style>
    /* Critical CSS for above-the-fold content */
    .hero { ... }
    .nav { ... }
  </style>
  <link rel="stylesheet" href="/styles.css">
</head>

Tools:

  • critical npm package
  • Critters (built into Next.js)

Quick Win #7: Reduce Third-Party Scripts

Every analytics and tracking script slows you down.

Audit Scripts

# Check what's loading
Chrome DevTools > Network > Filter: JS

Defer Non-Critical Scripts

<!-- Bad -->
<script src="analytics.js"></script>

<!-- Good -->
<script defer src="analytics.js"></script>

<!-- Better -->
<script>
  // Load after page is interactive
  window.addEventListener('load', () => {
    const script = document.createElement('script');
    script.src = 'analytics.js';
    document.body.appendChild(script);
  });
</script>

Self-Host When Possible

<!-- Slower - extra DNS lookup -->
<script src="https://cdn.example.com/lib.js"></script>

<!-- Faster - same origin -->
<script src="/vendor/lib.js"></script>

Quick Win #8: Database & API Optimization

Frontend speed doesn’t matter if the backend is slow.

Add Indexes

-- Find slow queries
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';

-- Add index
CREATE INDEX idx_users_email ON users(email);

Use Caching

// Redis for frequently accessed data
const user = await redis.get(`user:${id}`) ||
             await db.getUser(id).then(u => {
               redis.set(`user:${id}`, u, 'EX', 3600);
               return u;
             });

Pagination

//  Don't load everything
const posts = await db.query('SELECT * FROM posts');

//  Paginate
const posts = await db.query(
  'SELECT * FROM posts LIMIT 20 OFFSET ?',
  [page * 20]
);

Quick Win #9: CDN

Serve static assets from a CDN.

Benefits:

  • Faster delivery (closer to users)
  • Reduced server load
  • Better caching

Options:

  • Cloudflare (free!)
  • AWS CloudFront
  • Vercel (automatic for Next.js)
  • Netlify (automatic)

Quick Win #10: Measure Everything

You can’t improve what you don’t measure.

Real User Monitoring

// Web Vitals
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);

Performance API

// Measure specific operations
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');

performance.measure('fetch-duration', 'fetch-start', 'fetch-end');
const measure = performance.getEntriesByName('fetch-duration')[0];
console.log(`Fetch took ${measure.duration}ms`);

Tools

  • Lighthouse CI - Automated performance testing
  • SpeedCurve - Historical performance tracking
  • Google Analytics - Page load times

The Performance Checklist

Before launch, check:

  • Images optimized and lazy loaded
  • Code split by route
  • Bundle size analyzed and optimized
  • Fonts using font-display: swap
  • Critical CSS inlined
  • Third-party scripts deferred
  • Cache headers set correctly
  • Using a CDN
  • Performance monitoring in place

When to Stop Optimizing

You’re done when:

  • Core Web Vitals are green
  • Lighthouse score > 90
  • Page loads in < 3s on 4G
  • Users aren’t complaining

Perfect is the enemy of good. Ship and iterate.

My Performance Stack

  • Next.js - Automatic optimization
  • Vercel - Edge functions and CDN
  • Cloudflare - DNS and additional CDN
  • imgix - Image optimization
  • Sentry - Performance monitoring

Conclusion

Performance optimization is a journey, not a destination. Start with the quick wins:

  1. Optimize images
  2. Split code
  3. Cache aggressively
  4. Defer non-critical JS

These alone will get you 80% of the way there.

The remaining 20% requires profiling and targeted fixes. But most sites never need to go that deep.

Now go make your site fast! 🚀