PatternsCode Splitting Strategies
Preload Strategies
Intelligent preloading for instant perceived performance
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! 🚀