Performance Techniques You'll Master
- • Code splitting and lazy loading strategies
- • React memoization patterns and pitfalls
- • Bundle size optimization techniques
- • Performance monitoring and measurement
- • Memory leak prevention and detection
- • Real-world banking app optimizations
Why Performance Matters in Banking
When optimizing the IDFC FIRST Bank mobile app that serves 10+ million users, every millisecond matters. A 1-second delay in transaction processing can cost thousands in lost revenue and damage user trust. Poor performance in financial applications isn't just inconvenient—it's a business risk.
This guide shares performance optimization strategies that helped us achieve sub-3-second load times, maintain 60fps interactions, and handle peak loads of 100k+ concurrent users during salary credit days.
Performance Metrics That Matter
Banking App Performance Targets
First Contentful Paint
<1.2s
Critical for user trust
Largest Contentful Paint
<2.5s
Complete page load
Cumulative Layout Shift
<0.1
Visual stability
Code Splitting Strategies
Route-Based Code Splitting
// Route-based splitting with React.lazy
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner';
import ErrorBoundary from './components/ErrorBoundary';
// Lazy load pages
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Transactions = lazy(() => import('./pages/Transactions'));
const Payments = lazy(() => import('./pages/Payments'));
const Investments = lazy(() => import('./pages/Investments'));
// Preload critical routes
const AccountsPage = lazy(() =>
import(/* webpackChunkName: "accounts" */ './pages/Accounts')
);
function App() {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/accounts" element={<AccountsPage />} />
<Route path="/transactions" element={<Transactions />} />
<Route path="/payments" element={<Payments />} />
<Route path="/investments" element={<Investments />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
}
// Smart preloading based on user behavior
import { useEffect } from 'react';
function useRoutePreloading() {
useEffect(() => {
// Preload likely next routes on idle
const preloadRoutes = async () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Preload based on analytics data
import('./pages/Transactions'); // Most accessed after dashboard
import('./pages/Payments'); // Second most common
});
}
};
preloadRoutes();
}, []);
}
// Component-based splitting for large features
const TransactionHistory = lazy(() =>
import('./components/TransactionHistory').then(module => ({
default: module.TransactionHistory
}))
);
const PaymentForm = lazy(() =>
import('./components/PaymentForm').then(module => ({
default: module.PaymentForm
}))
);
function TransactionsPage() {
const [showHistory, setShowHistory] = useState(false);
return (
<div>
<h1>Transactions</h1>
<Suspense fallback={<div>Loading payment form...</div>}>
<PaymentForm />
</Suspense>
{showHistory && (
<Suspense fallback={<div>Loading history...</div>}>
<TransactionHistory />
</Suspense>
)}
<button onClick={() => setShowHistory(true)}>
View History
</button>
</div>
);
}Dynamic Imports for Libraries
// Load heavy libraries only when needed
import { useState } from 'react';
function ChartComponent({ data }) {
const [ChartLibrary, setChartLibrary] = useState(null);
const [loading, setLoading] = useState(false);
const loadChart = async () => {
if (ChartLibrary) return;
setLoading(true);
try {
// Load Chart.js only when user requests charts
const { Chart, registerables } = await import('chart.js');
Chart.register(...registerables);
const { Line } = await import('react-chartjs-2');
setChartLibrary(() => Line);
} catch (error) {
console.error('Failed to load chart library:', error);
} finally {
setLoading(false);
}
};
if (!ChartLibrary) {
return (
<div className="chart-placeholder">
<button onClick={loadChart} disabled={loading}>
{loading ? 'Loading Chart...' : 'Show Chart'}
</button>
</div>
);
}
return <ChartLibrary data={data} />;
}
// Conditional feature loading based on user permissions
function AdminPanel({ user }) {
const [AdminComponents, setAdminComponents] = useState(null);
useEffect(() => {
if (user.role === 'admin') {
// Load admin components only for admin users
Promise.all([
import('./components/UserManagement'),
import('./components/SystemMetrics'),
import('./components/AuditLogs')
]).then(([UserMgmt, Metrics, Audit]) => {
setAdminComponents({
UserManagement: UserMgmt.default,
SystemMetrics: Metrics.default,
AuditLogs: Audit.default
});
});
}
}, [user.role]);
if (user.role !== 'admin') {
return <div>Access denied</div>;
}
if (!AdminComponents) {
return <div>Loading admin panel...</div>;
}
return (
<div>
<AdminComponents.UserManagement />
<AdminComponents.SystemMetrics />
<AdminComponents.AuditLogs />
</div>
);
}
// Progressive enhancement for non-critical features
function useProgressiveFeature(featureName) {
const [feature, setFeature] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const loadFeature = async () => {
try {
const module = await import(`./features/${featureName}`);
if (isMounted) {
setFeature(module.default);
}
} catch (err) {
if (isMounted) {
setError(err);
console.warn(`Failed to load feature ${featureName}:`, err);
}
}
};
// Load on interaction or after initial render
const timer = setTimeout(loadFeature, 100);
return () => {
isMounted = false;
clearTimeout(timer);
};
}, [featureName]);
return { feature, error, loading: !feature && !error };
}Memoization Patterns
Smart Component Memoization
import { memo, useMemo, useCallback, useState } from 'react';
// Expensive list item component
const TransactionItem = memo(({ transaction, onSelect, isSelected }) => {
// Only re-render if transaction, onSelect, or isSelected changes
return (
<div
className={`transaction-item ${isSelected ? 'selected' : ''}`}
onClick={() => onSelect(transaction.id)}
>
<div className="amount">
{transaction.currency} {transaction.amount.toFixed(2)}
</div>
<div className="description">{transaction.description}</div>
<div className="date">
{new Date(transaction.date).toLocaleDateString()}
</div>
</div>
);
});
// Parent component with optimized callbacks
function TransactionsList({ transactions, filters }) {
const [selectedId, setSelectedId] = useState(null);
// Memoize filtered transactions
const filteredTransactions = useMemo(() => {
return transactions.filter(transaction => {
if (filters.type && transaction.type !== filters.type) return false;
if (filters.minAmount && transaction.amount < filters.minAmount) return false;
if (filters.maxAmount && transaction.amount > filters.maxAmount) return false;
return true;
});
}, [transactions, filters]);
// Memoize expensive calculations
const transactionSummary = useMemo(() => {
return filteredTransactions.reduce((acc, transaction) => {
acc.total += transaction.amount;
acc.count += 1;
if (transaction.type === 'credit') {
acc.credits += transaction.amount;
} else {
acc.debits += transaction.amount;
}
return acc;
}, { total: 0, count: 0, credits: 0, debits: 0 });
}, [filteredTransactions]);
// Stable callback reference
const handleTransactionSelect = useCallback((transactionId) => {
setSelectedId(transactionId);
// Analytics tracking
analytics.track('transaction_selected', { transactionId });
}, []);
return (
<div className="transactions-list">
<div className="summary">
<div>Total: {transactionSummary.count} transactions</div>
<div>Credits: ₹{transactionSummary.credits.toFixed(2)}</div>
<div>Debits: ₹{transactionSummary.debits.toFixed(2)}</div>
</div>
<div className="list">
{filteredTransactions.map(transaction => (
<TransactionItem
key={transaction.id}
transaction={transaction}
onSelect={handleTransactionSelect}
isSelected={selectedId === transaction.id}
/>
))}
</div>
</div>
);
}
// Custom hook for expensive operations
function useExpensiveCalculation(data, dependencies) {
return useMemo(() => {
// Simulate expensive calculation
console.log('Performing expensive calculation...');
const result = data.reduce((acc, item) => {
// Complex processing
return acc + item.value * item.multiplier;
}, 0);
return result;
}, [data, ...dependencies]);
}
// Memoization with custom comparison
const ProductCard = memo(({ product, user, onAddToCart }) => {
const isInWishlist = useMemo(() => {
return user.wishlist.includes(product.id);
}, [user.wishlist, product.id]);
const discountedPrice = useMemo(() => {
const discount = user.isPremium ? 0.1 : 0;
return product.price * (1 - discount);
}, [product.price, user.isPremium]);
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>Price: ₹{discountedPrice.toFixed(2)}</p>
{user.isPremium && (
<span className="premium-discount">10% Premium Discount Applied</span>
)}
<button onClick={() => onAddToCart(product.id)}>
Add to Cart
</button>
{isInWishlist && <span className="wishlist-icon">❤️</span>}
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison function
return (
prevProps.product.id === nextProps.product.id &&
prevProps.product.price === nextProps.product.price &&
prevProps.user.isPremium === nextProps.user.isPremium &&
prevProps.user.wishlist.includes(prevProps.product.id) ===
nextProps.user.wishlist.includes(nextProps.product.id)
);
});Virtual Scrolling for Large Lists
import { useState, useEffect, useMemo, useRef } from 'react';
function VirtualizedTransactionList({ transactions }) {
const [scrollTop, setScrollTop] = useState(0);
const [containerHeight, setContainerHeight] = useState(0);
const containerRef = useRef(null);
const ITEM_HEIGHT = 80; // Fixed height for each transaction item
const BUFFER = 5; // Extra items to render for smooth scrolling
// Calculate visible items
const visibleItems = useMemo(() => {
const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER);
const endIndex = Math.min(
transactions.length - 1,
Math.floor((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER
);
return {
startIndex,
endIndex,
items: transactions.slice(startIndex, endIndex + 1)
};
}, [scrollTop, containerHeight, transactions]);
// Handle scroll events
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
};
// Observe container size changes
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerHeight(entry.contentRect.height);
}
});
resizeObserver.observe(container);
setContainerHeight(container.clientHeight);
return () => resizeObserver.disconnect();
}, []);
const totalHeight = transactions.length * ITEM_HEIGHT;
const offsetY = visibleItems.startIndex * ITEM_HEIGHT;
return (
<div
ref={containerRef}
className="virtual-list-container"
style={{ height: '400px', overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div
style={{
transform: `translateY(${offsetY}px)`,
position: 'absolute',
width: '100%'
}}
>
{visibleItems.items.map((transaction, index) => (
<div
key={transaction.id}
style={{ height: ITEM_HEIGHT }}
className="transaction-item"
>
<div className="transaction-content">
<div className="amount">
{transaction.currency} {transaction.amount.toFixed(2)}
</div>
<div className="description">{transaction.description}</div>
<div className="date">
{new Date(transaction.date).toLocaleDateString()}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
// Alternative: Using react-window for complex virtualization
import { FixedSizeList as List } from 'react-window';
const TransactionRow = ({ index, style, data }) => (
<div style={style} className="transaction-row">
<div className="transaction-content">
<span className="amount">
{data[index].currency} {data[index].amount.toFixed(2)}
</span>
<span className="description">{data[index].description}</span>
<span className="date">
{new Date(data[index].date).toLocaleDateString()}
</span>
</div>
</div>
);
function OptimizedTransactionList({ transactions }) {
return (
<List
height={400}
itemCount={transactions.length}
itemSize={80}
itemData={transactions}
>
{TransactionRow}
</List>
);
}Bundle Optimization
Webpack Bundle Analysis
// webpack.config.js optimizations
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Vendor libraries
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// React and related libraries
react: {
test: /[\/]node_modules[\/](react|react-dom|react-router)[\/]/,
name: 'react',
chunks: 'all',
priority: 20
},
// UI libraries
ui: {
test: /[\/]node_modules[\/](@mui|antd|bootstrap)[\/]/,
name: 'ui',
chunks: 'all',
priority: 15
},
// Charts and visualization
charts: {
test: /[\/]node_modules[\/](chart.js|d3|plotly)[\/]/,
name: 'charts',
chunks: 'all',
priority: 15
},
// Common code between pages
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
// Enable tree shaking
usedExports: true,
sideEffects: false
},
plugins: [
// Analyze bundle size in development
process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'server',
openAnalyzer: true
})
].filter(Boolean)
};
// package.json scripts
{
"scripts": {
"analyze": "ANALYZE=true npm run build",
"build:prod": "NODE_ENV=production webpack --mode=production",
"build:dev": "NODE_ENV=development webpack --mode=development"
}
}
// Tree shaking optimization
// utils/index.js - Proper export structure for tree shaking
export const formatCurrency = (amount, currency = 'INR') => {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency
}).format(amount);
};
export const formatDate = (date) => {
return new Intl.DateTimeFormat('en-IN').format(new Date(date));
};
export const validateEmail = (email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
// Import only what you need
import { formatCurrency, formatDate } from './utils'; // Good
// import * as utils from './utils'; // Avoid - imports everythingImage and Asset Optimization
// Image optimization component
import { useState, useRef, useEffect } from 'react';
function OptimizedImage({
src,
alt,
width,
height,
className,
lazy = true,
quality = 75
}) {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(!lazy);
const imgRef = useRef(null);
// Intersection Observer for lazy loading
useEffect(() => {
if (!lazy) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [lazy]);
// Generate responsive image URLs
const generateSrcSet = (baseSrc) => {
const sizes = [320, 640, 768, 1024, 1280];
return sizes
.map(size => `${baseSrc}?w=${size}&q=${quality} ${size}w`)
.join(', ');
};
return (
<div ref={imgRef} className={`image-container ${className}`}>
{isInView && (
<img
src={src}
srcSet={generateSrcSet(src)}
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 25vw"
alt={alt}
width={width}
height={height}
loading={lazy ? 'lazy' : 'eager'}
onLoad={() => setIsLoaded(true)}
style={{
opacity: isLoaded ? 1 : 0,
transition: 'opacity 0.3s ease'
}}
/>
)}
{!isLoaded && (
<div
className="image-placeholder"
style={{ width, height, backgroundColor: '#f0f0f0' }}
>
Loading...
</div>
)}
</div>
);
}
// Progressive image loading
function ProgressiveImage({ src, placeholder, alt, ...props }) {
const [currentSrc, setCurrentSrc] = useState(placeholder);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const img = new Image();
img.onload = () => {
setCurrentSrc(src);
setIsLoaded(true);
};
img.src = src;
}, [src]);
return (
<img
{...props}
src={currentSrc}
alt={alt}
style={{
filter: isLoaded ? 'none' : 'blur(5px)',
transition: 'filter 0.3s ease',
...props.style
}}
/>
);
}
// WebP format with fallback
function WebPImage({ src, alt, ...props }) {
const [format, setFormat] = useState('webp');
const handleError = () => {
if (format === 'webp') {
setFormat('jpg');
}
};
const imageSrc = src.replace(/\.(jpg|jpeg|png)$/i, `.${format}`);
return (
<img
{...props}
src={imageSrc}
alt={alt}
onError={handleError}
/>
);
}Performance Monitoring
Real User Monitoring (RUM)
// Performance monitoring hook
import { useEffect, useRef } from 'react';
function usePerformanceMonitoring(pageName) {
const startTime = useRef(Date.now());
useEffect(() => {
// Track page load performance
const trackPagePerformance = () => {
if ('performance' in window) {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
const metrics = {
page: pageName,
loadTime: Date.now() - startTime.current,
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
largestContentfulPaint: getLCP()
};
// Send to analytics
analytics.track('page_performance', metrics);
}
};
// Track LCP
const getLCP = () => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
});
};
// Track when page becomes interactive
const trackInteractivity = () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
trackPagePerformance();
});
} else {
setTimeout(trackPagePerformance, 0);
}
};
trackInteractivity();
return () => {
// Cleanup performance observers
};
}, [pageName]);
}
// Memory usage monitoring
function useMemoryMonitoring() {
useEffect(() => {
const trackMemoryUsage = () => {
if ('memory' in performance) {
const memory = performance.memory;
const memoryData = {
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
usagePercentage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100
};
// Alert if memory usage is high
if (memoryData.usagePercentage > 80) {
console.warn('High memory usage detected:', memoryData);
analytics.track('high_memory_usage', memoryData);
}
}
};
const interval = setInterval(trackMemoryUsage, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, []);
}
// Component performance profiler
function ProfiledComponent({ children, name }) {
const renderCount = useRef(0);
const startTime = useRef(0);
const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
renderCount.current += 1;
const performanceData = {
component: name,
phase,
actualDuration,
baseDuration,
renderCount: renderCount.current
};
// Log slow renders
if (actualDuration > 16) { // 60fps threshold
console.warn(`Slow render detected in ${name}:`, performanceData);
analytics.track('slow_render', performanceData);
}
};
return (
<Profiler id={name} onRender={onRender}>
{children}
</Profiler>
);
}
// Banking app specific monitoring
function useBankingMetrics() {
const trackTransactionPerformance = useCallback((transactionType, duration) => {
analytics.track('transaction_performance', {
type: transactionType,
duration,
timestamp: Date.now()
});
// Alert for slow transactions
if (duration > 5000) { // 5 second threshold
analytics.track('slow_transaction', {
type: transactionType,
duration,
userAgent: navigator.userAgent,
connection: navigator.connection?.effectiveType
});
}
}, []);
const trackAPIPerformance = useCallback((endpoint, method, duration, status) => {
analytics.track('api_performance', {
endpoint,
method,
duration,
status,
timestamp: Date.now()
});
}, []);
return { trackTransactionPerformance, trackAPIPerformance };
}Memory Leak Prevention
Common Leak Patterns and Solutions
// Event listener cleanup
function useEventListener(event, handler, element = window) {
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(event, eventListener);
return () => {
element.removeEventListener(event, eventListener);
};
}, [event, element]);
}
// Timer cleanup
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
// Subscription cleanup
function useWebSocket(url) {
const [socket, setSocket] = useState(null);
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
setSocket(ws);
return () => {
ws.close();
};
}, [url]);
return { socket, messages };
}
// Async operation cleanup
function useAsyncOperation() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const isMountedRef = useRef(true);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const performAsyncOperation = useCallback(async (operation) => {
setLoading(true);
try {
const result = await operation();
// Only update state if component is still mounted
if (isMountedRef.current) {
setData(result);
}
} catch (error) {
if (isMountedRef.current) {
console.error('Async operation failed:', error);
}
} finally {
if (isMountedRef.current) {
setLoading(false);
}
}
}, []);
return { data, loading, performAsyncOperation };
}
// Observable cleanup (RxJS)
function useObservable(observable$) {
const [value, setValue] = useState(null);
useEffect(() => {
const subscription = observable$.subscribe(setValue);
return () => {
subscription.unsubscribe();
};
}, [observable$]);
return value;
}Production Results
IDFC FIRST Bank App Performance Improvements
65%
Load time reduction
80%
Bundle size reduction
90%
Memory usage optimization
4.9★
App store rating
Performance Checklist
Development
- • Implement code splitting for routes
- • Use React.memo for expensive components
- • Optimize bundle size with tree shaking
- • Implement lazy loading for images
- • Use virtual scrolling for large lists
- • Clean up subscriptions and timers
Production
- • Monitor Core Web Vitals
- • Track real user performance
- • Set up performance budgets
- • Implement error boundaries
- • Use CDN for static assets
- • Monitor memory usage patterns
Conclusion
Performance optimization is not a one-time task but an ongoing process that requires measurement, optimization, and monitoring. The strategies outlined here have proven effective at banking scale, where every millisecond of delay can impact user trust and business outcomes.
Start with measuring your current performance, implement the optimizations that provide the biggest impact for your use case, and establish monitoring to prevent performance regressions. Remember that perceived performance is often more important than actual performance—keep users engaged with smart loading strategies and responsive interactions.
Need Help Optimizing Your React App?
Performance optimization can be complex, especially at scale. I help teams identify bottlenecks and implement optimization strategies that deliver measurable results.