Free image CDN: GitHub pages & js

Delivering optimized images quickly is crucial for web performance. While commercial CDNs offer robust solutions, developers can set up a free image CDN using GitHub Pages and JavaScript-based CDN libraries.
Understanding image CDNs
An Image CDN (Content Delivery Network) caches and serves images from servers geographically closer to users, significantly improving load times. Optimizing images through a CDN reduces bandwidth, enhances SEO, and boosts overall web performance.
Setting up GitHub pages
GitHub Pages provides free static hosting directly from your GitHub repositories:
-
Create a GitHub repository named
image-cdn
. -
Clone the repository locally:
git clone https://github.com/your-username/image-cdn.git cd image-cdn
-
Create an
images
folder and add your images. -
Commit and push your changes:
git add images git commit -m "Add initial images" git push origin main
-
Enable GitHub Pages in your repository settings, selecting the
main
branch as the source.
Your images are now accessible via
https://your-username.github.io/image-cdn/images/your-image.jpg
.
GitHub pages limitations
Be aware of GitHub Pages' limitations:
- Soft bandwidth limit of 100GB per month.
- Recommended repository size under 1GB (strongly recommended under 5GB).
- No built-in image processing.
- The default GitHub Pages domain may be blocked in some corporate environments.
These limitations make GitHub Pages suitable for small to medium projects.
Integrating a JavaScript CDN
To optimize image delivery, integrate a JavaScript CDN library like jsDelivr. jsDelivr provides global caching.
Reference your images through jsDelivr:
<img
src="https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/your-image.jpg"
alt="Optimized Image"
/>
This ensures your images are cached globally.
Reliability and fallback
While jsDelivr offers excellent performance, implement fallback strategies:
function loadImage(imageElement, primarySrc, fallbackSrc) {
imageElement.onerror = function () {
console.warn('Primary CDN failed, using fallback')
imageElement.src = fallbackSrc
}
imageElement.src = primarySrc
}
const img = document.getElementById('my-image')
loadImage(
img,
'https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image.jpg',
'https://your-username.github.io/image-cdn/images/image.jpg',
)
Alternative CDN options include Statically, unpkg, or Cloudflare Pages.
Automating with GitHub actions
Automate image optimization and deployment using GitHub Actions:
Create .github/workflows/compress-images.yml
:
name: Compress Images
on:
pull_request:
paths:
- '**.jpg'
- '**.jpeg'
- '**.png'
- '**.webp'
jobs:
build:
if: github.event.pull_request.head.repo.full_name == github.repository
name: calibreapp/image-actions
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Compress Images
uses: calibreapp/image-actions@main
with:
githubToken: $
This workflow automatically optimizes images on pull requests.
Implementing custom optimization
For more control, use a Node.js script with Sharp:
const sharp = require('sharp')
const fs = require('fs').promises
const path = require('path')
async function optimizeImage(inputPath, outputPath, options = {}) {
try {
const image = sharp(inputPath)
const metadata = await image.metadata()
const config = {
jpeg: { quality: 80, progressive: true },
png: { quality: 80, compressionLevel: 9 },
webp: { quality: 80 },
}
switch (metadata.format) {
case 'jpeg':
await image.jpeg(config.jpeg).toFile(outputPath)
break
case 'png':
await image.png(config.png).toFile(outputPath)
break
case 'webp':
await image.webp(config.webp).toFile(outputPath)
break
default:
throw new Error(`Unsupported format: ${metadata.format}`)
}
console.log(`Optimized: ${path.basename(inputPath)}`)
return outputPath
} catch (error) {
console.error(`Failed to optimize ${inputPath}:`, error)
throw error
}
}
async function processDirectory(inputDir, outputDir) {
try {
await fs.mkdir(outputDir, { recursive: true })
const files = await fs.readdir(inputDir)
for (const file of files) {
const inputPath = path.join(inputDir, file)
const outputPath = path.join(outputDir, file)
const stat = await fs.stat(inputPath)
if (stat.isFile() && /\.(jpe?g|png|webp)$/i.test(file)) {
await optimizeImage(inputPath, outputPath)
}
}
console.log('All images processed successfully')
} catch (error) {
console.error('Directory processing failed:', error)
}
}
processDirectory('original-images', 'optimized-images')
Testing your setup
Verify your CDN:
-
Browser Developer Tools: Check the Network panel.
-
Performance Testing Tools:
-
Real User Monitoring: Use the Performance API:
document.addEventListener('DOMContentLoaded', () => { window.addEventListener('load', () => { const imageEntries = performance .getEntriesByType('resource') .filter((entry) => entry.initiatorType === 'img') const totalLoadTime = imageEntries.reduce((sum, entry) => sum + entry.duration, 0) const avgLoadTime = totalLoadTime / imageEntries.length console.log(`Average image load time: ${avgLoadTime.toFixed(2)}ms`) imageEntries.forEach((entry) => { console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`) }) }) })
Security
Implement these security measures:
Cors headers
Create a _headers
file:
/*
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000
Content-Security-Policy: default-src 'self'; img-src 'self' https://cdn.jsdelivr.net
Prevent hotlinking
If traffic exceeds limits, use a JavaScript check:
document.addEventListener('DOMContentLoaded', () => {
const images = document.querySelectorAll('img[data-src]')
const allowedDomains = ['yourdomain.com', 'localhost']
if (allowedDomains.some((domain) => window.location.hostname.includes(domain))) {
images.forEach((img) => {
img.src = img.getAttribute('data-src')
})
}
})
Advanced optimization
Responsive images
<img
srcset="
https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image-small.jpg 480w,
https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image-medium.jpg 800w,
https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image-large.jpg 1200w
"
sizes="(max-width: 600px) 480px,
(max-width: 1200px) 800px,
1200px"
src="https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image-large.jpg"
alt="Responsive Image"
loading="lazy"
/>
Modern image formats
Use a format detection and fallback system:
<picture>
<source
srcset="https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image.avif"
type="image/avif"
/>
<source
srcset="https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image.webp"
type="image/webp"
/>
<img
src="https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/image.jpg"
alt="Optimized image with format fallbacks"
loading="lazy"
/>
</picture>
Benefits and limitations
Benefits:
- Zero cost for small to medium projects.
- Easy setup and automation.
- Improved global performance.
Limitations:
- GitHub Pages has bandwidth (100GB/month) and storage limits (1-5GB recommended).
- Less control compared to commercial CDNs.
- No built-in image transformation.
For larger projects or advanced needs, consider services like Transloadit's Image Manipulation Service.
By following these steps, you've created a free image CDN using GitHub Pages, a JavaScript CDN, and GitHub Actions.