194 lines
4.3 KiB
TypeScript
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))
|
|
}
|