Performance Optimization Guide ⚡
Accelerate your reptile care app with advanced performance techniques. A comprehensive guide to making CrittrHavens blazing fast.

Performance Philosophy
Speed is a feature, not a luxury.
Core Principles
Performance Strategy:
- Measure First, Optimize Second: Data-driven performance improvements
- Progressive Enhancement: Fast initial load, enhanced features later
- Perceived Performance: Make it feel fast, not just be fast
- Mobile-First Performance: Optimize for constrained devices
- Continuous Monitoring: Track performance metrics over time
Performance Budget
Target Metrics:
// Performance goals
const performanceBudget = {
// Core Web Vitals
LCP: 2500, // Largest Contentful Paint < 2.5s
FID: 100, // First Input Delay < 100ms
CLS: 0.1, // Cumulative Layout Shift < 0.1
// Additional metrics
TTI: 3800, // Time to Interactive < 3.8s
FCP: 1800, // First Contentful Paint < 1.8s
// Bundle sizes
mainBundle: 200, // Main JS bundle < 200KB
cssBundle: 50, // CSS bundle < 50KB
totalSize: 500, // Total resources < 500KB
};
Code Splitting and Lazy Loading
Load only what you need, when you need it.
Route-Based Code Splitting
Lazy Loading Routes:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
// Lazy load all route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const CrittrDetails = lazy(() => import('./pages/CrittrDetails'));
const HabitatDetails = lazy(() => import('./pages/HabitatDetails'));
const Reports = lazy(() => import('./pages/Reports'));
// Heavy features loaded on demand
const VetReport = lazy(() =>
import(/* webpackChunkName: "vet-report" */ './pages/VetReport')
);
function App() {
return (
<Suspense fallback={<PageLoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/crittrs/:id" element={<CrittrDetails />} />
<Route path="/habitats/:id" element={<HabitatDetails />} />
<Route path="/reports" element={<Reports />} />
<Route path="/vet-report" element={<VetReport />} />
</Routes>
</Suspense>
);
}
Component-Level Code Splitting
Lazy Loading Heavy Components:
// Split heavy components
const ChartComponent = lazy(() => import('./components/ChartComponent'));
const ImageEditor = lazy(() => import('./components/ImageEditor'));
const PdfGenerator = lazy(() => import('./components/PdfGenerator'));
// Load on interaction
function ReportPage() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<Button onClick={() => setShowChart(true)}>
Show Growth Chart
</Button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<ChartComponent data={data} />
</Suspense>
)}
);
}
Dynamic Imports
Conditional Module Loading:
// Load modules based on user actions
async function exportData(format: string) {
switch (format) {
case 'pdf':
const { generatePDF } = await import('./utils/pdf-generator');
return generatePDF(data);
case 'excel':
const { generateExcel } = await import('./utils/excel-generator');
return generateExcel(data);
case 'csv':
const { generateCSV } = await import('./utils/csv-generator');
return generateCSV(data);
}
}
// Load polyfills only when needed
async function loadPolyfills() {
if (!window.IntersectionObserver) {
await import('intersection-observer');
}
if (!window.ResizeObserver) {
await import('resize-observer-polyfill');
}
}
Image Optimization Strategies
Serving the right image at the right time.
Lazy Loading Images
Intersection Observer Pattern:
import { useState, useEffect, useRef } from 'react';
export function LazyImage({ src, alt, placeholder, className }) {
const [imageSrc, setImageSrc] = useState(placeholder);
const [imageRef, setImageRef] = useState<HTMLImageElement>();
const observerRef = useRef<IntersectionObserver>();
useEffect(() => {
if (!imageRef) return;
observerRef.current = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Load the real image
setImageSrc(src);
// Stop observing
observerRef.current?.unobserve(entry.target);
}
});
},
{
rootMargin: '50px', // Start loading 50px before visible
}
);
observerRef.current.observe(imageRef);
return () => {
observerRef.current?.disconnect();
};
}, [imageRef, src]);
return (
<img
ref={setImageRef}
src={imageSrc}
alt={alt}
className={className}
loading="lazy" // Native lazy loading fallback
/>
);
}
Responsive Images
Serving Optimized Sizes:
export function ResponsiveImage({ src, alt, sizes }) {
// Generate srcset for different resolutions
const generateSrcSet = (baseUrl: string) => {
const widths = [320, 640, 768, 1024, 1280, 1920];
return widths
.map(w => `${baseUrl}?w=${w} ${w}w`)
.join(', ');
};
return (
<picture>
{/* WebP for modern browsers */}
<source
type="image/webp"
srcSet={generateSrcSet(src.replace(/\.[^.]+$/, '.webp'))}
sizes={sizes}
/>
{/* JPEG fallback */}
<source
type="image/jpeg"
srcSet={generateSrcSet(src)}
sizes={sizes}
/>
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
/>
</picture>
);
}
Image Preprocessing
Supabase Storage Transformation:
// Generate optimized image URLs
export function getOptimizedImageUrl(
path: string,
options: ImageOptions = {}
) {
const {
width = 800,
height,
quality = 80,
format = 'webp'
} = options;
const params = new URLSearchParams({
width: width.toString(),
quality: quality.toString(),
format
});
if (height) {
params.append('height', height.toString());
}
return `${SUPABASE_URL}/storage/v1/render/image/public/${path}?${params}`;
}
// Progressive image loading
export function ProgressiveImage({ src, alt }) {
const [currentSrc, setCurrentSrc] = useState(
getOptimizedImageUrl(src, { width: 40, quality: 10 })
);
useEffect(() => {
// Load low quality immediately
const lowQuality = new Image();
lowQuality.src = getOptimizedImageUrl(src, { width: 400, quality: 50 });
lowQuality.onload = () => {
setCurrentSrc(lowQuality.src);
// Then load high quality
const highQuality = new Image();
highQuality.src = getOptimizedImageUrl(src, { width: 1200, quality: 90 });
highQuality.onload = () => setCurrentSrc(highQuality.src);
};
}, [src]);
return <img src={currentSrc} alt={alt} />;
}
Query Optimization with React Query
Efficient data fetching and caching.
Query Configuration
Optimized Query Settings:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Cache data for 5 minutes
staleTime: 5 * 60 * 1000,
// Keep in cache for 10 minutes
gcTime: 10 * 60 * 1000,
// Don't refetch on window focus by default
refetchOnWindowFocus: false,
// Retry failed requests with exponential backoff
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
Prefetching Strategies
Anticipatory Data Loading:
// Prefetch on hover
export function CrittrCard({ crittr }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['crittr', crittr.id],
queryFn: () => fetchCrittrDetails(crittr.id),
staleTime: 10 * 1000, // Consider fresh for 10 seconds
});
};
return (
<Card
onMouseEnter={handleMouseEnter}
onClick={() => navigate(`/crittrs/${crittr.id}`)}
>
{/* Card content */}
</Card>
);
}
// Prefetch next page
export function usePrefetchNextPage(currentPage: number) {
const queryClient = useQueryClient();
useEffect(() => {
const nextPage = currentPage + 1;
queryClient.prefetchQuery({
queryKey: ['crittrs', { page: nextPage }],
queryFn: () => fetchCrittrs({ page: nextPage }),
});
}, [currentPage, queryClient]);
}
Query Deduplication
Preventing Redundant Requests:
// Share queries across components
export function useCrittrData(crittrId: string) {
// This query will be shared by all components
return useQuery({
queryKey: ['crittr', crittrId],
queryFn: () => fetchCrittr(crittrId),
// Use cached data as placeholder while fetching
placeholderData: () => {
// Try to use data from list query
const listData = queryClient.getQueryData(['crittrs']);
return listData?.find(c => c.id === crittrId);
},
});
}
Virtual Scrolling Implementation
Efficiently rendering large lists.
Basic Virtual Scrolling
Using TanStack Virtual:
import { useVirtualizer } from '@tanstack/react-virtual';
export function VirtualCrittrList({ crittrs }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: crittrs.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Estimated item height
overscan: 5, // Render 5 items outside viewport
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<CrittrCard crittr={crittrs[virtualItem.index]} />
))}
);
}
Dynamic Height Virtual Scrolling
Variable Item Heights:
export function DynamicVirtualList({ items, renderItem }) {
const [measurements, setMeasurements] = useState<Record<number, number>>({});
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: useCallback(
(index) => measurements[index] || 100,
[measurements]
),
measureElement: useCallback(
(element, entry, instance) => {
const index = entry.index;
const height = element.getBoundingClientRect().height;
if (measurements[index] !== height) {
setMeasurements(prev => ({ ...prev, [index]: height }));
instance.measureElement(element);
}
},
[measurements]
),
});
return (
<div ref={parentRef} className="h-full overflow-auto">
{/* Virtual list implementation */}
);
}
Bundle Size Optimization
Keeping your JavaScript lean.
Tree Shaking
Removing Unused Code:
// ❌ Bad: Import entire library
import * as _ from 'lodash';
const result = _.debounce(fn, 300);
// ✅ Good: Import only what you need
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// ❌ Bad: Import entire icon library
import * as Icons from 'lucide-react';
<Icons.Home />
// ✅ Good: Import specific icons
import { Home } from 'lucide-react';
<Home />
Bundle Analysis
Vite Configuration:
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'query-vendor': ['@tanstack/react-query'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'utils': ['date-fns', 'clsx', 'tailwind-merge'],
},
},
},
// Enable minification
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
plugins: [
// Bundle analyzer
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
});
Dynamic Imports for Large Libraries
Load Heavy Dependencies on Demand:
// Chart library loaded only when needed
async function showChart(data: ChartData) {
const { Chart } = await import('chart.js/auto');
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'line',
data: data,
});
}
// PDF generation loaded on export
async function exportPDF() {
const { jsPDF } = await import('jspdf');
const doc = new jsPDF();
// Generate PDF
}
Memory Leak Prevention
Keeping memory usage under control.
Cleanup Patterns
Proper Resource Cleanup:
export function useEventListener(
event: string,
handler: EventListener,
element = window
) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event: Event) => savedHandler.current(event);
// Add listener
element.addEventListener(event, eventListener);
// Cleanup function
return () => {
element.removeEventListener(event, eventListener);
};
}, [event, element]);
}
Observer Cleanup
Managing Observers:
export function useIntersectionObserver(
ref: RefObject<Element>,
options?: IntersectionObserverInit
) {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setIntersecting(entry.isIntersecting);
}, options);
if (ref.current) {
observer.observe(ref.current);
}
// Cleanup
return () => {
observer.disconnect();
};
}, [ref, options]);
return isIntersecting;
}
WeakMap for Object References
Preventing Memory Leaks:
// Use WeakMap for object metadata
const crittrMetadata = new WeakMap<Crittr, Metadata>();
export function getCrittrMetadata(crittr: Crittr): Metadata {
if (!crittrMetadata.has(crittr)) {
crittrMetadata.set(crittr, computeMetadata(crittr));
}
return crittrMetadata.get(crittr)!;
}
// Objects can be garbage collected when no longer referenced
Performance Monitoring Tools
Measuring and tracking performance.
Web Vitals Monitoring
Real User Monitoring:
import { getCLS, getFID, getLCP, getTTFB, getFCP } from 'web-vitals';
function sendToAnalytics(metric: Metric) {
// Send to your analytics endpoint
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
});
// Use sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', body);
}
}
// Monitor Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
getFCP(sendToAnalytics);
Custom Performance Marks
Application-Specific Metrics:
export class PerformanceMonitor {
mark(name: string) {
performance.mark(name);
}
measure(name: string, startMark: string, endMark?: string) {
performance.measure(name, startMark, endMark);
const measure = performance.getEntriesByName(name, 'measure')[0];
// Log to analytics
this.logMetric({
name,
duration: measure.duration,
timestamp: Date.now(),
});
}
// Track component render time
trackComponentRender(componentName: string) {
const startMark = `${componentName}-start`;
const endMark = `${componentName}-end`;
this.mark(startMark);
return () => {
this.mark(endMark);
this.measure(`${componentName}-render`, startMark, endMark);
};
}
}
// Usage in component
export function ExpensiveComponent() {
useEffect(() => {
const endTracking = performanceMonitor.trackComponentRender('ExpensiveComponent');
return endTracking;
}, []);
return <div>...;
}
React DevTools Profiler
Component Performance Analysis:
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
// Log render performance
console.log(`${id} (${phase}) took ${actualDuration}ms`);
// Track slow renders
if (actualDuration > 16) { // Slower than 60fps
logSlowRender({
component: id,
phase,
duration: actualDuration,
});
}
}
export function ProfiledApp() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>
);
}
Mobile Performance Considerations
Optimizing for mobile devices.
Touch Responsiveness
Optimizing Touch Interactions:
// Use passive event listeners
export function usePassiveTouchHandlers(ref: RefObject<HTMLElement>) {
useEffect(() => {
const element = ref.current;
if (!element) return;
const options = { passive: true };
const handleTouchStart = (e: TouchEvent) => {
// Handle touch
};
element.addEventListener('touchstart', handleTouchStart, options);
return () => {
element.removeEventListener('touchstart', handleTouchStart);
};
}, [ref]);
}
// Debounce expensive operations
export function useDeboucedTouch(handler: Function, delay = 100) {
const timeoutRef = useRef<NodeJS.Timeout>();
const debouncedHandler = useCallback((...args) => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => handler(...args), delay);
}, [handler, delay]);
return debouncedHandler;
}
Mobile-Specific Optimizations
Reduced Motion and Data Saver:
// Respect user preferences
export function useReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(
window.matchMedia('(prefers-reduced-motion: reduce)').matches
);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
const handleChange = (e: MediaQueryListEvent) => {
setPrefersReducedMotion(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
return prefersReducedMotion;
}
// Data saver mode
export function useDataSaver() {
const [saveData, setSaveData] = useState(false);
useEffect(() => {
// Check connection type
const connection = navigator.connection;
if (connection) {
setSaveData(
connection.saveData ||
connection.effectiveType === 'slow-2g' ||
connection.effectiveType === '2g'
);
}
}, []);
return saveData;
}
// Conditional loading based on connection
export function AdaptiveImage({ src, alt }) {
const saveData = useDataSaver();
if (saveData) {
// Load low quality or placeholder
return <img src={getLowQualityUrl(src)} alt={alt} />;
}
return <ResponsiveImage src={src} alt={alt} />;
}
Database Query Optimization
Efficient data fetching from Supabase.
Query Optimization
Efficient Database Queries:
// ❌ Bad: Fetching everything
const { data } = await supabase
.from('crittrs')
.select('*');
// ✅ Good: Select only needed fields
const { data } = await supabase
.from('crittrs')
.select('id, name, species, last_fed')
.eq('haven_id', havenId)
.order('name')
.limit(20);
// ❌ Bad: N+1 queries
for (const crittr of crittrs) {
const logs = await supabase
.from('logs')
.select('*')
.eq('crittr_id', crittr.id);
}
// ✅ Good: Single query with join
const { data } = await supabase
.from('crittrs')
.select(`
id,
name,
logs (
id,
type,
created_at
)
`)
.eq('haven_id', havenId);
Pagination Strategies
Efficient Data Loading:
// Cursor-based pagination
export async function fetchCrittrsPaginated(cursor?: string, limit = 20) {
let query = supabase
.from('crittrs')
.select('*')
.order('created_at', { ascending: false })
.limit(limit);
if (cursor) {
query = query.lt('created_at', cursor);
}
const { data, error } = await query;
return {
items: data,
nextCursor: data?.[data.length - 1]?.created_at,
};
}
// Infinite scroll implementation
export function useInfiniteCrittrs() {
return useInfiniteQuery({
queryKey: ['crittrs', 'infinite'],
queryFn: ({ pageParam }) => fetchCrittrsPaginated(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: undefined,
});
}
Caching Strategies
Smart caching for instant responses.
Service Worker Caching
PWA Cache Strategy:
// service-worker.js
const CACHE_NAME = 'crittrhavens-v1';
const urlsToCache = [
'/',
'/static/css/main.css',
'/static/js/bundle.js',
'/offline.html',
];
// Install and cache essential resources
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Network-first strategy for API calls
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
// Clone and cache the response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseToCache));
return response;
})
.catch(() => {
// Fallback to cache if offline
return caches.match(event.request);
})
);
}
});
Local Storage Caching
Persistent Client-Side Cache:
class LocalCache {
private prefix = 'crittrhavens_cache_';
set(key: string, data: any, ttl = 3600000) { // 1 hour default
const item = {
data,
timestamp: Date.now(),
ttl,
};
try {
localStorage.setItem(this.prefix + key, JSON.stringify(item));
} catch (e) {
// Handle quota exceeded
this.cleanup();
}
}
get(key: string) {
const item = localStorage.getItem(this.prefix + key);
if (!item) return null;
const parsed = JSON.parse(item);
// Check if expired
if (Date.now() - parsed.timestamp > parsed.ttl) {
localStorage.removeItem(this.prefix + key);
return null;
}
return parsed.data;
}
cleanup() {
// Remove oldest entries if storage is full
const items = Object.keys(localStorage)
.filter(key => key.startsWith(this.prefix))
.map(key => ({
key,
timestamp: JSON.parse(localStorage.getItem(key)!).timestamp,
}))
.sort((a, b) => a.timestamp - b.timestamp);
// Remove oldest 25%
const toRemove = Math.floor(items.length * 0.25);
items.slice(0, toRemove).forEach(item => {
localStorage.removeItem(item.key);
});
}
}
Best Practices
Performance optimization guidelines.
Performance Checklist
Before deployment:
- Code splitting implemented for routes
- Images lazy loaded and optimized
- Bundle size under 500KB
- Core Web Vitals passing
- Virtual scrolling for long lists
- Memory leaks prevented
- Database queries optimized
- Caching strategy implemented
- Mobile performance tested
- Performance monitoring active
Do's and Don'ts
✅ DO:
- Measure performance before optimizing
- Use production builds for testing
- Optimize critical rendering path
- Implement progressive enhancement
- Cache expensive computations
❌ DON'T:
- Optimize prematurely
- Block the main thread
- Load unnecessary resources
- Ignore mobile performance
- Cache sensitive data
Common Patterns
Reusable performance solutions.
Debounce and Throttle
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
export function useThrottle<T>(value: T, interval: number): T {
const [throttledValue, setThrottledValue] = useState(value);
const lastUpdated = useRef(Date.now());
useEffect(() => {
const now = Date.now();
if (now - lastUpdated.current >= interval) {
lastUpdated.current = now;
setThrottledValue(value);
} else {
const timer = setTimeout(() => {
lastUpdated.current = Date.now();
setThrottledValue(value);
}, interval - (now - lastUpdated.current));
return () => clearTimeout(timer);
}
}, [value, interval]);
return throttledValue;
}
Memoization Patterns
// Memoize expensive computations
export const calculateStats = memoize((crittrs: Crittr[]) => {
// Expensive calculation
return {
total: crittrs.length,
bySpecies: groupBy(crittrs, 'species'),
averageAge: mean(crittrs.map(c => c.age)),
};
});
// React memo with comparison
export const CrittrCard = memo(
({ crittr }: { crittr: Crittr }) => {
return <Card>{/* Component */}</Card>;
},
(prevProps, nextProps) => {
// Only re-render if crittr data changes
return prevProps.crittr.id === nextProps.crittr.id &&
prevProps.crittr.updatedAt === nextProps.crittr.updatedAt;
}
);
Next Steps
Continue optimizing your application.
Making your reptile care app faster than a striking snake. 🐍