How to Add a CDN to Your Vite Project

Back to Blog
Lukas Gisder-Dubé
8 min read
cdnvitereactvuefrontendtutorial

How to Add a CDN to Your Vite Project

Tired of slow builds and lagging page loads in your Vite app? A CDN might be the quick fix you're looking for.

Vite has become the go-to build tool for modern frontend projects. It's fast, it's simple, and it just works. But when it comes to serving images and other assets, most Vite projects still rely on bundling everything into the public folder or importing assets directly.

That works fine in development. In production? You're leaving performance on the table.

Let me show you how to integrate a CDN into your Vite project—whether you're using React, Vue, or vanilla JS. We'll cover both static assets and dynamic uploads.

Why Bother with a CDN for Vite Projects?

Vite already optimizes your JavaScript and CSS. Why add a CDN for images?

Bundle size. Every image in your public folder or imported via import adds to your deployment size. CDN-hosted assets don't.

Cache efficiency. Your Vercel/Netlify deployment cache resets every deploy. CDN caches are persistent—that hero image stays cached globally even when you push a typo fix.

Global performance. Vite builds are served from wherever you deploy. CDN assets are served from edge locations worldwide. For users in Singapore loading images from your US-hosted app, that's the difference between 200ms and 50ms.

Dynamic content. User uploads, generated images, or content from a CMS don't belong in your bundle anyway.

Setting Up: The 5-Minute Version

Let's get a basic integration running. I'll use easyCDN for the examples, but the patterns apply to any CDN.

First, grab your API keys. Sign up at easyCDN, create a project, and note your project ID, CDN URL, and API keys (secret key for server uploads, public key for browser uploads).

Add these to your environment. Note that only VITE_ prefixed variables are exposed to the client:

bash
# .env.local (client-side, committed to repo is OK)
VITE_CDN_URL=https://cdn.easycdn.co/your-project-id
VITE_EASYCDN_PROJECT_ID=your_project_id
VITE_EASYCDN_PUBLIC_KEY=your_public_key

# .env (server-side only, add to .gitignore!)
EASYCDN_SECRET_KEY=your_secret_key

Install the SDK based on your needs:

bash
# For server-side uploads (Express, Fastify, etc.)
npm install @easycdn/server

# For browser uploads (React component)
npm install @easycdn/react

That's the setup. Now that you've got the basics, let's dive into specific ways to use your CDN with Vite.

Pattern 1: Static Asset References

The simplest approach is referencing CDN URLs directly. Create a utility function to make this clean:

ts
// src/lib/cdn.ts
const CDN_URL = import.meta.env.VITE_CDN_URL

export function cdnUrl(path: string): string {
  // Remove leading slash if present
  const cleanPath = path.startsWith('/') ? path.slice(1) : path
  return `${CDN_URL}/${cleanPath}`
}

Use it in your components:

tsx
// React
import { cdnUrl } from '../lib/cdn'

function HeroSection() {
  return (
    <section>
      <img
        src={cdnUrl('images/hero-banner.webp')}
        alt="Hero banner"
        loading="lazy"
      />
    </section>
  )
}
vue
<!-- Vue -->
<script setup>
import { cdnUrl } from '../lib/cdn'
</script>

<template>
  <section>
    <img
      :src="cdnUrl('images/hero-banner.webp')"
      alt="Hero banner"
      loading="lazy"
    />
  </section>
</template>

This works great for images you upload once and reference throughout your app—logos, illustrations, background images.

Pattern 2: Image Component with Preview Support

easyCDN automatically generates optimized preview versions of your images at upload time. You configure the preview settings (width, quality, format) in your project dashboard, and every uploaded image gets a -preview variant.

Let's build a component that makes it easy to switch between original and preview versions:

tsx
// src/components/CdnImage.tsx
import { cdnUrl } from '../lib/cdn'

interface CdnImageProps {
  src: string
  alt: string
  width?: number
  height?: number
  className?: string
  fallback?: string
  usePreview?: boolean
}

export function CdnImage({
  src,
  alt,
  width,
  height,
  className,
  fallback = '/fallback-image.png',
  usePreview = false
}: CdnImageProps) {
  // If usePreview is true, reference the preview version
  // Preview files are named: original-name-preview.webp
  const imageSrc = usePreview
    ? src.replace(/\.[^.]+$/, '-preview.webp')
    : src

  return (
    <img
      src={cdnUrl(imageSrc)}
      alt={alt}
      width={width}
      height={height}
      className={className}
      loading="lazy"
      onError={(e) => {
        e.currentTarget.src = fallback
      }}
    />
  )
}

Now you can use it throughout your app:

tsx
{/* Full resolution for hero images */}
<CdnImage
  src="products/widget-pro.png"
  alt="Widget Pro product image"
  width={800}
/>

{/* Optimized preview for thumbnails */}
<CdnImage
  src="products/widget-pro.png"
  alt="Widget Pro thumbnail"
  width={400}
  usePreview
/>

Upload once at full resolution, and easyCDN creates an optimized preview automatically. Configure preview settings (default: 500px width, 80% quality, WebP format) in your project dashboard.

Pattern 3: File Uploads with Vite + Express/Fastify

For apps with user uploads, you'll need a backend endpoint. Here's a minimal setup with Express:

ts
// server/upload.ts
import express from 'express'
import multer from 'multer'
import { createClient } from '@easycdn/server'

const app = express()
const upload = multer({ storage: multer.memoryStorage() })

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

app.post('/api/upload', upload.single('file'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file provided' })
  }

  try {
    const result = await cdn.upload(req.file.buffer, {
      fileName: `uploads/${Date.now()}-${req.file.originalname}`,
      contentType: req.file.mimetype,
    })

    res.json({ url: result.asset.url })
  } catch (error) {
    console.error('Upload failed:', error)
    res.status(500).json({ error: 'Upload failed' })
  }
})

On the frontend, create an upload hook:

ts
// src/hooks/useUpload.ts
import { useState } from 'react'

export function useUpload() {
  const [uploading, setUploading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function upload(file: File): Promise<string | null> {
    setUploading(true)
    setError(null)

    const formData = new FormData()
    formData.append('file', file)

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      })

      if (!response.ok) {
        throw new Error('Upload failed')
      }

      const { url } = await response.json()
      return url
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Upload failed')
      return null
    } finally {
      setUploading(false)
    }
  }

  return { upload, uploading, error }
}

Pattern 4: Direct Browser Uploads

For larger files or high-traffic apps, uploading through your server can be inefficient since your server becomes a bottleneck. easyCDN offers a React component for direct browser uploads:

bash
npm install @easycdn/react
tsx
import { Dropzone } from '@easycdn/react'

function FileUploader() {
  return (
    <Dropzone
      publicKey={import.meta.env.VITE_EASYCDN_PUBLIC_KEY}
      projectId={import.meta.env.VITE_EASYCDN_PROJECT_ID}
      onUploadComplete={(asset) => {
        console.log('Uploaded:', asset.url)
      }}
      maxFiles={5}
      maxSize={1024 * 1024 * 100} // 100MB
    />
  )
}

The Dropzone handles chunked uploads, progress tracking, and retries automatically. Files go directly from the browser to easyCDN's servers—no server bottleneck.

Vite Config Tips

A few Vite configuration tweaks that help with CDN integration. Using define lets you inject environment variables into your client-side code at build time:

ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  // Useful if you want to keep some assets local
  build: {
    assetsInlineLimit: 4096, // Inline assets under 4KB
  },

  // Define CDN URL for production builds
  define: {
    __CDN_URL__: JSON.stringify(process.env.VITE_CDN_URL),
  },
})

Migrating Existing Assets

Already have images in your public folder? Here's a quick migration script:

ts
// scripts/migrate-to-cdn.ts
import { readdir, readFile } from 'fs/promises'
import { join, extname } from 'path'
import { createClient } from '@easycdn/server'

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

const ASSET_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg']

async function migrateAssets(dir: string) {
  const files = await readdir(dir, { withFileTypes: true })

  for (const file of files) {
    const fullPath = join(dir, file.name)

    if (file.isDirectory()) {
      await migrateAssets(fullPath)
      continue
    }

    const ext = extname(file.name).toLowerCase()
    if (!ASSET_EXTENSIONS.includes(ext)) continue

    const buffer = await readFile(fullPath)
    const relativePath = fullPath.replace('public/', '')

    const result = await cdn.upload(buffer, {
      filename: relativePath,
    })

    console.log(`Migrated: ${relativePath} -> ${result.url}`)
  }
}

migrateAssets('public/images')

Run it once, update your references, and you're done.

Performance Wins

After integrating a CDN into a client's Vite + React app (a mid-sized e-commerce site with 100+ product images, deployed on Vercel and tested across 5 global regions), we saw:

  • Deployment size: Down from 45MB to 12MB (images moved to CDN)
  • Build time: 40% faster (fewer assets to process)
  • LCP (Largest Contentful Paint): Improved by 800ms on average
  • Monthly hosting costs: Down $30 (smaller deployments = cheaper)

The biggest win was iteration speed. Pushing code changes no longer meant re-uploading 30MB of images to Vercel.

Common Gotchas

Environment variables. Remember that Vite only exposes env vars prefixed with VITE_ to the client. Keep your secret key server-side only and never commit it to version control.

Older browser support. Preview images are generated in the format you configure (WebP by default). If you need to support older browsers that don't handle WebP, change your project's preview format to JPEG in the dashboard, or keep original files in a compatible format as fallbacks.

Wrapping Up

Adding a CDN to your Vite project isn't complicated, but it does require thinking about assets differently. Instead of bundling everything, you're hosting assets separately and referencing them by URL.

The result? Faster builds, quicker page loads, and a neat split between code and content.

Ready to speed up your app? Sign up for easyCDN and have your Vite project serving assets from the edge in minutes. The free tier handles most side projects, and scaling up is straightforward when you need it.

Let me know in the comments if you run into any snags with your setup!

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