Loading page
escF1F2F3F4F5F6F7F8F9F10F11F12
~`!1@2#3$4%5^6&7*8(9)0_-+=delete
tabQWERTYUIOP{[}]|\
caps lockASDFGHJKL:;"'return
shiftZXCVBNM<,>.?/shift
fn⌃control⌥option⌘command⌘command⌥option◀▲▼▶
“Bounce it, sleep on it, listen again tomorrow.”
How enabling prefetch=true on all links turned our Next.js 15 app into a server-crushing nightmare, with lessons learned about performance optimization and resource management
In our quest for the fastest possible user experience, we decided to enable prefetch={true} on every single Link component in our Next.js 15 application. What seemed like a performance optimization quickly turned into a server-crushing nightmare.
We had a complex application with hundreds of routes, dynamic pages, and server-side data fetching. Our naive implementation looked something like this:
// ❌ BAD: Prefetching everything
import Link from 'next/link';
// This was everywhere in our app
<Link href="/dashboard" prefetch={true}>
Dashboard
</Link>
<Link href="/tracks/[id]" prefetch={true}>
Track Details
</Link>
<Link href="/albums/[id]/edit" prefetch={true}>
Edit Album
</Link>
// Even worse: Dynamic routes with prefetch
<Link href={`/tracks/${trackId}`} prefetch={true}>
View Track
</Link>
Within hours of deployment, our server metrics went haywire. CPU usage spiked to 90%+, memory consumption doubled, and response times increased from 200ms to 2+ seconds. The server was essentially trying to pre-render every possible route combination simultaneously.
// Before: Healthy server
CPU Usage: 15-25%
Memory: 512MB
Response Time: 200ms
Concurrent Users: 50
// After: Server nightmare
CPU Usage: 85-95% ⚠️
Memory: 1.2GB ⚠️
Response Time: 2000ms+ ⚠️
Concurrent Users: 10 ⚠️
// The problem: Next.js was trying to pre-render
// - 50+ dynamic routes
// - 200+ static routes
// - All possible parameter combinations
// - Server-side data fetching for each route
Next.js 15's prefetch mechanism works by pre-rendering pages and their data on the server. When you have hundreds of links with prefetch enabled, the server attempts to render all possible route combinations, including dynamic routes with various parameters. This creates an exponential explosion of server-side rendering work.
// Our route structure
;/tracks/[id] / // 50 tracks = 50 pre-renders
albums /
[id] / // 30 albums = 30 pre-renders
albums /
[id] /
edit / // 30 albums = 30 pre-renders
users /
[id] / // 100 users = 100 pre-renders
tracks /
[id] /
comments // 50 tracks = 50 pre-renders
// Total potential pre-renders: 260+
// Each with server-side data fetching
// Each consuming memory and CPU
// All happening simultaneously
The server was spending more time pre-rendering unused pages than serving actual user requests. Database connections were exhausted, memory leaks occurred from unclosed connections, and the application became completely unresponsive during peak usage.
[ERROR] Database connection pool exhausted
[WARN] Memory usage at 95% - forcing garbage collection
[ERROR] Request timeout after 30 seconds
[WARN] 50+ concurrent prefetch requests
[ERROR] Server unresponsive - restarting...
// The server was essentially DDOSing itself
// with prefetch requests
We implemented a strategic prefetching approach that only pre-renders the most likely user paths, uses conditional prefetching based on user behavior, and implements proper resource management.
// ✅ GOOD: Conditional prefetching
import Link from 'next/link';
import { usePathname } from 'next/navigation';
function SmartLink({ href, children, ...props }) {
const pathname = usePathname();
// Only prefetch if we're on a related page
const shouldPrefetch = pathname.startsWith('/dashboard') &&
href.startsWith('/dashboard');
return (
<Link href={href} prefetch={shouldPrefetch} {...props}>
{children}
</Link>
);
}
// ✅ GOOD: Hover-based prefetching
function HoverPrefetchLink({ href, children }) {
const [shouldPrefetch, setShouldPrefetch] = useState(false);
return (
<Link
href={href}
prefetch={shouldPrefetch}
onMouseEnter={() => setShouldPrefetch(true)}
>
{children}
</Link>
);
}
// ✅ GOOD: Priority-based prefetching
const HIGH_PRIORITY_ROUTES = ['/dashboard', '/profile', '/settings'];
function PriorityLink({ href, children }) {
const isHighPriority = HIGH_PRIORITY_ROUTES.some(route =>
href.startsWith(route)
);
return (
<Link href={href} prefetch={isHighPriority}>
{children}
</Link>
);
}
We implemented monitoring to track prefetch performance and set hard limits on concurrent prefetch operations. This prevents the server from being overwhelmed while still providing the performance benefits where they matter most.
// Prefetch monitoring
const prefetchMetrics = {
activePrefetches: 0,
maxConcurrentPrefetches: 10,
prefetchSuccessRate: 0,
averagePrefetchTime: 0,
}
// Rate limiting prefetch requests
function rateLimitedPrefetch(href) {
if (
prefetchMetrics.activePrefetches >=
prefetchMetrics.maxConcurrentPrefetches
) {
console.warn('Prefetch limit reached, skipping:', href)
return
}
prefetchMetrics.activePrefetches++
// ... prefetch logic
}
// Server-side prefetch limits
export const runtime = 'nodejs'
export const maxDuration = 30 // 30 seconds max for prefetch
// Memory monitoring
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
// 500MB
console.warn('High memory usage, disabling prefetch')
// Disable prefetch temporarily
}
The key lesson is that prefetching should be strategic, not universal. Consider user behavior patterns, prioritize critical paths, and always monitor server resources. What works for small applications can be catastrophic at scale.
✅ DO:
- Prefetch only high-priority routes
- Use conditional prefetching based on context
- Implement hover-based prefetching
- Monitor server resources
- Set hard limits on concurrent prefetches
- Use prefetch={false} for dynamic routes
❌ DON'T:
- Enable prefetch on all links
- Prefetch dynamic routes with many parameters
- Ignore server resource monitoring
- Prefetch rarely-visited pages
- Prefetch without user interaction signals
// The golden rule: Prefetch should be
// strategic, not universal
After implementing smart prefetching, our server performance returned to normal levels. CPU usage dropped back to 15-25%, memory consumption stabilized, and response times improved to 150ms average. The application now scales properly while still providing fast navigation for critical user paths.
Next.js 15's prefetching is worth having, and like any optimization it has trade-offs worth reading before you turn it on everywhere. The lesson here is that performance optimizations should always be measured, monitored, and applied strategically rather than universally. Sometimes the best optimization is knowing when not to optimize.