Mobile CDN Prefetch Strategies: Predicting and Preloading Content Before Users Need It

Last year I was debugging a travel app that kept getting one-star reviews saying "so slow" even though their edge cache hit ratio was 94%. The problem wasn't the cache — it was timing. Users were opening a hotel details page, waiting 800ms for images and reviews to load, then bouncing. We turned on predictive prefetch for the next two likely hotels based on scroll behavior, and that 800ms dropped to under 40ms. The complaints vanished overnight. That's the whole promise of mobile prefetch done right — you aren't just caching faster, you're eliminating the wait entirely.

Why Mobile Prefetch Isn't Just Desktop Prefetch on a Smaller Screen

I've seen teams copy their desktop rel=prefetch setup straight into their mobile web view and wonder why their data bills explode and nothing feels faster. Desktop prefetch assumes cheap bandwidth, stable Wi-Fi, and plenty of battery. Mobile throws all of that out the window. You've got users flipping between 5G, congested LTE, and spotty Wi-Fi, often with Data Saver turned on, and the OS will happily kill your background fetches if you're too aggressive. If your anticipatory caching strategy doesn't account for connection type and device state, you're preloading content users will never see while burning through their data plan.

The other trap is treating every prefetch the same. On desktop, you might prefetch five article pages ahead without thinking twice. On mobile, each prefetch is a trade-off. I worked with an e-commerce client in 2021 that prefetched the entire product carousel — 12 high-res images — for every category view. Their CDN logs looked great, prefetch hit rate was 70%, but their Android users on mid-range devices saw jank and battery drain because we'd saturated the network queue and evicted more useful entries from the memory cache layer. We cut it to prefetching only the next product detail JSON and a single 400px thumbnail, lazy-loading the rest. Hit rate stayed at 68% but wasted bytes dropped by 83%.

What actually works is hinting at the HTTP layer and letting the client decide. For mobile web and hybrid apps, Early Hints (103) and Link headers are far more respectful than blindly injecting prefetch tags with JavaScript. They let the browser or your Mobile CDN SDK Integration Guide: Adding Edge Acceleration to iOS and Android Apps respect priority signals, connection hints, and Save-Data preferences. The HTTP Working Group Specifications outline how clients should handle these hints, and the web.dev Articles on priority hints explain why prefetch should almost always be priority=low on mobile.

// Sending prefetch hints from the edge, not the client
// CDN edge responds with 103 Early Hints for the most likely next step
HTTP/1.1 103 Early Hints
Link: </api/products/p_88291.json>; rel=prefetch; as=fetch; priority=low
Link: </images/p_88291_thumb_400.webp>; rel=prefetch; as=image; priority=low
Cache-Control: public, max-age=60

HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, max-age=300
Link: </api/products/p_88291.json>; rel=prefetch; as=fetch
Link: </images/p_88291_thumb_400.webp>; rel=prefetch; as=image
// Client SDK will ignore these if Save-Data: on or effectiveType = '2g' / 'slow-2g'

Content Prediction Models That Don't Require a Data Science Team

You don't need a massive machine learning pipeline to make content prediction useful. In my experience, the fanciest models often lose to simple, explainable heuristics that you can run on-device without a round trip. For most apps, 80% of the value comes from three signals: sequential navigation patterns, user-specific recency, and viewport behavior. If a user always goes from Search -> Product -> Reviews -> Add to Cart, you can predict with decent confidence that after Product, Reviews is next. That's a first-order Markov model you can build from your CDN access logs in an afternoon, no TensorFlow required.

Where I've seen teams get preloading right is by keeping the model on the edge or on the device, and keeping it fresh. Sending every tap back to a central server for prediction adds latency that defeats the whole purpose. Instead, we typically generate a lightweight prediction manifest at the edge — essentially a ranked list of URLs the user is likely to request next — and send it as a JSON payload with the current response or as a Service Worker update. For native apps, your CDN SDK can maintain a tiny on-device LRU of transition probabilities. It learns that User A binge-reads sports articles sequentially, while User B jumps between headlines, and adjusts how aggressively it prefetches. You'll want to cap your predictions to the top 1-2 candidates; after that, accuracy falls off a cliff and you're just wasting bytes.

Don't forget viewport and interaction signals — they're gold on mobile. If someone's scrolled 70% down a feed and paused, they're probably going to tap something in the lower half of the screen. Prefetching the detail data for the two items currently in view is far more accurate than prefetching the next page of results blindly. We added intersection-observer-based prefetch to a media app's feed and saw our mobile prefetch hit rate jump from 41% to 62% without increasing total prefetched bytes, simply because we were predicting based on what was actually visible, not what was next in the API pagination.

// Simple on-device content prediction for mobile prefetch
// No ML library needed - just transition counts from recent sessions
class PrefetchPredictor {
  constructor(limit = 2) {
    this.limit = limit;
    this.transitions = new Map(); // "screen:A -> screen:B" : count
  }

  recordTransition(from, to) {
    const key = `${from}->${to}`;
    this.transitions.set(key, (this.transitions.get(key) || 0) + 1);
  }

  predictNext(currentScreen) {
    const candidates = [];
    for (const [key, count] of this.transitions) {
      if (key.startsWith(currentScreen + "->")) {
        candidates.push({ target: key.split("->")[1], score: count });
      }
    }
    return candidates
      .sort((a, b) => b.score - a.score)
      .slice(0, this.limit)
      .map(c => `/api/screens/${c.target}.json`);
  }

  // Only prefetch if conditions are good
  shouldPrefetch(connection) {
    if (navigator.connection?.saveData) return false;
    if (['slow-2g', '2g'].includes(connection.effectiveType)) return false;
    if (connection.downlink < 0.8) return false;
    return true;
  }
}

How Aggressive Should Your Preloading Actually Be?

There's no single right answer for how much to prefetch — it depends entirely on your content type and your users' tolerance for data use. A news app where articles are 30KB of JSON can afford to be far more aggressive than a video commerce app where each product page pulls 2MB of images. I've learned to frame it as a budget: you get a prefetch budget per navigation, measured in bytes and requests, and your prediction confidence has to justify spending it. If your model is only 35% sure the user wants Page 2, you shouldn't spend 500KB on it. If it's 85% sure, go ahead.

This is also where you need to think about Offline-First Architecture with CDN: Building Apps That Work Without Connectivity principles. Prefetching isn't just about speed, it's about resilience. On mobile, a prefetch that lands in the disk cache while the user still has connectivity can be the difference between a usable app on the subway and a blank screen. But you have to set proper cache lifetimes — prefetched responses should use stale-while-revalidate and shorter max-age values than user-requested content, so you don't serve stale anticipatory data when the user actually navigates. I usually set prefetched API responses to 60-120 seconds while demand-fetched ones get 300 seconds.

Prefetch Strategy Hit Rate Observed Wasted Bytes / Session P95 Latency Saved Battery Impact
Next page only (1 JSON + 1 thumb) 61-66% ~45 KB 280-340 ms Negligible
Top 2 predicted screens (ML-lite) 58-63% ~85 KB 410-480 ms Low
Viewport-aware (2 visible items) 62-68% ~70 KB 390-520 ms Low
Aggressive (next 3 pages, all images) 64-71% ~420 KB 500-610 ms High - increased radio wake-ups
No prefetch (baseline) 0% 0 KB 0 ms Baseline

From Theory to Edge Config: Implementing Prefetch Without Blowing Up Your Origin

Alright, you've got a prediction model — even a simple one like "users who land on /product/* have a 68% chance of hitting /cart in the next 90 seconds." Now you've got to actually wire it up without turning the CDN into a DDoS cannon pointed at your own origin. The cleanest starting point is declarative hints from the edge. We like to have the edge inject Link: </api/product/related.json>; rel=prefetch; as=fetch or Speculation Rules via a response header based on the request pattern, not hard-coded in your app HTML. That way you can turn it on/off with a feature flag at the edge and A/B test it in minutes, not sprints.

On the client, you don't want blind prefetching. Tie it to two things: viewport intent and network quality. Use an IntersectionObserver to prefetch when a link is within 300-400px of the viewport, and gate it with navigator.connection. If you're on 4G with an effective RTT under 150ms and saveData is false, go ahead and prefetch 2-3 critical resources. If you're on 3G, or effectiveType drops to '2g', or the user is on a metered connection, don't. We've seen this simple gating cut wasted prefetch bytes by 40-55% while keeping your hit rate above 30%. For a deeper dive on gating, check how Mobile Connection Detection and Switching: Adapting CDN Behavior to Network Type handles real-time network classification, and pair it with Adaptive Quality Delivery for Mobile: Network-Aware Content Optimization so you're not prefetching a 4K hero image for someone on a flaky 3G link.

Here's the lightweight client pattern we deploy in production apps — it respects data saver, caps concurrency, and adds edge-cache awareness via a custom header:

// mobile-aware prefetch with network gating and cache check
const PREFETCH_LIMIT = 3;
const seen = new Set();

async function maybePrefetch(url) {
  if (seen.has(url) || seen.size >= PREFETCH_LIMIT) return;
  const conn = navigator.connection;
  if (conn && (conn.saveData || /2g/.test(conn.effectiveType))) return;
  
  // Ask edge if it's already hot in cache (X-Cache-Status: HIT)
  const head = await fetch(url, { method: 'HEAD', credentials: 'omit' });
  if (head.headers.get('X-Cache-Status') !== 'HIT') return;

  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.as = 'fetch';
  link.href = url;
  link.crossOrigin = 'anonymous';
  document.head.appendChild(link);
  seen.add(url);
}

// Trigger on near-viewport intent
const observer = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) maybePrefetch(e.target.href);
  });
}, { rootMargin: '400px' });

document.querySelectorAll('a[data-prefetch]').forEach(a => observer.observe(a));

Advanced Patterns and When Prefetch Starts Hurting You

Once you're past the basics, the real bugs show up in your RUM data, not your logs. The classic one is cache bloat and origin churn. If your prefetch accuracy drops below about 25%, you're evicting useful content from the edge LRU to make room for guesses nobody uses. We track this with a simple metric: prefetch_efficiency = prefetched_requests_used / prefetched_requests_total measured over a 5-minute window per PoP. If that dips under 0.28 for more than 15 minutes, our edge config automatically throttles prefetch injection from 3 down to 1 resource per navigation. You should also set a separate, shorter TTL for prefetched objects — say 60-120 seconds vs. your normal 10-minute edge TTL — so bad guesses expire fast.

Another gotcha that's uniquely mobile: TLS and battery cost. Keeping the radio awake for speculative fetches isn't free. On Android, every prefetch that opens a new TLS 1.3 connection can keep the radio in high-power state for an extra 20-30 seconds. If you're prefetching across multiple domains without connection coalescing, you're draining battery and you might be breaking your pinning strategy. Reuse the existing connection, stick to same-origin or properly coalesced HTTP/2 origins, and make sure your prefetch fetches respect your pinning configuration. If you haven't locked that down, read Mobile CDN Certificate Pinning: Securing App-to-Edge Connections Against Interception before you add speculative cross-origin requests.

Finally, don't try to debug this with synthetic tests. Use real user monitoring segmented by network type, device memory, and geography. Look for the telltale sign of over-prefetching: Time to First Byte looks great in the lab, but your 75th percentile data transfer and 95th percentile origin load spike by 18-25% in production. The fix isn't to kill prefetch — it's to get surgical. Prefetch the JSON, not the images. Prefetch the next API call's response, let the image lazy-load normally. In our last rollout for a media app, prefetching just the 3.2KB JSON payload for the next article delivered 82% of the perceived speed gain while using 11% of the bytes of full-page prefetching.

Frequently Asked Questions

Should I use preload or prefetch for mobile?

They're not interchangeable. Use preload for resources you *know* the current page will need — critical CSS, the hero image, the web font. It's high-priority and blocks the window's load event. Use prefetch for resources you *think* the next navigation will need. It's low-priority, handled during idle time, and won't compete with current page rendering. If you're not at least 70% confident the user will need it next, it's a prefetch, not a preload.

Won't prefetch waste data on metered or limited plans?

It will if you let it. Always check navigator.connection.saveData and effectiveType before firing. A good rule of thumb: never prefetch on 2G, never prefetch if saveData is true, and limit to 1 small resource (<50KB) on 3G. On 4G/5G and Wi-Fi, you can be more aggressive with 2-3 resources. Also, respect the HTTP Save-Data header at the CDN edge to disable speculative headers server-side.

How do I actually measure if prefetch is working?

Track three metrics together: 1) Prefetch Hit Rate — what % of prefetched resources are actually used within 2 minutes, 2) Navigation Speedup — the delta in next-page LCP or Time-to-Interactive for hits vs. misses, and 3) Byte Waste — total prefetched bytes not used per session. You're winning if your hit rate is 25-40%, LCP on prefetched navigations is 250-600ms faster, and waste stays under 80KB per session on mobile.

Does prefetching hurt the CDN cache hit ratio?

Not if you do it right, but it can. Unbounded prefetching fills the edge with low-value, single-use objects and pushes out your real hot cache. Mitigate it by using a separate, smaller TTL for speculative content, using Vary: Save-Data, Effective-Network-Type properly, and enabling Tiered Caching so a miss at the edge can still be a hit at the shield PoP instead of going all the way to origin.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker