Optimizing Frontend Performance: Beyond Code Splitting

Optimizing Frontend Performance: Beyond Code Splitting

BySanjay Goraniya
2 min read
Share:

Optimizing Frontend Performance: Beyond Code Splitting

Frontend performance directly impacts user experience and business metrics. A slow site loses users, and users don't come back. After optimizing frontends that serve millions of users, I've learned that performance optimization is a continuous process, not a one-time task.

The Performance Budget

Set clear performance targets:

Code
// Performance budget
const budget = {
  firstContentfulPaint: 1.5, // seconds
  largestContentfulPaint: 2.5, // seconds
  timeToInteractive: 3.5, // seconds
  totalBlockingTime: 300, // milliseconds
  cumulativeLayoutShift: 0.1
};

Core Web Vitals

Largest Contentful Paint (LCP)

Time until largest content element is visible.

Optimization:

Code
// Optimize images
<img 
  src="image.jpg"
  loading="lazy"
  decoding="async"
  srcset="image-400w.jpg 400w, image-800w.jpg 800w"
  sizes="(max-width: 600px) 400px, 800px"
/>

// Preload critical resources
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero-image.jpg" as="image" />

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

First Input Delay (FID)

Time from first user interaction to browser response.

Optimization:

Code
// Break up long tasks
function processData(data) {
  // Split into chunks
  const chunkSize = 100;
  let index = 0;
  
  function processChunk() {
    const chunk = data.slice(index, index + chunkSize);
    // Process chunk
    processChunkData(chunk);
    
    index += chunkSize;
    if (index < data.length) {
      // Yield to browser
      setTimeout(processChunk, 0);
    }
  }
  
  processChunk();
}

// Use Web Workers for heavy computation
const worker = new Worker('data-processor.js');
worker.postMessage(largeDataSet);
worker.onmessage = (event) => {
  const result = event.data;
  updateUI(result);
};

Cumulative Layout Shift (CLS)

Visual stability measure.

Optimization:

Code
// Reserve space for images
<img 
  src="image.jpg"
  width="800"
  height="600"
  style="aspect-ratio: 800/600"
/>

// Reserve space for dynamic content
<div style="min-height: 200px;">
  {loading ? <Spinner /> : <Content />}
</div>

// Avoid inserting content above existing content
// Bad: Inserting ads at top
// Good: Reserve space, then fill

Advanced Code Splitting

Route-Based Splitting

Code
// Automatic with React Router
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

<Suspense fallback={<Loading />}>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
  </Routes>
</Suspense>

Component-Based Splitting

Code
// Split heavy components
const Chart = lazy(() => import('./components/Chart'));
const DataTable = lazy(() => import('./components/DataTable'));

function Dashboard() {
  return (
    <div>
      <Header />
      <Suspense fallback={<ChartSkeleton />}>
        <Chart data={data} />
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <DataTable data={data} />
      </Suspense>
    </div>
  );
}

Feature-Based Splitting

Code
// Split by feature
const AdminPanel = lazy(() => 
  import('./features/admin/AdminPanel')
);
const UserDashboard = lazy(() => 
  import('./features/user/UserDashboard')
);

Image Optimization

Modern Image Formats

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

Responsive Images

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

Lazy Loading

Code
// Native lazy loading
<img src="image.jpg" loading="lazy" alt="Description" />

// Intersection Observer for custom behavior
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      imageObserver.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

Bundle Optimization

Tree Shaking

Code
// Bad: Imports entire library
import _ from 'lodash';
const result = _.map(items, item => item.id);

// Good: Import only what you need
import map from 'lodash/map';
const result = map(items, item => item.id);

// Better: Use native
const result = items.map(item => item.id);

Analyze Bundle

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

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

Dynamic Imports

Code
// Load only when needed
async function loadHeavyLibrary() {
  const library = await import('./heavy-library');
  return library.default;
}

// Use when needed
button.addEventListener('click', async () => {
  const library = await loadHeavyLibrary();
  library.doSomething();
});

Rendering Optimization

Virtual Scrolling

Code
// For long lists
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

Memoization

Code
// Memoize expensive components
const ExpensiveComponent = React.memo(({ data }) => {
  const processed = useMemo(() => {
    return expensiveProcessing(data);
  }, [data]);
  
  return <div>{processed}</div>;
});

// Memoize callbacks
const handleClick = useCallback(() => {
  doSomething();
}, [dependencies]);

Debounce and Throttle

Code
// Debounce search
const debouncedSearch = useMemo(
  () => debounce((query) => {
    performSearch(query);
  }, 300),
  []
);

// Throttle scroll
useEffect(() => {
  const handleScroll = throttle(() => {
    updateScrollPosition();
  }, 100);
  
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

Caching Strategies

Service Workers

Code
// Cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js'
      ]);
    })
  );
});

// Serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

HTTP Caching

Code
// Set cache headers
app.use(express.static('public', {
  maxAge: '1y',
  etag: true,
  lastModified: true
}));

Network Optimization

Preconnect and Prefetch

Code
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://api.example.com" />
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />

<!-- Prefetch resources -->
<link rel="prefetch" href="/next-page.html" />
<link rel="prefetch" href="/api/data.json" />

HTTP/2 and HTTP/3

Code
// HTTP/2 allows multiplexing
// Multiple requests over single connection

// HTTP/3 (QUIC) improves on HTTP/2
// Better performance on unreliable networks

Real-World Example

Challenge: E-commerce site, 5+ second load time, high bounce rate.

Optimizations Applied:

  1. Code splitting - Route and component-based
  2. Image optimization - WebP, lazy loading, responsive
  3. Bundle optimization - Tree shaking, dynamic imports
  4. Caching - Service worker, HTTP caching
  5. Rendering optimization - Virtual scrolling, memoization
  6. Network optimization - Preconnect, HTTP/2

Results:

  • Load time: 5s → 1.2s
  • LCP: 4s → 1.5s
  • Bounce rate: 40% → 15%
  • Conversion rate: +25%

Performance Monitoring

Real User Monitoring (RUM)

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

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

Performance API

Code
// Measure custom metrics
const markStart = performance.mark('operation-start');
// ... do work ...
const markEnd = performance.mark('operation-end');

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

Best Practices

  1. Measure first - Know your baseline
  2. Set performance budget - Clear targets
  3. Optimize critical path - Above-the-fold content
  4. Lazy load everything - Below-the-fold content
  5. Optimize images - Modern formats, responsive
  6. Minimize JavaScript - Tree shake, code split
  7. Use caching - Service workers, HTTP caching
  8. Monitor continuously - Track Core Web Vitals

Conclusion

Frontend performance optimization is an ongoing process. The key is to:

  • Measure continuously - Know your metrics
  • Optimize incrementally - Small improvements add up
  • Focus on users - Real user metrics matter
  • Test regularly - Performance can regress

Remember: Performance is a feature. Users notice fast sites, and fast sites convert better.

What frontend performance challenges have you faced? What optimizations have had the biggest impact?

Share:

Related Posts