Generated by Spark: Lazy-load chart libraries (recharts, d3) to reduce bundle size further

This commit is contained in:
2026-01-17 09:49:05 +00:00
committed by GitHub
parent 6fa92030d2
commit 602b10c6f3
11 changed files with 800 additions and 4 deletions

View File

@@ -0,0 +1,63 @@
import { useRecharts } from '@/hooks'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Warning } from '@phosphor-icons/react'
interface LazyBarChartProps {
data: Array<Record<string, any>>
xKey: string
yKey: string
width?: number
height?: number
color?: string
}
export function LazyBarChart({
data,
xKey,
yKey,
width = 600,
height = 300,
color = '#8884d8'
}: LazyBarChartProps) {
const { library: recharts, loading, error } = useRecharts()
if (loading) {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<Warning className="h-4 w-4" />
<AlertDescription>
Failed to load chart library. Please refresh the page.
</AlertDescription>
</Alert>
)
}
if (!recharts) {
return null
}
const { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } = recharts
return (
<ResponsiveContainer width={width} height={height}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey={yKey} fill={color} />
</BarChart>
</ResponsiveContainer>
)
}

View File

@@ -0,0 +1,87 @@
import { useD3 } from '@/hooks'
import { useEffect, useRef } from 'react'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Warning } from '@phosphor-icons/react'
interface LazyD3ChartProps {
data: Array<{ label: string; value: number }>
width?: number
height?: number
color?: string
}
export function LazyD3BarChart({
data,
width = 600,
height = 300,
color = '#8884d8'
}: LazyD3ChartProps) {
const { library: d3, loading, error } = useD3()
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!d3 || !svgRef.current || !data.length) return
const svg = d3.select(svgRef.current)
svg.selectAll('*').remove()
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
const chartWidth = width - margin.left - margin.right
const chartHeight = height - margin.top - margin.bottom
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
const x = d3.scaleBand()
.range([0, chartWidth])
.padding(0.1)
.domain(data.map(d => d.label))
const y = d3.scaleLinear()
.range([chartHeight, 0])
.domain([0, d3.max(data, d => d.value) || 0])
g.append('g')
.attr('transform', `translate(0,${chartHeight})`)
.call(d3.axisBottom(x))
g.append('g')
.call(d3.axisLeft(y))
g.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', d => x(d.label) || 0)
.attr('y', d => y(d.value))
.attr('width', x.bandwidth())
.attr('height', d => chartHeight - y(d.value))
.attr('fill', color)
}, [d3, data, width, height, color])
if (loading) {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<Warning className="h-4 w-4" />
<AlertDescription>
Failed to load D3 library. Please refresh the page.
</AlertDescription>
</Alert>
)
}
return (
<svg ref={svgRef} width={width} height={height} />
)
}

View File

@@ -0,0 +1,63 @@
import { useRecharts } from '@/hooks'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Warning } from '@phosphor-icons/react'
interface LazyLineChartProps {
data: Array<Record<string, any>>
xKey: string
yKey: string
width?: number
height?: number
color?: string
}
export function LazyLineChart({
data,
xKey,
yKey,
width = 600,
height = 300,
color = '#8884d8'
}: LazyLineChartProps) {
const { library: recharts, loading, error } = useRecharts()
if (loading) {
return (
<div className="space-y-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<Warning className="h-4 w-4" />
<AlertDescription>
Failed to load chart library. Please refresh the page.
</AlertDescription>
</Alert>
)
}
if (!recharts) {
return null
}
const { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } = recharts
return (
<ResponsiveContainer width={width} height={height}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xKey} />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey={yKey} stroke={color} />
</LineChart>
</ResponsiveContainer>
)
}

View File

@@ -8,6 +8,9 @@ export { FileTabs } from './FileTabs'
export { LabelWithBadge } from './LabelWithBadge'
export { LazyInlineMonacoEditor } from './LazyInlineMonacoEditor'
export { LazyMonacoEditor, preloadMonacoEditor } from './LazyMonacoEditor'
export { LazyLineChart } from './LazyLineChart'
export { LazyBarChart } from './LazyBarChart'
export { LazyD3BarChart } from './LazyD3BarChart'
export { LoadingFallback } from './LoadingFallback'
export { LoadingState } from './LoadingState'
export { MonacoEditorPanel } from './MonacoEditorPanel'

View File

@@ -0,0 +1,140 @@
import { useState, useEffect } from 'react'
import { loadRecharts, loadD3, loadThree, loadReactFlow } from '@/lib/library-loader'
type LoadState<T> = {
library: T | null
loading: boolean
error: Error | null
}
export function useRecharts() {
const [state, setState] = useState<LoadState<typeof import('recharts')>>({
library: null,
loading: true,
error: null,
})
useEffect(() => {
console.log('[HOOK] 🎨 useRecharts: Starting load')
let mounted = true
loadRecharts()
.then(recharts => {
if (mounted) {
console.log('[HOOK] ✅ useRecharts: Loaded successfully')
setState({ library: recharts, loading: false, error: null })
}
})
.catch(error => {
if (mounted) {
console.error('[HOOK] ❌ useRecharts: Load failed', error)
setState({ library: null, loading: false, error })
}
})
return () => {
mounted = false
}
}, [])
return state
}
export function useD3() {
const [state, setState] = useState<LoadState<typeof import('d3')>>({
library: null,
loading: true,
error: null,
})
useEffect(() => {
console.log('[HOOK] 📊 useD3: Starting load')
let mounted = true
loadD3()
.then(d3 => {
if (mounted) {
console.log('[HOOK] ✅ useD3: Loaded successfully')
setState({ library: d3, loading: false, error: null })
}
})
.catch(error => {
if (mounted) {
console.error('[HOOK] ❌ useD3: Load failed', error)
setState({ library: null, loading: false, error })
}
})
return () => {
mounted = false
}
}, [])
return state
}
export function useThree() {
const [state, setState] = useState<LoadState<typeof import('three')>>({
library: null,
loading: true,
error: null,
})
useEffect(() => {
console.log('[HOOK] 🎮 useThree: Starting load')
let mounted = true
loadThree()
.then(three => {
if (mounted) {
console.log('[HOOK] ✅ useThree: Loaded successfully')
setState({ library: three, loading: false, error: null })
}
})
.catch(error => {
if (mounted) {
console.error('[HOOK] ❌ useThree: Load failed', error)
setState({ library: null, loading: false, error })
}
})
return () => {
mounted = false
}
}, [])
return state
}
export function useReactFlow() {
const [state, setState] = useState<LoadState<typeof import('reactflow')>>({
library: null,
loading: true,
error: null,
})
useEffect(() => {
console.log('[HOOK] 🔀 useReactFlow: Starting load')
let mounted = true
loadReactFlow()
.then(reactflow => {
if (mounted) {
console.log('[HOOK] ✅ useReactFlow: Loaded successfully')
setState({ library: reactflow, loading: false, error: null })
}
})
.catch(error => {
if (mounted) {
console.error('[HOOK] ❌ useReactFlow: Load failed', error)
setState({ library: null, loading: false, error })
}
})
return () => {
mounted = false
}
}, [])
return state
}

View File

@@ -1,6 +1,7 @@
export * from './core/use-kv-state'
export * from './core/use-debounced-save'
export * from './core/use-clipboard'
export * from './core/use-library-loader'
export * from './ui/use-dialog'
export * from './ui/use-selection'

View File

@@ -130,6 +130,67 @@ if (loader.isLoaded('MyComponent')) {
loader.reset()
```
### `library-loader.ts`
Lazy loading utilities for heavy chart and visualization libraries.
**Supported Libraries:**
- Recharts (~450KB)
- D3 (~500KB)
- Three.js (~600KB)
- ReactFlow (~300KB)
**Key Functions:**
#### `loadRecharts()`, `loadD3()`, `loadThree()`, `loadReactFlow()`
Load libraries with retry logic and caching.
**Usage:**
```typescript
import { loadRecharts, loadD3 } from '@/lib/library-loader'
async function loadChart() {
const recharts = await loadRecharts()
// Use recharts
}
```
#### `preloadLibrary(libraryName)`
Preload library before it's needed.
**Usage:**
```typescript
import { preloadLibrary } from '@/lib/library-loader'
// Preload on hover
<button onMouseEnter={() => preloadLibrary('recharts')}>
View Charts
</button>
```
#### `clearLibraryCache()`
Clear all cached library imports.
**React Hooks:**
Use with hooks for automatic loading state management:
```typescript
import { useRecharts, useD3 } from '@/hooks'
function Chart() {
const { library: recharts, loading, error } = useRecharts()
if (loading) return <Skeleton />
if (error) return <Alert>Failed to load</Alert>
if (!recharts) return null
const { LineChart } = recharts
return <LineChart />
}
```
**See `/docs/LAZY_LOADING_CHARTS.md` for complete documentation.**
### `utils.ts`
General utility functions (shadcn standard).

111
src/lib/library-loader.ts Normal file
View File

@@ -0,0 +1,111 @@
const libraryCache = new Map<string, Promise<any>>()
interface LibraryLoadOptions {
timeout?: number
retries?: number
}
async function loadWithRetry<T>(
libraryName: string,
importFn: () => Promise<T>,
options: LibraryLoadOptions = {}
): Promise<T> {
const { timeout = 10000, retries = 3 } = options
if (libraryCache.has(libraryName)) {
console.log(`[LIBRARY] ✅ ${libraryName} already loaded from cache`)
return libraryCache.get(libraryName)!
}
console.log(`[LIBRARY] 📦 Loading ${libraryName}...`)
const loadPromise = new Promise<T>((resolve, reject) => {
let attempts = 0
const attemptLoad = async () => {
attempts++
console.log(`[LIBRARY] 🔄 Loading ${libraryName} (attempt ${attempts}/${retries})`)
const timeoutId = setTimeout(() => {
console.warn(`[LIBRARY] ⏰ ${libraryName} load timeout after ${timeout}ms`)
reject(new Error(`${libraryName} load timeout after ${timeout}ms`))
}, timeout)
try {
const library = await importFn()
clearTimeout(timeoutId)
console.log(`[LIBRARY] ✅ ${libraryName} loaded successfully`)
resolve(library)
} catch (error) {
clearTimeout(timeoutId)
console.error(`[LIBRARY] ❌ ${libraryName} load failed (attempt ${attempts}):`, error)
if (attempts < retries) {
console.log(`[LIBRARY] 🔁 Retrying ${libraryName} in ${attempts * 1000}ms...`)
setTimeout(attemptLoad, attempts * 1000)
} else {
console.error(`[LIBRARY] ❌ ${libraryName} all retry attempts exhausted`)
reject(error)
}
}
}
attemptLoad()
})
libraryCache.set(libraryName, loadPromise)
return loadPromise
}
export async function loadRecharts() {
return loadWithRetry('recharts', () => import('recharts'))
}
export async function loadD3() {
return loadWithRetry('d3', () => import('d3'))
}
export async function loadThree() {
return loadWithRetry('three', () => import('three'))
}
export async function loadReactFlow() {
return loadWithRetry('reactflow', () => import('reactflow'))
}
export function preloadLibrary(libraryName: 'recharts' | 'd3' | 'three' | 'reactflow') {
console.log(`[LIBRARY] 🎯 Preloading ${libraryName}`)
switch (libraryName) {
case 'recharts':
loadRecharts().catch(err => console.warn(`[LIBRARY] ⚠️ Preload failed for recharts:`, err))
break
case 'd3':
loadD3().catch(err => console.warn(`[LIBRARY] ⚠️ Preload failed for d3:`, err))
break
case 'three':
loadThree().catch(err => console.warn(`[LIBRARY] ⚠️ Preload failed for three:`, err))
break
case 'reactflow':
loadReactFlow().catch(err => console.warn(`[LIBRARY] ⚠️ Preload failed for reactflow:`, err))
break
}
}
export function getLibraryLoadStatus(libraryName: string): 'not-loaded' | 'loading' | 'loaded' | 'error' {
if (!libraryCache.has(libraryName)) {
return 'not-loaded'
}
const promise = libraryCache.get(libraryName)!
return promise.then(
() => 'loaded',
() => 'error'
) as any
}
export function clearLibraryCache() {
console.log('[LIBRARY] 🧹 Clearing library cache')
libraryCache.clear()
}

View File

@@ -1,6 +1,7 @@
import { getEnabledPages } from '@/config/page-loader'
import { preloadComponentByName, ComponentName } from '@/lib/component-registry'
import { FeatureToggles } from '@/types/project'
import { preloadLibrary } from '@/lib/library-loader'
interface PreloadStrategy {
preloadAdjacent: boolean
@@ -148,6 +149,13 @@ export class RoutePreloadManager {
})
}
preloadLibraries(libraries: Array<'recharts' | 'd3' | 'three' | 'reactflow'>) {
console.log('[PRELOAD_MGR] 📚 Preloading libraries:', libraries)
libraries.forEach(lib => {
preloadLibrary(lib)
})
}
isPreloaded(pageId: string): boolean {
return this.preloadedRoutes.has(pageId)
}