@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
JavaScript
/**
* @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
});
}
});
},
};
}