How to Serve Static Assets for Your Next.js App

Back to Blog
Lukas Gisder-Dubé
6 min read
cdnweb-performancenext.jsstatic-assetsfrontend

How to Serve Static Assets for Your Next.js App

Last week, I deployed a Next.js project and watched my Vercel bill creep up from serving product images directly from my app. If this sounds familiar, you're not alone.

Next.js is fantastic for building modern web apps, but when it comes to serving static assets at scale—images, fonts, PDFs, videos—you'll eventually hit a wall. That's where a CDN comes in, and setting one up is way easier than you might think.

Why Does the /public Folder Fall Short?

When you drop files into your Next.js /public folder, they get served from your origin server. For small projects with low traffic, this works fine. But here's what happens as you grow:

  • Performance bottlenecks: Your server handles both app logic AND static file delivery, while users far from your server (like in Sydney) face delays as files travel from Virginia
  • Higher hosting costs: Every image request counts against your bandwidth and serverless function executions
  • Poor user experience: No edge caching means consistently slower load times globally

Watch out for surprise Vercel bills: Many Next.js developers get unexpected charges from Vercel's image optimization. Using next/image with files from your /public folder triggers server-side resizing on every uncached request—and those Image Optimization costs add up fast. A CDN with pre-optimized images avoids this entirely.

I learned this the hard way when a blog post went viral and my site ground to a halt. The images were the bottleneck—not my actual Next.js code.

Ready to offload that burden? Let's dive into the setup.

Why Use a CDN for Static Assets?

A Content Delivery Network stores copies of your files on servers around the world. When someone requests an image, they get it from the nearest server instead of your origin. The benefits are immediate:

  • Faster load times—according to Google's Web Vitals documentation, CDNs can reduce latency significantly for global users
  • Reduced server load on your Next.js app
  • Lower bandwidth costs since you're not serving files from your main hosting
  • Better Core Web Vitals which directly impacts SEO

Setting Up Your Next.js App with a CDN

Let's walk through the practical steps. I'll show you how to upload assets to a CDN and reference them in your Next.js components.

These examples use @easycdn/server v1.x—check the docs for the latest version.

Step 1: Upload Your Assets

First, you'll want to get your static files onto a CDN. With easyCDN, you can do this programmatically using the Node.js SDK:

ts
import { createClient } from '@easycdn/server'

const client = createClient({
  secretKey: process.env.EASYCDN_SECRET_KEY!,
})

// Upload a single file
try {
  const result = await client.upload('./assets/hero-image.webp')
  console.log(result.asset.url)
  // https://cdn.easycdn.co/abc123/7db88d71-9238-4621-92d9-acfe122e4420.webp
} catch (error) {
  console.error('Upload failed:', error)
}

You can also upload assets via drag and drop in the easyCDN dashboard.

For batch uploads (like when you're migrating an existing project), you can loop through your /public folder:

ts
import { createClient } from '@easycdn/server'
import fs from 'fs'
import path from 'path'

const client = createClient({
  secretKey: process.env.EASYCDN_SECRET_KEY!,
})

const publicDir = './public/images'
const files = fs.readdirSync(publicDir)

for (const file of files) {
  const filePath = path.join(publicDir, file)

  try {
    const result = await client.upload(filePath, { fileName: file })
    console.log(`Uploaded ${file}: ${result.asset.url}`)
  } catch (error) {
    console.error(`Failed to upload ${file}:`, error)
  }
}

Tip: For nested folders, use a recursive function or tools like glob to traverse directories.

Step 2: Reference CDN URLs in Your Components

Once your assets are on the CDN, the result.asset.url gives you the full URL to use. Store these URLs in your database or config file, then reference them in your components:

tsx
// components/Hero.tsx

// Use the URL returned from the upload response
const heroImageUrl = 'https://cdn.easycdn.co/abc123/7db88d71-9238-4621-92d9-acfe122e4420.webp'

export function Hero() {
  return (
    <section>
      <img
        src={heroImageUrl}
        alt="Hero image"
        width={1200}
        height={600}
        loading="eager"
      />
    </section>
  )
}

Why use <img> instead of Next.js Image? When you use the Next.js <Image> component with external URLs, Next.js proxies the image through its own optimization pipeline. This means your image gets written to Vercel's CDN, adding unnecessary latency and bandwidth costs. Since your images are already optimized and served from easyCDN's edge locations, using a native <img> tag delivers them directly to users without the extra hop.

Step 3: Handle Fonts and User Uploads

Custom fonts are another common culprit for slow page loads. Instead of self-hosting from /public/fonts, upload them to your CDN and use the returned URL in your CSS:

css
/* globals.css - use the URL returned from uploading your font file */
@font-face {
  font-family: 'CustomFont';
  src: url('https://cdn.easycdn.co/abc123/a1b2c3d4-e5f6-7890-abcd-ef1234567890.woff2') format('woff2');
  font-display: swap;
}

The font-display: swap ensures your text remains visible while the font loads. Combined with CDN edge caching, your fonts will load faster than ever.

For user uploads like profile pictures or document attachments, easyCDN provides a ready-to-use React component. Just drop in the <Dropzone> and you're done:

tsx
'use client'
import { Dropzone } from '@easycdn/react'

export default function MyUploadPage() {
  return (
    <Dropzone
      publicKey={process.env.NEXT_PUBLIC_EASYCDN_PUBLIC_KEY}
      onUploadComplete={({ tempId, previewUrl }) => {
        console.log('Upload complete:', { tempId, previewUrl })
        // persist this asset in the backend using the Node.js SDK
      }}
    />
  )
}

Then persist the uploaded file in your API route:

ts
// app/api/persist/route.ts
import { createClient } from '@easycdn/server'

const client = createClient({
  secretKey: process.env.EASYCDN_SECRET_KEY!,
})

export async function POST(request: Request) {
  const { tempAssetId } = await request.json()

  const persistedAsset = await client.persist({ tempAssetId })

  return Response.json({ asset: persistedAsset })
}

Now your user uploads go straight to the CDN without ever touching your server's disk. Check the docs for more details on persisting assets and error handling.

Quick Wins for Better Performance

With your assets on the CDN, here are some easy tweaks to maximize performance:

  1. Use WebP or AVIF for images—they're significantly smaller than PNG or JPEG
  2. Set far-future cache headers for versioned assets (easyCDN handles this automatically)
  3. Lazy load below-the-fold images using loading="lazy" on your <img> tags
  4. Preload critical assets like hero images and fonts in your <head>

Wrapping Up

Moving your static assets to a CDN is one of the highest-impact, lowest-effort optimizations you can make for your Next.js app. Your users get faster load times, your server gets breathing room, and your hosting bill stays reasonable.

Your Next.js app deserves better than serving images from a /public folder. Give it a proper CDN.

Ready to host your assets?

Create your free account and start serving your assets in minutes.

No credit card required • Get started in under 2 minutes