Efficient image delivery: creating your own CDN
Building your own Content Delivery Network (CDN) for images can significantly enhance your application's performance and give you full control over image delivery. In this guide, we'll explore how to create and optimize a custom CDN for efficient image delivery.
Benefits of using a CDN for images
A CDN for images offers several advantages:
- Reduced server load and bandwidth costs: Offloading image delivery to a CDN minimizes the strain on your primary servers.
- Faster image loading times: Serve images from locations closer to your users for reduced latency.
- Improved SEO performance: Faster page loads can boost your search engine rankings.
- Scalability: Easily handle traffic spikes without additional infrastructure.
- Custom optimization: Tailor image delivery for different devices and contexts.
Understanding CDN functionality
A CDN is a network of distributed servers that deliver content based on the geographic location of the user. By caching content at multiple edge locations, a CDN reduces latency and improves load times for users worldwide.
Components needed to create your own image CDN
To set up a custom image CDN, you'll need:
- A web server to handle incoming requests
- A storage solution for your images (local or cloud-based)
- Image processing tools for optimization
- A caching mechanism to speed up delivery
- Security measures to protect your assets
- Monitoring tools to track performance and load
Setting up a basic CDN infrastructure
Let's create a basic CDN using Node.js and Express. We'll set up a server that serves optimized images on demand.
const express = require('express')
const compression = require('compression')
const path = require('path')
const sharp = require('sharp')
const fs = require('fs')
const app = express()
app.use(compression())
const allowedExtensions = ['.jpg', '.jpeg', '.png']
const cacheControl = (req, res, next) => {
res.set('Cache-Control', 'public, max-age=31536000')
next()
}
const optimizeImage = async (req, res, next) => {
try {
const imageName = req.params.image
const imageExtension = path.extname(imageName).toLowerCase()
if (!allowedExtensions.includes(imageExtension)) {
return res.status(400).send('Unsupported image format')
}
const imagePath = path.join(__dirname, 'images', imageName)
if (!fs.existsSync(imagePath)) {
return res.status(404).send('Image not found')
}
const width = parseInt(req.query.width)
const quality = parseInt(req.query.quality) || 80
let transformer = sharp(imagePath)
if (!isNaN(width)) {
transformer = transformer.resize(width, null, {
withoutEnlargement: true,
})
}
const buffer = await transformer.webp({ quality }).toBuffer()
res.type('image/webp')
res.send(buffer)
} catch (error) {
next(error)
}
}
app.get('/images/:image', cacheControl, optimizeImage)
app.listen(3000, () => {
console.log('CDN server running on port 3000')
})
In this code:
- Compression: We enable compression middleware to compress responses.
- Caching: The
cacheControl
middleware sets caching headers to leverage browser caching. - Image Optimization: The
optimizeImage
middleware resizes and converts images to WebP format.
Integrating your custom CDN with cloud storage
For better scalability, integrate your CDN with cloud storage services like Google Cloud Storage or Amazon S3. Here's how you can modify the server to fetch images from Google Cloud Storage:
const { Storage } = require('@google-cloud/storage')
const storage = new Storage()
const fetchFromCloud = async (req, res, next) => {
try {
const bucketName = 'your-bucket-name'
const fileName = req.params.image
const width = parseInt(req.query.width)
const quality = parseInt(req.query.quality) || 80
const bucket = storage.bucket(bucketName)
const file = bucket.file(fileName)
const [exists] = await file.exists()
if (!exists) {
return res.status(404).send('Image not found')
}
let transformer = sharp()
if (!isNaN(width)) {
transformer = transformer.resize(width, null, {
withoutEnlargement: true,
})
}
transformer = transformer.webp({ quality })
res.type('image/webp')
file.createReadStream().pipe(transformer).on('error', next).pipe(res)
} catch (error) {
next(error)
}
}
app.get('/cloud-images/:image', cacheControl, fetchFromCloud)
Remember to authenticate with your cloud provider and replace 'your-bucket-name'
with your actual
bucket name.
Optimizing image compression and format
Implement adaptive image optimization based on client capabilities by detecting supported image
formats from the Accept
header.
const getOptimalFormat = (req) => {
const accept = req.headers.accept || ''
if (accept.includes('image/avif')) return 'avif'
if (accept.includes('image/webp')) return 'webp'
return 'jpeg'
}
const optimizeForClient = async (req, res, next) => {
try {
const format = getOptimalFormat(req)
const imageName = req.params.image
const imagePath = path.join(__dirname, 'images', imageName)
const width = parseInt(req.query.width)
if (!fs.existsSync(imagePath)) {
return res.status(404).send('Image not found')
}
let transformer = sharp(imagePath)
if (!isNaN(width)) {
transformer = transformer.resize(width, null, {
withoutEnlargement: true,
})
}
switch (format) {
case 'avif':
transformer = transformer.avif({ quality: 50 })
res.type('image/avif')
break
case 'webp':
transformer = transformer.webp({ quality: 80 })
res.type('image/webp')
break
default:
transformer = transformer.jpeg({ quality: 85 })
res.type('image/jpeg')
}
const buffer = await transformer.toBuffer()
res.send(buffer)
} catch (error) {
next(error)
}
}
This middleware selects the optimal image format based on the client's capabilities, improving compression and loading times.
Leveraging cache for faster image delivery
To speed up image delivery, implement a caching layer using Redis:
const Redis = require('ioredis')
const redis = new Redis()
const cacheMiddleware = async (req, res, next) => {
const cacheKey = `img:${req.originalUrl}`
try {
const cachedImage = await redis.getBuffer(cacheKey)
if (cachedImage) {
res.type('image/webp')
return res.send(cachedImage)
}
res.sendResponse = res.send
res.send = async (body) => {
await redis.set(cacheKey, body, 'EX', 86400) // Cache for 24 hours
res.sendResponse(body)
}
next()
} catch (error) {
next(error)
}
}
app.get('/images/:image', cacheControl, cacheMiddleware, optimizeImage)
This caching middleware checks if an image is cached before processing, reducing server load and response times.
Enhancing security: securing your image data
Implement basic security measures to protect your CDN:
const rateLimit = require('express-rate-limit')
const helmet = require('helmet')
app.use(helmet())
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // limit each IP to 1000 requests per windowMs
})
app.use('/images/', limiter)
const validateImageRequest = (req, res, next) => {
const width = parseInt(req.query.width)
if (width && (width < 50 || width > 2000)) {
return res.status(400).send('Invalid width parameter')
}
next()
}
app.get('/images/:image', validateImageRequest, cacheControl, cacheMiddleware, optimizeImage)
In this code:
- Helmet: Sets secure HTTP headers.
- Rate Limiting: Prevents abuse by limiting the number of requests per IP.
- Input Validation: Ensures query parameters are within acceptable ranges.
Monitoring and scaling your custom CDN
Monitoring helps you understand performance and identify bottlenecks. Consider using middleware like
response-time
and external tools like Prometheus or Grafana for comprehensive monitoring.
const responseTime = require('response-time')
app.use(
responseTime((req, res, time) => {
console.log(`${req.method} ${req.url}: ${time.toFixed(2)}ms`)
}),
)
app.use((err, req, res, next) => {
console.error(`Error processing ${req.url}:`, err)
res.status(500).send('Internal Server Error')
})
Testing the performance of your image CDN
Perform load testing to ensure your CDN can handle expected traffic. Use tools like autocannon
for
this purpose:
const autocannon = require('autocannon')
const test = async () => {
const result = await autocannon({
url: 'http://localhost:3000/images/test.jpg',
connections: 100,
duration: 10,
})
console.log(result)
}
test()
This script simulates 100 concurrent connections for 10 seconds to test the throughput of your CDN.
Conclusion
Building your own CDN for images provides flexibility and control over image delivery while optimizing performance. By implementing caching, compression, and monitoring, you can create a robust solution for serving images at scale.
If you prefer a managed solution for image processing and delivery, consider using Transloadit, which offers a reliable image CDN service out of the box.