ai-wpa/lib/lazy-loading.tsx

194 lines
4.3 KiB
TypeScript

/**
* Lazy loading utilities for performance optimization
*/
import { lazy, ComponentType } from 'react'
/**
* Create a lazy-loaded component with better error handling
*/
export function createLazyComponent<T extends ComponentType<any>>(
importFunction: () => Promise<{ default: T }>,
displayName?: string
): T {
const LazyComponent = lazy(importFunction)
if (displayName) {
;(LazyComponent as any).displayName = `Lazy(${displayName})`
}
return LazyComponent as unknown as T
}
/**
* Intersection Observer hook for lazy loading elements
*/
import { useEffect, useRef, useState } from 'react'
interface UseIntersectionObserverOptions {
threshold?: number
root?: Element | null
rootMargin?: string
freezeOnceVisible?: boolean
}
export function useIntersectionObserver(options: UseIntersectionObserverOptions = {}) {
const { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false } = options
const [entry, setEntry] = useState<IntersectionObserverEntry>()
const [node, setNode] = useState<Element | null>(null)
const observer = useRef<IntersectionObserver | null>(null)
const frozen = entry?.isIntersecting && freezeOnceVisible
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
setEntry(entry)
}
useEffect(() => {
const hasIOSupport = !!window.IntersectionObserver
if (!hasIOSupport || frozen || !node) return
const observerParams = { threshold, root, rootMargin }
const isNewObserver = !observer.current
const hasOptionsChanged =
observer.current &&
(observer.current.root !== root ||
observer.current.rootMargin !== rootMargin ||
observer.current.thresholds[0] !== threshold)
if (isNewObserver || hasOptionsChanged) {
if (observer.current) {
observer.current.disconnect()
}
observer.current = new window.IntersectionObserver(updateEntry, observerParams)
}
observer.current.observe(node)
return () => observer.current?.disconnect()
}, [node, threshold, root, rootMargin, frozen])
const cleanup = () => {
if (observer.current) {
observer.current.disconnect()
observer.current = null
}
}
useEffect(() => {
return cleanup
}, [])
return [setNode, !!entry?.isIntersecting, entry, cleanup] as const
}
/**
* Lazy loading image component
*/
import Image from 'next/image'
import { cn } from './utils'
interface LazyImageProps {
src: string
alt: string
width?: number
height?: number
className?: string
fill?: boolean
placeholder?: 'blur' | 'empty'
blurDataURL?: string
priority?: boolean
sizes?: string
}
export function LazyImage({
src,
alt,
width,
height,
className,
fill = false,
placeholder = 'empty',
blurDataURL,
priority = false,
sizes,
...props
}: LazyImageProps) {
const [setRef, isVisible] = useIntersectionObserver({
threshold: 0.1,
freezeOnceVisible: true,
})
return (
<div ref={setRef} className={cn('overflow-hidden', className)}>
{(isVisible || priority) && (
<Image
src={src}
alt={alt}
width={width}
height={height}
fill={fill}
placeholder={placeholder}
blurDataURL={blurDataURL}
priority={priority}
sizes={sizes}
className="transition-opacity duration-300"
{...props}
/>
)}
</div>
)
}
/**
* Lazy loading wrapper for any content
*/
interface LazyContentProps {
children: React.ReactNode
className?: string
fallback?: React.ReactNode
threshold?: number
rootMargin?: string
}
export function LazyContent({
children,
className,
fallback = null,
threshold = 0.1,
rootMargin = '50px',
}: LazyContentProps) {
const [setRef, isVisible] = useIntersectionObserver({
threshold,
rootMargin,
freezeOnceVisible: true,
})
return (
<div ref={setRef} className={className}>
{isVisible ? children : fallback}
</div>
)
}
/**
* Preload images for better performance
*/
export function preloadImage(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const img = new window.Image()
img.onload = () => resolve()
img.onerror = reject
img.src = src
})
}
/**
* Preload multiple images
*/
export async function preloadImages(sources: string[]): Promise<void[]> {
return Promise.all(sources.map(preloadImage))
}