PatternsCode Splitting Strategies
Preload Strategies
Intelligent preloading for instant perceived performance
Preload Strategies
Don't wait for the user to click. Preload intelligently based on behavior, analytics, and heuristics for instant navigation.
🎯 The Goal
Without Preloading:
User clicks link → Start download → Wait 2s → Show page
Total: 2000ms ❌
With Preloading:
User hovers (500ms) → Preload in background
User clicks → Already loaded → Show page instantly
Total: 0ms ✅📊 Preload Types
| Strategy | When | Use Case | Hit Rate |
|---|---|---|---|
| Hover | Mouse enters link | Desktop | 80-90% |
| Viewport | Link visible | All devices | 60-70% |
| Intent | Hover + delay | Desktop | 90-95% |
| Predictive | Analytics | All | 70-80% |
| Idle | Browser idle | All | 100% (eventual) |
| Sequential | Current → Next | Onboarding | 100% |
🎯 Pattern 1: Hover Preload
Basic Hover
import { useRouter } from 'next/router';
function Navigation() {
const router = useRouter();
const prefetch = (href: string) => {
router.prefetch(href);
};
return (
<nav>
<a
href="/dashboard"
onMouseEnter={() => prefetch('/dashboard')}
>
Dashboard
</a>
<a
href="/settings"
onMouseEnter={() => prefetch('/settings')}
>
Settings
</a>
</nav>
);
}With Delay (Intent-Based)
function PrefetchLink({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter();
const timerRef = useRef<NodeJS.Timeout>();
const handleMouseEnter = () => {
// Wait 100ms to confirm intent
timerRef.current = setTimeout(() => {
router.prefetch(href);
}, 100);
};
const handleMouseLeave = () => {
// User moved away, cancel prefetch
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
return (
<a
href={href}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</a>
);
}👁️ Pattern 2: Viewport Preload
Intersection Observer
import { useInView } from 'react-intersection-observer';
function PrefetchLink({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter();
const { ref, inView } = useInView({
triggerOnce: true,
rootMargin: '100px' // Prefetch 100px before visible
});
useEffect(() => {
if (inView) {
router.prefetch(href);
}
}, [inView, href]);
return (
<a ref={ref} href={href}>
{children}
</a>
);
}Bulk Viewport Prefetch
function ProductGrid({ products }: { products: Product[] }) {
const { ref, inView } = useInView({ triggerOnce: true });
useEffect(() => {
if (inView) {
// Prefetch first 6 products
products.slice(0, 6).forEach(product => {
import(`./products/${product.id}`);
});
}
}, [inView, products]);
return (
<div ref={ref} className="grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}🔮 Pattern 3: Predictive Preload
Analytics-Based
// Track user navigation patterns
const navigationPatterns = {
'/': ['/products', '/about'], // 80% go to products, 15% to about
'/products': ['/products/:id', '/cart'], // 90% to product, 5% to cart
'/cart': ['/checkout'], // 95% to checkout
};
function usePredictivePrefetch() {
const router = useRouter();
const currentPath = router.pathname;
useEffect(() => {
const likelyNextPages = navigationPatterns[currentPath] || [];
// Prefetch likely next pages
likelyNextPages.forEach(page => {
router.prefetch(page);
});
}, [currentPath]);
}
// Usage
function App() {
usePredictivePrefetch();
return <Routes>...</Routes>;
}Machine Learning Based
interface PredictionModel {
predict(currentPage: string, userFeatures: UserFeatures): string[];
}
function useMLPrefetch(model: PredictionModel) {
const router = useRouter();
const { user } = useAuth();
useEffect(() => {
const predictedPages = model.predict(router.pathname, {
userId: user.id,
previousPages: router.history,
timeOnPage: Date.now() - router.enteredAt,
scrollDepth: window.scrollY / document.body.scrollHeight
});
// Prefetch top 3 predictions
predictedPages.slice(0, 3).forEach(page => {
router.prefetch(page);
});
}, [router.pathname]);
}🎨 Pattern 4: Idle Preload
requestIdleCallback
function useIdlePrefetch(routes: string[]) {
useEffect(() => {
if ('requestIdleCallback' in window) {
const id = requestIdleCallback(
() => {
// Prefetch during idle time
routes.forEach(route => {
import(`./pages/${route}`);
});
},
{ timeout: 2000 }
);
return () => cancelIdleCallback(id);
} else {
// Fallback for browsers without requestIdleCallback
const timer = setTimeout(() => {
routes.forEach(route => {
import(`./pages/${route}`);
});
}, 1000);
return () => clearTimeout(timer);
}
}, [routes]);
}
// Usage
function App() {
useIdlePrefetch(['/dashboard', '/settings', '/profile']);
return <Routes>...</Routes>;
}🔄 Pattern 5: Sequential Preload
Onboarding Flow
function OnboardingStep1() {
useEffect(() => {
// Preload next step immediately
import('./OnboardingStep2');
}, []);
return (
<div>
<h1>Step 1</h1>
<button onClick={() => router.push('/onboarding/step2')}>
Next
</button>
</div>
);
}
function OnboardingStep2() {
useEffect(() => {
// Preload next step
import('./OnboardingStep3');
}, []);
return (
<div>
<h1>Step 2</h1>
<button onClick={() => router.push('/onboarding/step3')}>
Next
</button>
</div>
);
}Wizard Pattern
const wizardSteps = [
() => import('./Step1'),
() => import('./Step2'),
() => import('./Step3'),
() => import('./Step4')
];
function Wizard() {
const [currentStep, setCurrentStep] = useState(0);
useEffect(() => {
// Preload next step
if (currentStep < wizardSteps.length - 1) {
wizardSteps[currentStep + 1]();
}
}, [currentStep]);
const Step = lazy(wizardSteps[currentStep]);
return (
<Suspense fallback={<Spinner />}>
<Step onNext={() => setCurrentStep(s => s + 1)} />
</Suspense>
);
}🎯 Pattern 6: Priority-Based Preload
interface PreloadTask {
route: string;
priority: number;
condition?: () => boolean;
}
class PreloadScheduler {
private queue: PreloadTask[] = [];
private loading = new Set<string>();
add(task: PreloadTask) {
this.queue.push(task);
this.queue.sort((a, b) => b.priority - a.priority);
this.process();
}
private async process() {
while (this.queue.length > 0) {
const task = this.queue.shift()!;
// Check condition
if (task.condition && !task.condition()) {
continue;
}
// Skip if already loading
if (this.loading.has(task.route)) {
continue;
}
this.loading.add(task.route);
try {
await import(`./pages/${task.route}`);
console.log(`Preloaded: ${task.route}`);
} catch (error) {
console.error(`Failed to preload: ${task.route}`, error);
} finally {
this.loading.delete(task.route);
}
}
}
}
// Usage
const scheduler = new PreloadScheduler();
// High priority (user hovering)
scheduler.add({ route: '/dashboard', priority: 100 });
// Medium priority (visible in viewport)
scheduler.add({ route: '/settings', priority: 50 });
// Low priority (idle time)
scheduler.add({ route: '/profile', priority: 10 });
// Conditional
scheduler.add({
route: '/admin',
priority: 80,
condition: () => user.role === 'admin'
});📊 Measuring Preload Effectiveness
interface PreloadMetrics {
prefetched: number;
hits: number;
misses: number;
hitRate: number;
}
class PreloadTracker {
private metrics: PreloadMetrics = {
prefetched: 0,
hits: 0,
misses: 0,
hitRate: 0
};
private prefetchedRoutes = new Set<string>();
onPrefetch(route: string) {
this.prefetchedRoutes.add(route);
this.metrics.prefetched++;
}
onNavigate(route: string) {
if (this.prefetchedRoutes.has(route)) {
this.metrics.hits++;
console.log('✅ Prefetch hit:', route);
} else {
this.metrics.misses++;
console.log('❌ Prefetch miss:', route);
}
this.metrics.hitRate = this.metrics.hits / (this.metrics.hits + this.metrics.misses);
// Send to analytics
analytics.track('prefetch_metrics', this.metrics);
}
getMetrics() {
return this.metrics;
}
}
const tracker = new PreloadTracker();
// Track prefetches
router.prefetch('/dashboard');
tracker.onPrefetch('/dashboard');
// Track navigation
router.push('/dashboard');
tracker.onNavigate('/dashboard');
// Check metrics
console.log('Hit rate:', tracker.getMetrics().hitRate); // 0.85 (85%)🎨 Resource Hints
Preload Critical Resources
<head>
<!-- Preload critical JS -->
<link rel="preload" href="/main.js" as="script">
<!-- Preload critical CSS -->
<link rel="preload" href="/critical.css" as="style">
<!-- Preload fonts -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
</head>Prefetch Next Pages
<!-- Prefetch likely next pages (low priority) -->
<link rel="prefetch" href="/dashboard.js" as="script">
<link rel="prefetch" href="/settings.js" as="script">DNS Prefetch
<!-- Resolve DNS early for external resources -->
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">Preconnect
<!-- Establish connection early -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com">🚀 Quicklink Library
npm install quicklinkimport { prefetch } from 'quicklink';
// Automatically prefetch all visible links
useEffect(() => {
prefetch();
}, []);
// Or specific links
useEffect(() => {
prefetch(['/dashboard', '/settings']);
}, []);📚 Best Practices
1. Start with Hover
// Highest ROI, easiest to implement
<a onMouseEnter={() => prefetch('/page')}>Link</a>2. Respect Data Saver
if (!navigator.connection?.saveData) {
// Only prefetch if user hasn't enabled data saver
prefetch('/dashboard');
}3. Monitor Hit Rate
// Aim for > 80% hit rate
if (hitRate < 0.8) {
console.warn('Prefetch strategy needs improvement');
}4. Limit Concurrent Prefetches
// Max 3 concurrent prefetches
const maxConcurrent = 3;
let loading = 0;
async function prefetch(route: string) {
if (loading >= maxConcurrent) {
await waitForSlot();
}
loading++;
await import(route);
loading--;
}🏢 Real-World Examples
Google Search
- Hover prefetch (search results)
- Predictive prefetch (autocomplete)
- Instant prefetch (I'm Feeling Lucky)
Result: < 100ms navigationGitHub
- Hover prefetch (repository links)
- Viewport prefetch (trending repos)
- Idle prefetch (popular pages)
Result: Instant navigation feel📚 Key Takeaways
- Hover prefetch - 80-90% hit rate on desktop
- Viewport prefetch - Works on mobile too
- Predictive - Use analytics for common paths
- Idle time - Prefetch popular pages
- Respect data saver - Check
navigator.connection - Monitor hit rate - Aim for > 80%
- Test on 3G - Ensure prefetch doesn't hurt
Preloading typically makes navigation feel instant (< 100ms perceived load time). Start with hover! 🚀