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) ​
| Metric | What | Good | Measures |
|---|---|---|---|
| LCP (Largest Contentful Paint) | Biggest visible element loads | < 2.5s | Loading speed |
| INP (Interaction to Next Paint) | Response time to user input | < 200ms | Interactivity |
| CLS (Cumulative Layout Shift) | Visual stability | < 0.1 | Layout 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 contentINP 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 startReact 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 now2. Analyze Your Bundle ​
bash
# Next.js
npx @next/bundle-analyzer
# Vite
npx vite-bundle-visualizer
# General
npx source-map-explorer build/static/js/*.js3. 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 nodesQuick 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