mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-28 07:34:56 +00:00
Generated by Spark: Lazy-load chart libraries (recharts, d3) to reduce bundle size further
This commit is contained in:
63
src/components/molecules/LazyBarChart.tsx
Normal file
63
src/components/molecules/LazyBarChart.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
87
src/components/molecules/LazyD3BarChart.tsx
Normal file
87
src/components/molecules/LazyD3BarChart.tsx
Normal 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} />
|
||||
)
|
||||
}
|
||||
63
src/components/molecules/LazyLineChart.tsx
Normal file
63
src/components/molecules/LazyLineChart.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
140
src/hooks/core/use-library-loader.ts
Normal file
140
src/hooks/core/use-library-loader.ts
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
111
src/lib/library-loader.ts
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user