initial commit
This commit is contained in:
193
lib/lazy-loading.tsx
Normal file
193
lib/lazy-loading.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
Reference in New Issue
Block a user