DEV Community

Zenith Walls
Zenith Walls

Posted on

How We Optimized Next.js 15 for 70,000+ High-Res 4K Wallpapers (Without Melting the Edge)

When we set out to build Zenith Walls, we made a promise: we were going to serve pristine, uncompromised 4K and 8K wallpapers — crisp enough for ultra-wide OLED monitors — while keeping the browsing experience buttery smooth on mobile and desktop alike.

Then reality hit.

We were dealing with a catalog of over 70,000 images sourced across multiple external CDNs and our own Cloudflare R2 object storage. If you've ever thrown hundreds of uncompressed 10MB–25MB images into a Next.js grid with infinite scroll, you know exactly what happens:

  1. Browser tabs lock up trying to decode dozens of 4K bitmaps in parallel.
  2. next/image optimizer falls apart when upstream CDNs reject requests missing Referer headers.
  3. Edge function CPU limits (10ms on Cloudflare Workers) choke if you try to dynamically resize raw 8K bitmaps server-side with Sharp/WASM on every request.
  4. Layout shifts (CLS) and awful Largest Contentful Paint (LCP) destroy your Core Web Vitals.

Here is the exact blueprint of how we re-architected our image pipeline in Next.js 15 App Router, slashed our LCP, and eliminated blank-loading flashes without ballooning infrastructure bills.


1. The Trap: Why Default <Image /> Wasn't Enough

Next.js has one of the best built-in image optimization pipelines in the industry. But once you scale to tens of thousands of high-res assets spread across distributed sources, you run into subtle edge cases.

The Upstream Hotlinking & CORP Problem

Many external image hosts protect their bandwidth by checking the HTTP Referer header. When next/image tries to optimize an image server-side, it makes a bare fetch without the browser's context. The CDN returns a 403 Forbidden or a 0-byte response, leaving the user staring at an empty card.

Even worse: when serving directly from Cloudflare R2 public buckets, direct browser fetches can get blocked by modern browser security policies enforcing Cross-Origin-Resource-Policy: same-origin.

The Edge CPU Budget Reality

In an edge deployment (like Cloudflare Workers via OpenNext or Vercel Edge), you typically have a tiny CPU execution window (10ms–50ms). Running heavyweight image transformation libraries like sharp or WASM libvips inside edge handlers on massive 8K source files will trigger immediate timeout errors.


2. The Solution: A 3-Tier Image Normalization Pipeline

Instead of passing raw database URLs directly into UI components, every single wallpaper URL flows through a strict, deterministic normalization pipeline before hitting the DOM.

Database image_url
  │
  ├── 1. getDisplayUrl(url, source)      → Strips thumbnail artifacts & normalizes CDN hostnames
  │
  ├── 2. getProxiedUrl(displayUrl, opts) → Routes through /api/proxy with source ID & dimensions
  │
  └── 3. Edge Proxy Route (/api/proxy)   → Injects headers, handles R2 bindings & edge caching
Enter fullscreen mode Exit fullscreen mode

Here’s what our client-facing transform looks like:

// lib/utils.ts
export function getProxiedUrl(
  url: string,
  options?: { width?: number; quality?: number; fallback?: string | null; sourceId?: string | null }
) {
  if (!url) return "";

  // Normalize protocol-relative URLs
  if (url.startsWith('//')) url = `https:${url}`;
  if (url.startsWith('/') || url.startsWith('data:')) return url;

  let proxied = `/api/proxy?url=${encodeURIComponent(url)}`;
  if (options?.width) proxied += `&width=${options.width}`;
  if (options?.quality) proxied += `&quality=${options.quality}`;
  if (options?.fallback) proxied += `&fallback=${encodeURIComponent(options.fallback)}`;
  if (options?.sourceId) proxied += `&sid=${encodeURIComponent(options.sourceId)}`;

  return proxied;
}
Enter fullscreen mode Exit fullscreen mode

And for downloads, we deliberately separate the pipeline:

// Grid display (optimized resolution for screen density)
const displaySrc = getProxiedUrl(getDisplayUrl(wallpaper.image_url, wallpaper.source), {
  width: 400,
  quality: 75
});

// Explicit download (always delivers untouched, full-resolution master)
const downloadSrc = getFullResolutionUrl(wallpaper.image_url, wallpaper.source);
Enter fullscreen mode Exit fullscreen mode

3. Streaming at the Edge: 10ms CPU Friendly Proxying

Rather than buffering multi-megabyte images in memory, our /api/proxy route acts as an edge stream coordinator.

  1. SSRF Guard & Hostname Allowlisting: Rejects any non-allowlisted domains before initiating a connection.
  2. Cloudflare R2 Direct Binding: When an R2 URL is detected, the worker fetches the asset directly through the internal R2 bucket binding, bypassing public internet roundtrips and CORP restrictions.
  3. Edge Streaming: The response stream is piped directly to the client with aggressive edge caching headers.
// app/api/proxy/route.ts
const CACHE_HEADERS = {
  'Cache-Control': 'public, max-age=604800, s-maxage=2592000, stale-while-revalidate=86400, immutable',
  'CDN-Cache-Control': 'public, max-age=2592000',
  'Cloudflare-CDN-Cache-Control': 'public, max-age=2592000',
  'Access-Control-Allow-Origin': '*',
  'X-Content-Type-Options': 'nosniff',
};

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const targetUrl = searchParams.get('url');

  if (!targetUrl || !isAllowedUrl(targetUrl)) {
    return new NextResponse('Invalid or forbidden URL', { status: 403 });
  }

  // 1. Fetch upstream with appropriate Referer to bypass CDN hotlinking
  const upstreamResponse = await fetch(targetUrl, {
    headers: {
      'User-Agent': 'ZenithWalls-ImageWorker/2.0',
      'Referer': new URL(targetUrl).origin,
      'Accept': 'image/avif,image/webp,image/*,*/*',
    },
  });

  if (!upstreamResponse.ok) {
    return getFallbackSvgResponse(upstreamResponse.status);
  }

  // 2. Stream directly back to client with 30-day immutable edge cache
  return new NextResponse(upstreamResponse.body, {
    status: 200,
    headers: {
      'Content-Type': upstreamResponse.headers.get('content-type') || 'image/jpeg',
      ...CACHE_HEADERS,
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Repeat requests hit Cloudflare’s global edge cache with sub-20ms response times worldwide, completely shielding both our origin servers and upstream CDNs.


4. Zero-Network Blur Placeholders (0ms CLS)

Blur placeholders are essential to prevent jarring layout shifts (CLS), but downloading a low-quality image placeholder (LQIP) over the network for every card in a 40-item grid adds 40 extra roundtrips.

We solved this during our data indexing phase:

  1. When a wallpaper is ingested, we compute and store its dominant_color (e.g. #1e1b4b or #0f172a).
  2. On the client, we generate an inline 1×1 SVG data URI on the fly.
function hexToBlurDataURL(hex: string | null | undefined): string | undefined {
  if (!hex) return undefined;
  const color = hex.startsWith("#") ? hex : `#${hex}`;
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="${color}"/></svg>`;
  const b64 = typeof btoa !== "undefined"
    ? btoa(svg)
    : Buffer.from(svg).toString("base64");
  return `data:image/svg+xml;base64,${b64}`;
}
Enter fullscreen mode Exit fullscreen mode

When passing this into Next.js <Image />:

<Image
  src={thumbUrl}
  alt={wallpaper.title}
  fill
  placeholder="blur"
  blurDataURL={hexToBlurDataURL(wallpaper.dominant_color)}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  className="object-cover transition-opacity duration-300"
/>
Enter fullscreen mode Exit fullscreen mode

Result: Zero extra network calls. The instant the card mounts, the background glows with the natural dominant palette of the wallpaper, smoothly crossfading into the crisp high-res WebP once downloaded.


5. Crushing LCP (Largest Contentful Paint)

High-res media sites often struggle with LCP because the browser downloads everything with equal priority or wastes bandwidth on background animations.

Here are the four high-impact tweaks that took our LCP down to sub-1.2s:

A. Strict Priority for Above-The-Fold Cards

We dynamically set priority and fetchPriority="high" only for the first row of visible cards (first 6 items on desktop):

<Image
  src={thumbUrl}
  alt={wallpaper.title}
  priority={index < 6}
  loading={index < 6 ? "eager" : "lazy"}
  // ...
/>
Enter fullscreen mode Exit fullscreen mode

B. Deferring Heavy Background Scripts & Video

If your hero section features an ambient animated preview or canvas effects, don't initialize them on initial parse. We defer heavy libraries like hls.js by 1,500ms after component mount:

useEffect(() => {
  const timer = setTimeout(() => {
    import('hls.js').then((Hls) => {
      // Initialize video background after LCP candidate has rendered
    });
  }, 1500);

  return () => clearTimeout(timer);
}, []);
Enter fullscreen mode Exit fullscreen mode

C. Preconnecting to CDN Origins

In our root layout.tsx, we add explicit <link rel="preconnect"> hints for our primary image domains, avoiding DNS resolution and TLS handshake lag during initial paint:

<link rel="preconnect" href="https://pub-b05fd36ab45911f92a719a54ce416246.r2.dev" />
<link rel="dns-prefetch" href="https://images.alphacoders.com" />
Enter fullscreen mode Exit fullscreen mode

6. Masonry Grid & DOM Memory Management

Rendering hundreds of cards in an infinite masonry grid can quickly trigger memory leaks and jank during scrolling.

The Single IntersectionObserver Pattern

Instead of attaching individual IntersectionObserver instances to every single card component, we use a single shared hook at the grid container level:

function useGridEntranceObserver() {
  const observerRef = useRef<IntersectionObserver | null>(null);

  const getObserver = useCallback(() => {
    if (!observerRef.current) {
      observerRef.current = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              (entry.target as HTMLElement).classList.add("is-visible");
              observerRef.current?.unobserve(entry.target);
            }
          });
        },
        { rootMargin: "0px 0px -40px 0px", threshold: 0 }
      );
    }
    return observerRef.current;
  }, []);

  return {
    observe: (el: HTMLElement | null) => el && getObserver().observe(el),
    unobserve: (el: HTMLElement | null) => el && observerRef.current?.unobserve(el),
  };
}
Enter fullscreen mode Exit fullscreen mode

On-Demand Category Loading

Instead of querying all 8 categories during server-side rendering on the homepage (which previously required 16 parallel queries fetching 120+ wallpapers), we now render only the active "Anime" tab on initial load. Other categories are fetched dynamically on tab switch with in-memory caching. This cut initial SSR data transfer by 87%.


Summary: The Key Takeaways

If you are building a media-heavy Next.js application in 2025/2026, here is what actually works in production:

  1. Never pass raw third-party URLs directly to the client: Always funnel assets through a normalized proxy pipeline to control headers, caching, and failover behavior.
  2. Keep the Edge lightweight: Avoid heavy WASM / Sharp resizing inside edge routes unless you are on a dedicated plan. Stream responses and let CDN edge caching do the heavy lifting.
  3. Use 1×1 SVG Dominant Color Blur Data URLs: It gives users an immediate visual anchor with zero network overhead and eliminates layout shifts.
  4. Be ruthless with above-the-fold prioritization: Only set priority={true} on the top 4–6 images and defer heavy video/canvas dependencies until after initial paint.
  5. Share observers across grid items: Don't spin up dozens of IntersectionObserver listeners for infinite scroll cards.

You can see the final result live in action at Zenith Walls.

Have questions about our Next.js image architecture or Cloudflare R2 setup? Drop a comment below!

Top comments (0)