feat: Phase 5 - lazy load recharts and add chart error boundaries
High priority performance and reliability improvements:
1. Lazy Load Recharts Library
- Wrapped all chart components (PlaytimeChart, TopGamesChart, GamesDistributionChart) with React.lazy()
- Charts now code-split into separate bundle (380 kB / 112 kB gzipped)
- Charts only load when Dashboard view is accessed, not on initial page load
- Added Suspense boundaries with ChartSkeleton fallback
- Significantly reduces initial bundle size for better performance
2. Chart Error Boundaries
- Created ChartErrorBoundary component to catch rendering errors
- Prevents chart failures from crashing the entire page
- Displays user-friendly fallback UI with error details
- Each chart wrapped independently for isolated error handling
- Fallback shows descriptive titles and recovery suggestions
3. Loading States
- Charts already have internal loading states for data fetching
- Added Suspense fallback skeletons for component code loading
- Comprehensive loading UX: Suspense → Data Fetch → Render
Architecture:
- ChartErrorBoundary (catches rendering errors)
- Suspense (handles code splitting/lazy loading)
- Chart Component (internal data fetch loading states)
Benefits:
- Faster initial page load (charts not in main bundle)
- Better error resilience (isolated chart failures)
- Improved user experience (clear loading and error states)
All changes verified with TypeScript check and production build.
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
import { Component, ReactNode } from 'react';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallbackTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error boundary component for chart components
|
||||||
|
* Catches rendering errors in recharts and displays fallback UI
|
||||||
|
*/
|
||||||
|
export class ChartErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error('Chart rendering error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
|
||||||
|
<div className="h-96 flex flex-col items-center justify-center gap-4">
|
||||||
|
<div className="p-4 bg-destructive/10 rounded-full">
|
||||||
|
<AlertTriangle size={32} className="text-destructive" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{this.props.fallbackTitle || 'Chart Error'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Unable to render chart. Please try refreshing the page.
|
||||||
|
</p>
|
||||||
|
{this.state.error && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-2 font-mono">
|
||||||
|
{this.state.error.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import { Clock, GamepadIcon, TrendingUp, BarChart3 } from 'lucide-react';
|
import { Clock, GamepadIcon, TrendingUp, BarChart3 } from 'lucide-react';
|
||||||
import { useUserStats } from '../../hooks/usePlayTracking';
|
import { useUserStats } from '../../hooks/usePlayTracking';
|
||||||
import { PlaytimeChart } from './PlaytimeChart';
|
|
||||||
import { TopGamesChart } from './TopGamesChart';
|
|
||||||
import { GamesDistributionChart } from './GamesDistributionChart';
|
|
||||||
import { useDateTimeFormat } from '../../hooks/useDateTimeFormat';
|
import { useDateTimeFormat } from '../../hooks/useDateTimeFormat';
|
||||||
|
import { ChartErrorBoundary } from './ChartErrorBoundary';
|
||||||
|
|
||||||
|
// Lazy load chart components to reduce initial bundle size
|
||||||
|
const PlaytimeChart = lazy(() => import('./PlaytimeChart').then(m => ({ default: m.PlaytimeChart })));
|
||||||
|
const TopGamesChart = lazy(() => import('./TopGamesChart').then(m => ({ default: m.TopGamesChart })));
|
||||||
|
const GamesDistributionChart = lazy(() => import('./GamesDistributionChart').then(m => ({ default: m.GamesDistributionChart })));
|
||||||
|
|
||||||
|
// Skeleton loader for chart components
|
||||||
|
function ChartSkeleton({ height = 'h-80' }: { height?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-lg p-6 border border-border shadow-md">
|
||||||
|
<div className={`${height} flex items-center justify-center`}>
|
||||||
|
<div className="animate-pulse text-muted-foreground">Loading chart...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatPlaytime(seconds: number): string {
|
function formatPlaytime(seconds: number): string {
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
@@ -121,15 +136,27 @@ export function UserStatsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Play Activity Over Time Chart */}
|
{/* Play Activity Over Time Chart */}
|
||||||
<PlaytimeChart />
|
<ChartErrorBoundary fallbackTitle="Play Activity Chart Error">
|
||||||
|
<Suspense fallback={<ChartSkeleton height="h-96" />}>
|
||||||
|
<PlaytimeChart />
|
||||||
|
</Suspense>
|
||||||
|
</ChartErrorBoundary>
|
||||||
|
|
||||||
{/* Charts Grid */}
|
{/* Charts Grid */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* Top Games Bar Chart */}
|
{/* Top Games Bar Chart */}
|
||||||
<TopGamesChart />
|
<ChartErrorBoundary fallbackTitle="Top Games Chart Error">
|
||||||
|
<Suspense fallback={<ChartSkeleton height="h-96" />}>
|
||||||
|
<TopGamesChart />
|
||||||
|
</Suspense>
|
||||||
|
</ChartErrorBoundary>
|
||||||
|
|
||||||
{/* Games Distribution Pie Chart */}
|
{/* Games Distribution Pie Chart */}
|
||||||
<GamesDistributionChart />
|
<ChartErrorBoundary fallbackTitle="Distribution Chart Error">
|
||||||
|
<Suspense fallback={<ChartSkeleton height="h-96" />}>
|
||||||
|
<GamesDistributionChart />
|
||||||
|
</Suspense>
|
||||||
|
</ChartErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user