Performance & Optimization
Page-Level Optimization Techniques
StockEase implements several optimization strategies to ensure fast page loads and smooth user experience.
Lazy Loading Pages
What Is Lazy Loading?
Lazy loading delays loading a page component until it's needed, reducing initial bundle size.
Implementation
/**
* App.tsx - Define lazy pages
*/
import { lazy, Suspense } from 'react';
import SkeletonLoader from './components/SkeletonLoader';
// Lazy load heavy page components
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const UserDashboard = lazy(() => import('./pages/UserDashboard'));
const AddProductPage = lazy(() => import('./pages/AddProductPage'));
const SearchProductPage = lazy(() => import('./pages/SearchProductPage'));
const ListStockPage = lazy(() => import('./pages/ListStockPage'));
const ChangeProductDetailsPage = lazy(
() => import('./pages/ChangeProductDetailsPage')
);
// Keep small pages as regular imports
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
// Route setup with Suspense
<Routes>
{/* Small pages - regular import */}
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
{/* Large pages - lazy loaded */}
<Route
path="/user"
element={
<Suspense fallback={<SkeletonLoader />}>
<UserDashboard />
</Suspense>
}
/>
<Route
path="/admin"
element={
<Suspense fallback={<SkeletonLoader />}>
<AdminDashboard />
</Suspense>
}
/>
</Routes>Bundle Impact
Without Lazy Loading:
app.js (500 KB)
ββ HomePage
ββ LoginPage
ββ AdminDashboard (200 KB)
ββ UserDashboard (180 KB)
ββ Other pages
With Lazy Loading:
app.js (150 KB) β Initial bundle (60KB smaller)
ββ Admin chunk (200 KB) β Loaded when needed
ββ User chunk (180 KB) β Loaded when needed
Loading Fallback
// Simple skeleton loader
<Suspense fallback={<SkeletonLoader />}>
<AdminDashboard />
</Suspense>
// Custom fallback
<Suspense fallback={
<div className="loading">
<Spinner />
<p>Loading dashboard...</p>
</div>
}>
<AdminDashboard />
</Suspense>React.memo - Prevent Unnecessary Re-renders
Problem: Unnecessary Re-renders
// Without memo - re-renders on every parent update
const UserDashboard = () => {
return <div>User Dashboard</div>;
};
// Parent component
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<UserDashboard /> {/* Re-renders when count changes */}
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</div>
);
};Solution: Memoize Component
// With memo - only re-renders if props change
const UserDashboard = React.memo(() => {
console.log('UserDashboard rendered');
return <div>User Dashboard</div>;
});
export default UserDashboard;
// Parent component
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<UserDashboard /> {/* Won't re-render */}
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</div>
);
};When to Use memo
// β
USE MEMO: Heavy component, rarely changing props
const AdminDashboard = React.memo(() => {
// Complex rendering, expensive calculations
});
// β DON'T USE: Simple component, props always change
const FilterBar = (props) => {
// Simple JSX, props change frequently
};
// β
USE MEMO: Expensive child components
const ProductTable = React.memo(({ products }) => {
// Renders 1000 rows
});
// β DON'T USE: Props always different
const Button = React.memo(({ onClick }) => {
// onClick function changes every render
});useMemo - Memoize Expensive Computations
Problem: Recalculating on Every Render
const AdminDashboard = () => {
const [products, setProducts] = useState([]);
const [filters, setFilters] = useState({});
// PROBLEM: Filters 1000 items every render
// Even if products and filters haven't changed!
const filteredProducts = products.filter(p =>
matchesFilters(p, filters)
);
return <ProductTable products={filteredProducts} />;
};Solution: Memoize the Calculation
const AdminDashboard = () => {
const [products, setProducts] = useState([]);
const [filters, setFilters] = useState({});
// Only recalculate if products or filters change
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(p => matchesFilters(p, filters));
}, [products, filters]); // Dependency array
return <ProductTable products={filteredProducts} />;
};Use Cases
// Expensive calculation
const statistics = useMemo(() => {
return {
totalPrice: products.reduce((sum, p) => sum + p.price, 0),
totalQuantity: products.reduce((sum, p) => sum + p.qty, 0),
avgPrice: totalPrice / products.length
};
}, [products]);
// Complex filtering
const filtered = useMemo(() => {
return applyFiltersAndSort(products, filters, sortBy);
}, [products, filters, sortBy]);
// Derived data
const groupedByCategory = useMemo(() => {
return groupBy(products, 'category');
}, [products]);useCallback - Memoize Event Handlers
Problem: Function References Change
const AdminDashboard = () => {
const [products, setProducts] = useState([]);
// PROBLEM: Function reference changes every render
// ProductTable will re-render even if it has memo()
const handleDelete = (id) => {
setProducts(products.filter(p => p.id !== id));
};
return (
<ProductTable
products={products}
onDelete={handleDelete} {/* New function every time */}
/>
);
};Solution: Memoize the Handler
const AdminDashboard = () => {
const [products, setProducts] = useState([]);
// Function reference stays the same
const handleDelete = useCallback((id) => {
setProducts(prev => prev.filter(p => p.id !== id));
}, []); // No dependencies = same function always
return (
<ProductTable
products={products}
onDelete={handleDelete} {/* Same function reference */}
/>
);
};With Dependencies
const AdminDashboard = () => {
const [products, setProducts] = useState([]);
const [filter, setFilter] = useState('');
// Only recreate handler if filter changes
const handleSearch = useCallback((query) => {
// Use current filter value
const results = searchProducts(query, filter);
displayResults(results);
}, [filter]); // Recreate if filter changes
return <SearchBar onSearch={handleSearch} />;
};Code Splitting Best Practices
Split by Route
// pages/
// βββ HomePage.tsx (small)
// βββ LoginPage.tsx (small)
// βββ AdminDashboard.tsx (large)
// βββ UserDashboard.tsx (large)
import { lazy } from 'react';
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const UserDashboard = lazy(() => import('./pages/UserDashboard'));Split Large Components
// Before: Everything in one file
// AdminDashboard.tsx (400 lines)
// After: Break into chunks
// AdminDashboard.tsx (100 lines - main)
// AdminDashboard/
// βββ ProductTable.tsx
// βββ FilterBar.tsx
// βββ StatisticsCards.tsxAnalyze Bundle
# Install analyzer
npm install --save-dev vite-plugin-visualizer
# Build and analyze
npm run build
# Open generated HTML file to see bundle compositionData Fetching Optimization
Pagination
// Load only 10 items at a time
const ListStockPage = () => {
const [page, setPage] = useState(1);
const pageSize = 10;
useEffect(() => {
// Only fetch current page
const start = (page - 1) * pageSize;
ProductService.getProducts({
skip: start,
take: pageSize
});
}, [page]);
return (
<div>
<ProductTable products={products} />
<Pagination
current={page}
total={totalPages}
onChange={setPage}
/>
</div>
);
};Debounced Search
const SearchProductPage = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// Debounce: wait 300ms before searching
const timer = setTimeout(() => {
if (query.trim()) {
ProductService.search(query)
.then(setResults);
}
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<SearchResults results={results} />
</div>
);
};Virtual Scrolling (For Large Lists)
// Use react-window for large lists
import { FixedSizeList as List } from 'react-window';
const LargeProductList = ({ products }) => {
return (
<List
height={600}
itemCount={products.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{products[index].name} - ${products[index].price}
</div>
)}
</List>
);
};Image Optimization
Use Lazy Loading for Images
<img
src="product.jpg"
loading="lazy" {/* Don't load until near viewport */}
alt="Product"
/>Responsive Images
<picture>
<source
media="(max-width: 640px)"
srcSet="product-sm.jpg"
/>
<source
media="(min-width: 641px)"
srcSet="product-lg.jpg"
/>
<img src="product.jpg" alt="Product" />
</picture>Network Performance
Reduce API Calls
// Before: Multiple calls
await getUser();
await getProducts();
await getStats();
// After: Single combined call
const data = await getInitialData();
// Returns { user, products, stats }Cache API Responses
const apiClient = axios.create();
const cache = new Map();
apiClient.interceptors.request.use(config => {
if (cache.has(config.url)) {
return Promise.resolve(cache.get(config.url));
}
return config;
});
apiClient.interceptors.response.use(response => {
cache.set(response.config.url, response);
return response;
});Performance Monitoring
Measure Component Render Time
import { useState, useEffect } from 'react';
const AdminDashboard = () => {
useEffect(() => {
const start = performance.now();
return () => {
const end = performance.now();
console.log(`AdminDashboard render time: ${end - start}ms`);
};
}, []);
return <div>Dashboard</div>;
};Monitor Web Vitals
// Use web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log); // Cumulative Layout Shift
getFID(console.log); // First Input Delay
getFCP(console.log); // First Contentful Paint
getLCP(console.log); // Largest Contentful Paint
getTTFB(console.log); // Time to First BytePerformance Checklist
β DO:
// Lazy load heavy pages
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
// Memoize expensive components
const ProductTable = React.memo(({ products }) => {});
// Memoize expensive calculations
const stats = useMemo(() => calculate(products), [products]);
// Memoize event handlers
const handleDelete = useCallback((id) => delete(id), []);
// Paginate large lists
ProductService.getProducts({ page, pageSize });
// Debounce search input
useEffect(() => {
const timer = setTimeout(() => search(query), 300);
return () => clearTimeout(timer);
}, [query]);β DON'T:
// Don't load all pages in initial bundle
import AdminDashboard from './pages/AdminDashboard';
import UserDashboard from './pages/UserDashboard';
// ... 10 more imports
// Don't calculate in render without memoization
const filtered = products.filter(...); // Every render!
// Don't pass new objects/arrays as props
<Component onClick={() => handler()} /> // New function!
// Don't load all data at once
ProductService.getProducts(); // 100,000 items!
// Don't search on every keystroke
<input onChange={(e) => search(e.target.value)} /> // 1000 API calls!Related Documentation
- Overview - Page structure
- Page Lifecycle - Component patterns
- Authentication - Protected routes
- Testing - Testing strategies
Last Updated: November 2025