Performance Optimizations - DrunkOnJava/MaterialMDashboard GitHub Wiki
This guide covers various performance optimization techniques implemented in Material Dashboard.
The application uses automatic code splitting through dynamic imports:
// Route-based code splitting
const Dashboard = lazy(() => import('./screens/Dashboard'));
const Charts = lazy(() => import('./screens/Charts'));
const Forms = lazy(() => import('./screens/Forms'));
// Component-based code splitting
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));
Vite configuration for optimized bundles:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk for core dependencies
vendor: ['react', 'react-dom', 'react-router-dom'],
// UI components chunk
ui: [
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'@radix-ui/react-tooltip',
],
// Charts chunk
charts: ['chart.js', 'react-chartjs-2'],
},
},
},
// Minification
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
},
});
Ensure proper tree shaking:
// ✅ Good - Named imports allow tree shaking
import { Button, Card } from '@/components/ui';
// ❌ Bad - Imports entire module
import * as UI from '@/components/ui';
Implement loading states with Suspense:
import { Suspense, lazy } from 'react';
import { DashboardSkeleton } from './components/DashboardSkeleton';
const Dashboard = lazy(() => import('./screens/Dashboard'));
function App() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
);
}
Use React.memo for expensive components:
// Memoize components that receive stable props
export const ExpensiveChart = React.memo(({ data, options }) => {
return <Line data={data} options={options} />;
}, (prevProps, nextProps) => {
// Custom comparison function
return prevProps.data === nextProps.data &&
prevProps.options === nextProps.options;
});
// useMemo for expensive calculations
const processedData = useMemo(() => {
return heavyDataProcessing(rawData);
}, [rawData]);
// useCallback for stable function references
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
Implement virtual scrolling for large lists:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Implemented lazy loading with IntersectionObserver:
// LazyImage component
import { useState, useEffect, useRef } from 'react';
export const LazyImage = ({ src, alt, placeholder, ...props }) => {
const [imageSrc, setImageSrc] = useState(placeholder);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '200px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [src]);
return <img ref={imgRef} src={imageSrc} alt={alt} {...props} />;
};
Use modern image formats:
<picture>
<source srcSet="/image.webp" type="image/webp" />
<source srcSet="/image.jpg" type="image/jpeg" />
<img src="/image.jpg" alt="Description" loading="lazy" />
</picture>
Optimize font loading:
/* Preload critical fonts */
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
/* Font display swap */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
}
Split contexts to prevent unnecessary re-renders:
// ❌ Bad - Single large context
const AppContext = createContext({
user: null,
theme: 'light',
settings: {},
// ... many more values
});
// ✅ Good - Separate contexts
const UserContext = createContext({ user: null });
const ThemeContext = createContext({ theme: 'light' });
const SettingsContext = createContext({ settings: {} });
Keep state close to where it's used:
// ✅ Good - Local state for local UI
function SearchBar() {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
// ❌ Bad - Global state for local UI
// Don't put query in global state if only SearchBar uses it
Implement caching strategies:
// Simple cache implementation
const cache = new Map();
async function fetchWithCache(url: string, options = {}) {
const cacheKey = `${url}-${JSON.stringify(options)}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const response = await fetch(url, options);
const data = await response.json();
cache.set(cacheKey, data);
return data;
}
// With expiration
class CacheWithExpiry {
private cache = new Map();
set(key: string, value: any, ttl: number) {
const expires = Date.now() + ttl;
this.cache.set(key, { value, expires });
}
get(key: string) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.value;
}
}
Debounce API requests:
import { debounce } from 'lodash-es';
function SearchComponent() {
const [results, setResults] = useState([]);
const debouncedSearch = useMemo(
() => debounce(async (query: string) => {
const data = await searchAPI(query);
setResults(data);
}, 300),
[]
);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
debouncedSearch(e.target.value);
};
return <input onChange={handleSearch} />;
}
Extract and inline critical CSS:
<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
.hero { ... }
.nav { ... }
</style>
<!-- Load non-critical CSS asynchronously -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
Use CSS modules or Tailwind for better performance:
// ✅ Good - Tailwind utilities (compiled at build time)
<div className="flex items-center justify-center p-4">
// ❌ Avoid - Runtime CSS-in-JS for static styles
const StyledDiv = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
`;
Tailwind configuration for CSS purging:
// tailwind.config.js
module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
// Tailwind automatically purges unused styles in production
}
Optimize event handlers:
// ✅ Good - Event delegation
function List({ items }) {
const handleClick = (e: React.MouseEvent) => {
const id = e.currentTarget.getAttribute('data-id');
if (id) {
handleItemClick(id);
}
};
return (
<ul onClick={handleClick}>
{items.map(item => (
<li key={item.id} data-id={item.id}>
{item.name}
</li>
))}
</ul>
);
}
// ❌ Bad - Handler for each item
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
Use CSS transforms for animations:
/* ✅ Good - GPU accelerated */
.slide-in {
transform: translateX(100%);
transition: transform 0.3s ease;
}
.slide-in.active {
transform: translateX(0);
}
/* ❌ Bad - Triggers layout */
.slide-in {
left: 100%;
transition: left 0.3s ease;
}
Monitor key metrics:
// Web Vitals monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics service
console.log(metric);
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Use React DevTools Profiler to identify performance bottlenecks:
- Open React DevTools
- Navigate to Profiler tab
- Start recording
- Interact with your app
- Stop recording and analyze
Performance profiling:
- Open Chrome DevTools
- Go to Performance tab
- Start recording
- Interact with your app
- Stop recording
- Analyze flame chart
Implement caching with service worker:
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/main.js',
'/offline.html',
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Implement app shell architecture:
// Cache app shell
const APP_SHELL_CACHE = 'app-shell-v1';
const urlsToCache = [
'/',
'/manifest.json',
'/static/css/main.css',
'/static/js/main.js',
];
- Run production build and check bundle sizes
- Test with slow network throttling
- Verify lazy loading is working
- Check for memory leaks
- Audit with Lighthouse
- Test on real devices
- Monitor Web Vitals
Set performance budgets:
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
// Warn if chunks exceed size
maxParallelFileOps: 2,
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].js',
},
},
// Fail build if bundle exceeds limit
chunkSizeWarningLimit: 500, // 500kb
},
};
Performance optimization is an ongoing process. Regular monitoring and profiling help identify bottlenecks and opportunities for improvement. Focus on:
- Initial load performance
- Runtime performance
- Network optimization
- Asset optimization
- User experience metrics
Remember to measure before and after optimizations to ensure improvements are effective.
- Deployment - Deploy your optimized app
- Monitoring - Set up performance monitoring
- Contributing - Share your optimizations