PerformanceWeb DevelopmentFrontend

Performance Optimization: Making the Web Faster

Practical strategies and techniques for optimizing web application performance, from bundle splitting to lazy loading.

PW

Piotr Wislowski

11 min read

Performance Optimization: Making the Web Faster

Web performance isn’t just about making your site feel snappy—it directly impacts user experience, conversion rates, and search engine rankings. Every millisecond matters. Users expect fast-loading pages, and Google’s Core Web Vitals have made performance a ranking factor.

Why Performance Matters

The statistics are compelling:

  • 53% of mobile users abandon sites that take longer than 3 seconds to load
  • 100ms delay can hurt conversion rates by up to 7%
  • 2-second delay in load time increases bounce rates by 103%

Core Web Vitals: The New Standard

Google’s Core Web Vitals focus on three key metrics:

Largest Contentful Paint (LCP)

Measures loading performance. Should occur within 2.5 seconds of when the page first starts loading.

// Measure LCP
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

First Input Delay (FID)

Measures interactivity. Should be less than 100 milliseconds.

// Measure FID
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  entries.forEach((entry) => {
    console.log('FID:', entry.processingStart - entry.startTime);
  });
}).observe({ type: 'first-input', buffered: true });

Cumulative Layout Shift (CLS)

Measures visual stability. Should maintain a CLS of less than 0.1.

// Measure CLS
let clsValue = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
    }
  }
  console.log('CLS:', clsValue);
}).observe({ type: 'layout-shift', buffered: true });

Loading Performance Optimization

Code Splitting

Split your JavaScript bundles to load only what’s needed:

// Route-based splitting
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

// Component-based splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function Page() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>
        Load Heavy Component
      </button>
      {showHeavy && (
        <Suspense fallback={<div>Loading component...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

Resource Prioritization

Use resource hints to optimize loading:

<!-- Preload critical resources -->
<link rel="preload" href="/fonts/critical.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero-image.jpg" as="image">

<!-- Prefetch resources for next page -->
<link rel="prefetch" href="/about.js">
<link rel="prefetch" href="/contact.js">

<!-- DNS prefetch for external domains -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//api.example.com">

<!-- Preconnect to critical third parties -->
<link rel="preconnect" href="//fonts.gstatic.com" crossorigin>

Image Optimization

Images often account for the majority of page weight:

<!-- Use modern formats with fallbacks -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero image" loading="lazy">
</picture>

<!-- Responsive images -->
<img
  srcset="
    small.jpg 300w,
    medium.jpg 600w,
    large.jpg 1200w
  "
  sizes="
    (max-width: 300px) 100vw,
    (max-width: 600px) 50vw,
    33vw
  "
  src="medium.jpg"
  alt="Responsive image"
  loading="lazy"
>

Service Worker Caching

Implement strategic caching with service workers:

// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
  '/',
  '/styles.css',
  '/app.js',
  '/offline.html'
];

// Install event - cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    // Cache-first strategy for images
    event.respondWith(
      caches.match(event.request)
        .then(response => {
          return response || fetch(event.request)
            .then(fetchResponse => {
              const responseClone = fetchResponse.clone();
              caches.open(CACHE_NAME)
                .then(cache => cache.put(event.request, responseClone));
              return fetchResponse;
            });
        })
    );
  } else if (event.request.url.includes('/api/')) {
    // Network-first strategy for API calls
    event.respondWith(
      fetch(event.request)
        .then(response => {
          if (response.ok) {
            const responseClone = response.clone();
            caches.open(CACHE_NAME)
              .then(cache => cache.put(event.request, responseClone));
          }
          return response;
        })
        .catch(() => caches.match(event.request))
    );
  }
});

Runtime Performance Optimization

Efficient JavaScript

Write performant JavaScript that doesn’t block the main thread:

// Use requestAnimationFrame for animations
function smoothAnimation() {
  let start = null;

  function animate(timestamp) {
    if (!start) start = timestamp;
    const progress = timestamp - start;

    // Update animation
    element.style.transform = `translateX(${progress / 10}px)`;

    if (progress < 2000) {
      requestAnimationFrame(animate);
    }
  }

  requestAnimationFrame(animate);
}

// Debounce expensive operations
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

const expensiveSearch = debounce((query) => {
  // Expensive search operation
  performSearch(query);
}, 300);

// Use Web Workers for heavy computations
// main.js
const worker = new Worker('heavy-computation.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (event) => {
  console.log('Result:', event.data);
};

// heavy-computation.js
self.onmessage = function(event) {
  const result = heavyComputation(event.data);
  self.postMessage(result);
};

Memory Management

Prevent memory leaks and optimize memory usage:

// Clean up event listeners
class Component {
  constructor() {
    this.handleClick = this.handleClick.bind(this);
    this.handleResize = this.handleResize.bind(this);
  }

  mount() {
    document.addEventListener('click', this.handleClick);
    window.addEventListener('resize', this.handleResize);
  }

  unmount() {
    // Always clean up!
    document.removeEventListener('click', this.handleClick);
    window.removeEventListener('resize', this.handleResize);
  }

  handleClick(event) {
    // Handle click
  }

  handleResize(event) {
    // Handle resize
  }
}

// Use WeakMap for private data to avoid memory leaks
const privateData = new WeakMap();

class MyClass {
  constructor() {
    privateData.set(this, { sensitive: 'data' });
  }

  getPrivateData() {
    return privateData.get(this);
  }
}

// Clear intervals and timeouts
class Timer {
  start() {
    this.intervalId = setInterval(() => {
      console.log('Timer tick');
    }, 1000);
  }

  stop() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
}

CSS Performance

Critical CSS Inlining

Load critical styles immediately and defer non-critical CSS:

<head>
  <style>
    /* Critical CSS inlined here */
    body { font-family: system-ui; }
    .hero { background: #007bff; color: white; }
  </style>

  <!-- Load non-critical CSS asynchronously -->
  <link rel="preload" href="/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head>

Efficient CSS Selectors

Write performant CSS selectors:

/* ❌ Inefficient - complex selectors */
body div.container ul li a.active {
  color: red;
}

/* ✅ Efficient - simple, specific selectors */
.active-link {
  color: red;
}

/* ❌ Avoid universal selectors */
* {
  box-sizing: border-box;
}

/* ✅ Be more specific */
html {
  box-sizing: border-box;
}
*, *::before, *::after {
  box-sizing: inherit;
}

CSS Containment

Use CSS containment to limit reflow and repaint:

.card {
  /* Contain layout and style recalculations */
  contain: layout style;
}

.isolated-component {
  /* Contain everything */
  contain: strict;
}

/* Use content-visibility for off-screen content */
.lazy-section {
  content-visibility: auto;
  contain-intrinsic-size: 200px;
}

Network Performance

HTTP/2 and HTTP/3 Optimization

Leverage modern protocol features:

// Server push (HTTP/2)
// Push critical resources before they're requested
app.get('/', (req, res) => {
  // Push CSS and JS
  res.push('/critical.css');
  res.push('/app.js');

  res.render('index');
});

// Multiplexing allows many requests over one connection
// No need to concatenate files in HTTP/2+

Compression

Enable proper compression:

// Node.js with compression middleware
const compression = require('compression');
app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  }
}));

// Brotli compression (better than gzip)
app.use(compression({
  brotli: {
    enabled: true,
    zlib: {}
  }
}));

Monitoring and Measurement

Real User Monitoring (RUM)

Track actual user performance:

// Basic RUM implementation
function sendPerformanceData() {
  if ('performance' in window) {
    const navigation = performance.getEntriesByType('navigation')[0];
    const paint = performance.getEntriesByType('paint');

    const metrics = {
      // Navigation timing
      dns: navigation.domainLookupEnd - navigation.domainLookupStart,
      connection: navigation.connectEnd - navigation.connectStart,
      request: navigation.responseStart - navigation.requestStart,
      response: navigation.responseEnd - navigation.responseStart,
      dom: navigation.domContentLoadedEventEnd - navigation.responseEnd,

      // Paint timing
      fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime,

      // Custom metrics
      timeToInteractive: Date.now() - performance.timing.navigationStart
    };

    // Send to analytics
    fetch('/api/performance', {
      method: 'POST',
      body: JSON.stringify(metrics),
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

// Send data when page is about to unload
window.addEventListener('beforeunload', sendPerformanceData);

Performance Budget

Set and monitor performance budgets:

// webpack-bundle-analyzer configuration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html'
    })
  ],
  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 250000,
    hints: 'error'
  }
};

// Lighthouse CI for automated monitoring
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000'],
      numberOfRuns: 3
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.8 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }]
      }
    }
  }
};

Framework-Specific Optimizations

React Performance

Optimize React applications:

// Use React.memo for component optimization
const ExpensiveComponent = React.memo(({ data, onAction }) => {
  return <div>{/* Complex rendering */}</div>;
}, (prevProps, nextProps) => {
  // Custom comparison
  return prevProps.data.id === nextProps.data.id;
});

// Use useMemo and useCallback
function OptimizedComponent({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.category === filter);
  }, [items, filter]);

  const handleItemClick = useCallback((id) => {
    console.log('Clicked item:', id);
  }, []);

  return (
    <div>
      {filteredItems.map(item => (
        <Item key={item.id} item={item} onClick={handleItemClick} />
      ))}
    </div>
  );
}

// Virtualize long lists
import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <List
      height={600}
      itemCount={items.length}
      itemSize={35}
    >
      {Row}
    </List>
  );
}

SvelteKit Performance

Leverage SvelteKit’s built-in optimizations:

// Preload data on hover
<a href="/slow-page" data-sveltekit-preload-data="hover">
  Slow Page
</a>

// Disable JavaScript for static pages
export const csr = false;
export const prerender = true;

// Optimize images
import { enhance } from '$app/forms';

<img
  src="/image.jpg"
  alt="Description"
  loading="lazy"
  decoding="async"
/>

Performance Testing Tools

Automated Testing

Integrate performance testing into your CI/CD:

// Playwright performance test
const { test, expect } = require('@playwright/test');

test('page loads within performance budget', async ({ page }) => {
  await page.goto('https://example.com');

  const performanceMetrics = await page.evaluate(() => {
    return JSON.stringify(performance.getEntriesByType('navigation')[0]);
  });

  const metrics = JSON.parse(performanceMetrics);
  const loadTime = metrics.loadEventEnd - metrics.navigationStart;

  expect(loadTime).toBeLessThan(3000); // Page should load in under 3 seconds
});

// Puppeteer performance monitoring
const puppeteer = require('puppeteer');

async function auditPage(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto(url, { waitUntil: 'networkidle2' });

  const metrics = await page.metrics();
  const performanceTiming = JSON.parse(
    await page.evaluate(() => JSON.stringify(performance.timing))
  );

  console.log('Performance Metrics:', {
    ...metrics,
    loadTime: performanceTiming.loadEventEnd - performanceTiming.navigationStart
  });

  await browser.close();
}

Conclusion

Web performance optimization is an ongoing process that requires attention at every stage of development. Key takeaways:

  1. Measure first - Use tools like Lighthouse, WebPageTest, and RUM to establish baselines
  2. Optimize loading - Implement code splitting, lazy loading, and resource prioritization
  3. Minimize main thread work - Use Web Workers and efficient algorithms
  4. Cache strategically - Implement service workers and HTTP caching
  5. Monitor continuously - Set up performance budgets and automated monitoring

Remember: performance is a feature, not an afterthought. Build performance considerations into your development workflow from day one, and your users will thank you with better engagement, conversion rates, and overall satisfaction.

The web is fast by default—it’s our job as developers to keep it that way.