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:
# .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_keyInstall the SDK based on your needs:
# For server-side uploads (Express, Fastify, etc.)
npm install @easycdn/server
# For browser uploads (React component)
npm install @easycdn/reactThat'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:
// 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:
// React
import { cdnUrl } from '../lib/cdn'
function HeroSection() {
return (
<section>
<img
src={cdnUrl('images/hero-banner.webp')}
alt="Hero banner"
loading="lazy"
/>
</section>
)
}<!-- 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:
// 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:
{/* 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:
// 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:
// 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:
npm install @easycdn/reactimport { 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:
// 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:
// 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!
