Skip to content

05 - Performance Optimization ​

Why This Matters for Glomopay ​

JD explicitly mentions "Drive performance optimization (Core Web Vitals, bundle sizes, rendering)." Checkout pages and dashboards must be fast — slow checkout = lost payments.


Core Web Vitals (Know These Cold) ​

MetricWhatGoodMeasures
LCP (Largest Contentful Paint)Biggest visible element loads< 2.5sLoading speed
INP (Interaction to Next Paint)Response time to user input< 200msInteractivity
CLS (Cumulative Layout Shift)Visual stability< 0.1Layout stability

LCP Optimization ​

tsx
// 1. Preload critical resources
<link rel="preload" href="/hero-image.webp" as="image" />

// 2. Use next/image for automatic optimization (Next.js)
import Image from 'next/image';
<Image src="/hero.webp" priority width={800} height={400} alt="..." />

// 3. Server-side render above-the-fold content
// In Next.js — use server components for static content

INP Optimization ​

tsx
// 1. Defer expensive work
function Dashboard() {
  const handleFilter = useCallback((filters: Filters) => {
    // Use startTransition for non-urgent updates
    startTransition(() => {
      setFilteredData(applyFilters(data, filters));
    });
  }, [data]);
}

// 2. Debounce user input
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDeferredValue(query); // React 18+

  // Expensive list filters on debouncedQuery, not query
  const results = useMemo(() => filterItems(debouncedQuery), [debouncedQuery]);
}

CLS Optimization ​

tsx
// 1. Always set dimensions on images/videos
<img width={300} height={200} src="..." alt="..." />

// 2. Reserve space for dynamic content
<div style={{ minHeight: 200 }}> {/* placeholder height */}
  {isLoading ? <Skeleton height={200} /> : <Content />}
</div>

// 3. Avoid injecting content above existing content
// BAD: Banner appears and pushes everything down
// GOOD: Reserve the banner space from the start

React Rendering Optimization ​

1. React.memo — Prevent Unnecessary Re-renders ​

tsx
// Only re-renders when props change (shallow compare)
const TransactionRow = memo(function TransactionRow({ tx }: { tx: Transaction }) {
  return <tr>...</tr>;
});

// With custom comparison
const ExpensiveChart = memo(ChartComponent, (prev, next) => {
  return prev.data.length === next.data.length
    && prev.data[0]?.id === next.data[0]?.id;
});

2. useMemo — Cache Expensive Computations ​

tsx
function Dashboard({ transactions }: { transactions: Transaction[] }) {
  // Only recalculates when transactions change
  const stats = useMemo(() => ({
    total: transactions.reduce((sum, tx) => sum + tx.amount, 0),
    count: transactions.length,
    avgAmount: transactions.length > 0
      ? transactions.reduce((sum, tx) => sum + tx.amount, 0) / transactions.length
      : 0,
  }), [transactions]);

  return <StatsDisplay stats={stats} />;
}

3. useCallback — Stable Function References ​

tsx
function PaymentForm({ onSubmit }: { onSubmit: (data: PaymentData) => void }) {
  // Stable reference — won't cause child re-renders
  const handleSubmit = useCallback((e: FormEvent) => {
    e.preventDefault();
    onSubmit(formData);
  }, [onSubmit, formData]);

  return <MemoizedForm onSubmit={handleSubmit} />;
}

When NOT to Optimize ​

  • Don't memo everything — it has a cost (comparison + memory)
  • Don't useMemo simple calculations (adding two numbers)
  • Profile first, optimize second — use React DevTools Profiler

Code Splitting & Lazy Loading ​

Route-Level Splitting (Most Impact) ​

tsx
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/checkout" element={<Checkout />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Component-Level Splitting ​

tsx
// Heavy component loaded on demand
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartLibrary = lazy(() => import('./components/Chart'));

function AdminPanel() {
  const [showEditor, setShowEditor] = useState(false);

  return (
    <div>
      <button onClick={() => setShowEditor(true)}>Edit</button>
      {showEditor && (
        <Suspense fallback={<Spinner />}>
          <RichTextEditor />
        </Suspense>
      )}
    </div>
  );
}

Dynamic Import for Utilities ​

tsx
// Don't import heavy libraries at the top level
async function generatePDF(data: ReportData) {
  const { jsPDF } = await import('jspdf'); // only loads when needed
  const doc = new jsPDF();
  // ...
}

Bundle Size Optimization ​

1. Tree Shaking — Import Only What You Need ​

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

// GOOD: Import specific function
import debounce from 'lodash/debounce';
debounce(fn, 300);

// BEST: Use native JS (no library needed)
// Most lodash utilities have native equivalents now

2. Analyze Your Bundle ​

bash
# Next.js
npx @next/bundle-analyzer

# Vite
npx vite-bundle-visualizer

# General
npx source-map-explorer build/static/js/*.js

3. External Heavy Dependencies ​

tsx
// Move heavy deps to CDN or dynamic imports
// date-fns instead of moment.js (tree-shakeable)
// native Intl API instead of i18n libraries for basic formatting

// Intl.NumberFormat for currency (zero bundle cost)
const formatCurrency = (amount: number, currency: string) =>
  new Intl.NumberFormat('en-IN', { style: 'currency', currency }).format(amount);

Virtualization (For Dashboards with Large Lists) ​

tsx
import { useVirtualizer } from '@tanstack/react-virtual';

function TransactionTable({ transactions }: { transactions: Transaction[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: transactions.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48, // row height
  });

  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
              width: '100%',
            }}
          >
            <TransactionRow tx={transactions[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}
// Renders 10,000 rows with only ~20 DOM nodes

Quick Optimization Checklist ​

  • [ ] Route-level code splitting with React.lazy
  • [ ] Images optimized (next/image or manual WebP + sizes)
  • [ ] No layout shift (dimensions on images, skeleton loaders)
  • [ ] Heavy components lazy loaded
  • [ ] Tree-shakeable imports (named imports, not default)
  • [ ] React.memo on expensive pure components
  • [ ] Virtualization for lists > 100 items
  • [ ] Debounce/throttle user input that triggers expensive updates
  • [ ] Lighthouse score > 90 on all metrics

Frontend interview preparation reference.