Incremental Prefetching in Next.js 16: Smarter Route Loading

6 min read1198 words

Our e-commerce category page had 48 product links. In Next.js 15, hovering triggered prefetching that downloaded 2.4MB of JavaScript and data—the full page payload times 48. Users on mobile data complained about unexpected data usage. Next.js 16's incremental prefetching changed this entirely. The same page now prefetches 180KB total. Here's how the new system works.

The Previous Prefetching Model

Next.js has always prefetched linked pages to make navigation feel instant. When a <Link> component enters the viewport or receives hover, Next.js downloads the destination page's assets.

The problem: it downloaded everything. Every product link triggered a full prefetch—JavaScript bundle, data payload, the works. Shared layouts downloaded repeatedly. A category page with 50 products meant potentially downloading the same layout 50 times.

// Each of these prefetched independently
<Link href="/products/shoe-1">Shoe 1</Link>
<Link href="/products/shoe-2">Shoe 2</Link>
<Link href="/products/shoe-3">Shoe 3</Link>
// ... 47 more links, each downloading the full /products/[slug] layout

Incremental Prefetching

Next.js 16 prefetches only what isn't already cached. The first product link prefetches:

  • The shared /products/[slug] layout (once)
  • The specific product's data
  • Any unique JavaScript for that page

Subsequent links prefetch only their unique data. The layout is already cached.

First link prefetch:
├── layout.js (shared) ✓ Download
├── page.js (shared) ✓ Download
└── product-data (unique) ✓ Download
 
Second link prefetch:
├── layout.js (shared) ✗ Cached
├── page.js (shared) ✗ Cached
└── product-data (unique) ✓ Download

No configuration needed. This happens automatically in Next.js 16.

Layout Deduplication

The layout deduplication system tracks which layouts have been prefetched. When multiple links share a layout, it downloads once and reuses:

Category page with 50 product links:
 
Next.js 15:
- Layout downloaded: 50 times
- Total transfer: 50 × 48KB = 2.4MB
 
Next.js 16:
- Layout downloaded: 1 time
- Product data: 50 × 3KB = 150KB
- Total transfer: 48KB + 150KB = 198KB
 
Reduction: 92%

The math improves with more links. A page with 100 product links sees even greater savings because the shared layout cost amortizes further.

Viewport-Aware Behavior

Next.js 16 adds intelligence about which links matter:

Viewport exit cancellation: When a link scrolls out of view, its pending prefetch request cancels. Users scrolling quickly through a long list don't trigger dozens of useless requests.

Hover prioritization: Links under the cursor get priority. If the user hovers a link that hasn't prefetched yet, it jumps the queue.

Re-entry handling: If a link re-enters the viewport after scrolling away, it re-queues for prefetching. But if the data was already cached, no new request fires.

Invalidation awareness: If cached data becomes stale (revalidation timer expires), prefetching fetches fresh data on the next trigger.

Measuring the Impact

I instrumented a category page to measure prefetch behavior:

// Monitoring prefetch requests
useEffect(() => {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.name.includes('/_next/')) {
        console.log('Prefetch:', entry.name, entry.transferSize);
      }
    }
  });
 
  observer.observe({ entryTypes: ['resource'] });
 
  return () => observer.disconnect();
}, []);

Results on our category page:

| Metric | Next.js 15 | Next.js 16 | |--------|------------|------------| | Prefetch requests | 48 | 52 | | Total transfer | 2.4MB | 198KB | | Time to prefetch all | 4.2s | 0.8s | | Navigation delay | 120ms | 40ms |

More requests, but far less data. The increased request count comes from more granular fetching. Each request is tiny compared to the full-page downloads before.

How It Works with Cache Components

Next.js 16's Cache Components feature works seamlessly with incremental prefetching. Pages using use cache get their cached portions served from the edge, with only dynamic parts fetched:

// app/products/[slug]/page.tsx
import { unstable_cache } from 'next/cache';
 
async function getProductDetails(slug: string) {
  'use cache';
  // This portion caches at the edge
  return db.products.findBySlug(slug);
}
 
async function getInventory(slug: string) {
  // This fetches fresh each time
  return inventory.check(slug);
}
 
export default async function ProductPage({ params }: { params: { slug: string } }) {
  const [product, inventory] = await Promise.all([
    getProductDetails(params.slug),
    getInventory(params.slug),
  ]);
 
  return (
    <div>
      <ProductInfo product={product} />
      <InventoryStatus inventory={inventory} />
    </div>
  );
}

When prefetching this page, Next.js fetches:

  1. The cached product details (from edge cache if available)
  2. The fresh inventory data
  3. Only the dynamic portions of the layout

The static shell renders immediately on navigation, with dynamic parts streaming in.

Prefetch Priority Hints

You can hint prefetch priority using the prefetch prop:

// High priority - prefetch immediately when in viewport
<Link href="/products/featured" prefetch={true}>
  Featured Product
</Link>
 
// Default - prefetch on viewport entry with standard priority
<Link href="/products/regular">
  Regular Product
</Link>
 
// Disabled - only prefetch on hover
<Link href="/products/rarely-visited" prefetch={false}>
  Rarely Visited
</Link>

The default behavior in Next.js 16 is smarter than before. Even with prefetch={true}, the incremental system applies—only uncached portions download.

Network Considerations

Incremental prefetching respects connection quality:

// Next.js automatically adjusts based on connection
// On slow connections or Save-Data header:
// - Prefetching becomes more conservative
// - Only high-priority links prefetch
// - Viewport triggers may be delayed

You can also detect this in your components:

function ProductGrid({ products }) {
  const [saveData, setSaveData] = useState(false);
 
  useEffect(() => {
    const connection = navigator.connection;
    if (connection?.saveData) {
      setSaveData(true);
    }
  }, []);
 
  return (
    <div className="grid grid-cols-4 gap-4">
      {products.map((product) => (
        <Link
          key={product.id}
          href={`/products/${product.slug}`}
          prefetch={saveData ? false : undefined}
        >
          <ProductCard product={product} />
        </Link>
      ))}
    </div>
  );
}

Debugging Prefetch Behavior

The Next.js DevTools (covered in my previous post) show prefetch activity:

User: Show prefetch activity for /category/shoes
 
AI: Analyzing prefetch logs for this route:
 
Prefetched links: 48
- Layout fetched: 1 time (48KB)
- Unique data fetched: 48 times (avg 3.2KB each)
- Cancelled requests: 12 (viewport exit)
- Cache hits: 36 (returning visitors)
 
Total transfer this session: 198KB
Estimated previous version: 2.3MB
 
No issues detected. Prefetching working as expected.

Migration Notes

Incremental prefetching activates automatically in Next.js 16. No code changes required. However, be aware:

Request patterns change: Monitoring tools may show more requests. This is expected—you're trading large monolithic requests for many small ones.

CDN considerations: If your CDN charges per-request, costs might increase slightly. However, bandwidth charges typically decrease significantly, resulting in net savings.

Caching headers matter more: Since prefetching is granular, ensure your API routes return appropriate cache headers. Poorly cached dynamic data triggers repeated fetches.

// Ensure proper caching headers on API routes
export async function GET(request: Request) {
  const data = await fetchData();
 
  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
    },
  });
}

Real-World Results

After upgrading our e-commerce site:

Mobile data usage dropped 85% on category pages. Users browse more products without data anxiety.

Navigation feels instant. The perceived latency dropped from 120ms to under 40ms because most data is already cached when users click.

Server load decreased paradoxically. Despite more requests, the total bandwidth served dropped. Edge caching handles most requests, reducing origin load.

The incremental prefetching model in Next.js 16 represents a fundamental improvement in how client-side navigation works. Rather than optimistically downloading everything, it downloads precisely what's needed. The result is faster navigation with less waste—exactly what users on any connection need.