Free image CDN: GitHub Pages & JS
Delivering optimized images quickly is crucial for web performance. While commercial CDNs offer robust solutions, small public projects can use GitHub Pages for static image hosting and jsDelivr for cached delivery of repository assets. Neither service automatically optimizes those images.
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
imagesfolder 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
mainbranch 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.
- Published site size limit of 1GB, with a recommended source repository limit of 1GB.
- No built-in image processing.
- The default GitHub Pages domain may be blocked in some corporate environments.
Use this setup for small public project assets, not private uploads or a commercial image-hosting service. GitHub Pages restricts commercial transaction and SaaS hosting. Check the usage limits and jsDelivr's usage policy before relying on either service.
Integrating a JavaScript CDN
Use a CDN service such as jsDelivr to cache public repository assets. It is not a JavaScript library that you install, and it does not resize images for you.
Reference your images through jsDelivr:
<img
src="https://cdn.jsdelivr.net/gh/your-username/image-cdn/images/your-image.jpg"
alt="Optimized Image"
/>
Pin a release tag or commit in the URL for predictable caching, for example
https://cdn.jsdelivr.net/gh/your-username/image-cdn@v1.0.0/images/your-image.jpg after creating that
tag. Update the version when images change.
Reliability and fallback
While jsDelivr offers excellent performance, implement fallback strategies:
function loadImage(imageElement, primarySrc, fallbackSrc) {
imageElement.onerror = function () {
imageElement.onerror = null
console.warn('Primary CDN failed, using fallback')
imageElement.src = fallbackSrc
}
imageElement.src = primarySrc
}
const img = document.getElementById('my-image')
if (!(img instanceof HTMLImageElement)) throw new Error('Missing image element: 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',
)
Add an <img id="my-image" alt="..."> element with appropriate alternative text before running
this code. If both origins fail, the browser retains the image's alternative text rather than
retrying the fallback indefinitely.
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
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Compress Images
uses: calibreapp/image-actions@26d5b54006db4da7a1b29903f33404a6f77b58cf
This workflow optimizes images on same-repository pull requests and can commit changes and post a summary. Forked pull requests are deliberately excluded. Review the action before updating the pinned commit; see its configuration.
Implementing custom optimization
For more control, install sharp in a Node.js 22 or later project and save this as optimize.cjs.
Put source images in original-images/ and run node optimize.cjs. Outputs go into a separate
directory; existing files there are replaced. JPEG and WebP quality settings are lossy, and setting
PNG quality enables palette quantization, which can also lose colors:
const sharp = require('sharp')
const fs = require('fs').promises
const path = require('path')
async function optimizeImage(inputPath, outputPath) {
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)
throw error
}
}
processDirectory('original-images', 'optimized-images').catch(() => {
process.exitCode = 1
})
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) if (imageEntries.length === 0) return 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
GitHub Pages does not use a _headers file to configure HTTP response headers. Check the actual
response headers with curl -I and use a host with configurable headers when your application
requires a custom CORS or cache policy. CORS controls script access to response bytes; it is not
authentication and does not prevent ordinary image embedding.
Prevent hotlinking
Client-side JavaScript cannot prevent another site from embedding public image URLs. If access control or traffic limits matter, use a provider that enforces signed URLs or another server-side policy. Do not store private or sensitive images in this public repository.
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:
- No hosting charge for small public projects within the providers' usage policies.
- Easy setup and automation.
- Improved global performance.
Limitations:
- GitHub Pages has a soft bandwidth limit of 100GB/month and a published-site limit of 1GB.
- Less control compared to commercial CDNs.
- No built-in image transformation.
For larger projects or advanced needs, consider services like Transloadit's Image Processing API.
By following these steps, you've created a free image CDN using GitHub Pages, a JavaScript CDN, and GitHub Actions.
