UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

279 lines (278 loc) 9.66 kB
/** * @fileoverview Lazy Loading Utilities * * This module provides utilities for implementing lazy loading patterns, * including React.lazy() wrappers, dynamic imports, and intersection observer-based loading. */ import { lazy, useState, useCallback, useEffect, useRef } from 'react'; // ============================================================================ // CORE LAZY LOADING UTILITIES // ============================================================================ /** * Enhanced React.lazy wrapper with error handling and retry logic */ export function createLazyComponent(loader, options = {}) { const { retryAttempts = 3, retryDelay = 1000, enableInDev = true } = options; // Disable lazy loading in development if specified if (process.env.NODE_ENV === 'development' && !enableInDev) { const component = lazy(loader); component.preload = () => loader(); return component; } let retryCount = 0; let loadPromise = null; const enhancedLoader = async () => { if (loadPromise) { return loadPromise; } loadPromise = (async () => { try { const result = await loader(); // Handle both default exports and direct component exports if (typeof result === 'function') { return { default: result }; } if (result && typeof result === 'object' && 'default' in result) { return result; } throw new Error('Invalid component export'); } catch (error) { loadPromise = null; // Reset for retry if (retryCount < retryAttempts) { retryCount++; await new Promise(resolve => setTimeout(resolve, retryDelay)); return enhancedLoader(); } throw error; } })(); return loadPromise; }; const LazyComponent = lazy(enhancedLoader); // Add preloading capabilities LazyComponent.preload = () => enhancedLoader(); return LazyComponent; } /** * Create lazy component with chunk information */ export function createLazyComponentWithChunk(loader, chunkInfo, options = {}) { const LazyComponent = createLazyComponent(loader, options); return Object.assign(LazyComponent, { chunkInfo, }); } /** * Dynamic import hook for runtime component loading */ export function useDynamicImport(loader, trigger = true) { const [state, setState] = useState({ component: null, loading: false, error: null, }); const load = useCallback(async () => { if (state.component || state.loading) return; setState(prev => ({ ...prev, loading: true, error: null })); try { const result = await loader(); const component = typeof result === 'function' ? result : result.default; setState({ component, loading: false, error: null }); } catch (error) { setState(prev => ({ ...prev, loading: false, error: error instanceof Error ? error : new Error(String(error)), })); } }, [loader, state.component, state.loading]); const retry = useCallback(() => { setState({ component: null, loading: false, error: null }); load(); }, [load]); useEffect(() => { if (trigger) { load(); } }, [trigger, load]); return { ...state, load, retry, }; } // ============================================================================ // INTERSECTION OBSERVER LAZY LOADING // ============================================================================ /** * Intersection observer hook for lazy loading */ export function useIntersectionObserver(options = {}) { const { rootMargin = '50px', threshold = 0.1, root = null, triggerOnce = true } = options; const [isIntersecting, setIsIntersecting] = useState(false); const elementRef = useRef(null); useEffect(() => { const element = elementRef.current; if (!element) return; const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { setIsIntersecting(true); if (triggerOnce) { observer.unobserve(element); } } else if (!triggerOnce) { setIsIntersecting(false); } }, { rootMargin, threshold, root, }); observer.observe(element); return () => { observer.unobserve(element); }; }, [rootMargin, threshold, root, triggerOnce]); return [elementRef, isIntersecting]; } // ============================================================================ // BUNDLE OPTIMIZATION UTILITIES // ============================================================================ /** * Preload multiple components */ export async function preloadComponents(components) { const preloadPromises = components .filter(component => typeof component.preload === 'function') .map(component => component.preload()); await Promise.allSettled(preloadPromises); } /** * Preload components by priority */ export async function preloadByPriority(components) { // Sort by priority const sortedComponents = components.sort((a, b) => { const priorityOrder = { high: 0, medium: 1, low: 2 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); // Preload high priority immediately const highPriority = sortedComponents.filter(c => c.priority === 'high'); await preloadComponents(highPriority.map(c => c.component)); // Preload medium priority after short delay setTimeout(async () => { const mediumPriority = sortedComponents.filter(c => c.priority === 'medium'); await preloadComponents(mediumPriority.map(c => c.component)); }, 100); // Preload low priority after longer delay setTimeout(async () => { const lowPriority = sortedComponents.filter(c => c.priority === 'low'); await preloadComponents(lowPriority.map(c => c.component)); }, 1000); } /** * Route-based preloading */ export function preloadOnRouteChange(routeComponentMap) { // This would be integrated with Next.js router if (typeof window !== 'undefined') { // Listen for route changes and preload accordingly const handleRouteChange = (url) => { const component = routeComponentMap[url]; if (component && component.preload) { component.preload(); } }; // Integration point for Next.js router // router.events.on('routeChangeStart', handleRouteChange); } } /** * Hover-based preloading */ export function useHoverPreload(loader, delay = 300) { const elementRef = useRef(null); const timeoutRef = useRef(); const preload = useCallback(() => { loader().catch(() => { // Ignore preload errors }); }, [loader]); useEffect(() => { const element = elementRef.current; if (!element) return; const handleMouseEnter = () => { timeoutRef.current = setTimeout(preload, delay); }; const handleMouseLeave = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; element.addEventListener('mouseenter', handleMouseEnter); element.addEventListener('mouseleave', handleMouseLeave); return () => { element.removeEventListener('mouseenter', handleMouseEnter); element.removeEventListener('mouseleave', handleMouseLeave); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [preload, delay]); return [elementRef, preload]; } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * Check if lazy loading is supported */ export function isLazyLoadingSupported() { return typeof window !== 'undefined' && 'IntersectionObserver' in window && 'Promise' in window; } /** * Get component bundle size estimate */ export function estimateComponentSize(component) { // This is a rough estimate based on string length const componentString = component.toString(); return componentString.length * 2; // Rough bytes estimate } /** * Create chunk naming strategy */ export function createChunkName(componentName, category) { const sanitized = componentName.replace(/[^a-zA-Z0-9]/g, ''); return category ? `${category}-${sanitized}` : sanitized; } /** * Create preloader for Next.js pages */ export function createPagePreloader(pageMap) { return { preloadPage: (route) => { const loader = pageMap[route]; if (loader) { loader().catch(() => { // Ignore preload errors }); } }, preloadPages: (routes) => { routes.forEach(route => { const loader = pageMap[route]; if (loader) { loader().catch(() => { // Ignore preload errors }); } }); }, }; }