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:
Darkraise
2026-01-25 16:02:24 +07:00
parent 896a1771aa
commit a81dbc02be
2 changed files with 93 additions and 6 deletions
@@ -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 { useUserStats } from '../../hooks/usePlayTracking';
import { PlaytimeChart } from './PlaytimeChart';
import { TopGamesChart } from './TopGamesChart';
import { GamesDistributionChart } from './GamesDistributionChart';
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 {
if (seconds < 60) {
@@ -121,15 +136,27 @@ export function UserStatsPanel() {
</div>
{/* Play Activity Over Time Chart */}
<PlaytimeChart />
<ChartErrorBoundary fallbackTitle="Play Activity Chart Error">
<Suspense fallback={<ChartSkeleton height="h-96" />}>
<PlaytimeChart />
</Suspense>
</ChartErrorBoundary>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Games Bar Chart */}
<TopGamesChart />
<ChartErrorBoundary fallbackTitle="Top Games Chart Error">
<Suspense fallback={<ChartSkeleton height="h-96" />}>
<TopGamesChart />
</Suspense>
</ChartErrorBoundary>
{/* Games Distribution Pie Chart */}
<GamesDistributionChart />
<ChartErrorBoundary fallbackTitle="Distribution Chart Error">
<Suspense fallback={<ChartSkeleton height="h-96" />}>
<GamesDistributionChart />
</Suspense>
</ChartErrorBoundary>
</div>
</div>
);