Introduction
In this article, we'll explore how to implement a fully client-side image compression tool that runs entirely in the browser. This approach provides excellent privacy protection, reduces server costs, and delivers a fast user experience.
Why Browser-Based Compression?
There are several compelling reasons to implement image compression in the browser:
1. Privacy Protection
When users compress images in the browser, their photos never leave their device. This is essential for:
- Business documents containing sensitive information
- Personal photos users don't want to upload to unknown servers
- Any content requiring strict data governance
2. Zero Server Costs
Traditional server-based image compression requires:
- Powerful servers with adequate CPU resources
- Bandwidth for uploading and downloading images
- Storage for temporary files
By processing images in the browser, we eliminate all these infrastructure costs.
3. Reduced Latency
Processing locally means no network round-trip delay. Users get instant feedback and faster results.
4. Offline Capability
Once the compression codecs are loaded, users can compress images without an internet connection.
Technical Architecture
Core Implementation
1. Data Structures
interface ImageFile {
id: string;
file: File;
previewUrl: string;
compressedUrl?: string;
compressedFileName?: string;
originalSize: number;
compressedSize?: number;
originalType: string;
}
interface Codecs {
decodeJpeg: (buffer: ArrayBuffer) => Promise<ImageData>;
encodeJpeg: (imageData: ImageData, options?: { quality: number }) => Promise<ArrayBuffer>;
decodePng: (buffer: ArrayBuffer) => Promise<ImageData>;
encodePng: (imageData: ImageData) => Promise<ArrayBuffer>;
decodeWebp: (buffer: ArrayBuffer) => Promise<ImageData>;
encodeWebp: (imageData: ImageData, options?: { quality: number }) => Promise<ArrayBuffer>;
decodeAvif: (buffer: ArrayBuffer) => Promise<ImageData>;
encodeAvif: (imageData: ImageData, options?: { quality: number }) => Promise<ArrayBuffer>;
}
2. Loading Codecs Dynamically
The application uses @jsquash libraries - WebAssembly-based image codecs that run entirely in the browser:
const JSDELivr = "https://esm.sh";
const CODEC_URLS = [
{ name: "JPEG", url: `${JSDELivr}/@jsquash/jpeg` },
{ name: "PNG", url: `${JSDELivr}/@jsquash/png` },
{ name: "WebP", url: `${JSDELivr}/@jsquash/webp` },
{ name: "AVIF", url: `${JSDELivr}/@jsquash/avif` },
];
export async function loadCodecs(onProgress?: (progress: number) => void): Promise<Codecs> {
const modules: Record<string, any> = {};
for (let i = 0; i < CODEC_URLS.length; i++) {
const codec = CODEC_URLS[i];
const mod = await eval(`import("${codec.url}")`);
modules[codec.name] = mod.default || mod;
if (onProgress) {
onProgress(Math.round(((i + 1) / CODEC_URLS.length) * 100));
}
}
return {
decodeJpeg: modules["JPEG"]?.decode,
encodeJpeg: modules["JPEG"]?.encode,
decodePng: modules["PNG"]?.decode,
encodePng: modules["PNG"]?.encode,
decodeWebp: modules["WebP"]?.decode,
encodeWebp: modules["WebP"]?.encode,
decodeAvif: modules["AVIF"]?.decode,
encodeAvif: modules["AVIF"]?.encode
};
}
3. Decoding Images
const decodeImage = async (buffer: ArrayBuffer, type: string): Promise<ImageData> => {
if (!codecs) throw new Error("Codecs not loaded");
let imageData: ImageData | null = null;
switch (type) {
case "image/jpeg":
imageData = await codecs.decodeJpeg(buffer);
break;
case "image/png":
imageData = await codecs.decodePng(buffer);
break;
case "image/webp":
imageData = await codecs.decodeWebp(buffer);
break;
case "image/avif":
imageData = await codecs.decodeAvif(buffer);
break;
default:
imageData = await codecs.decodeJpeg(buffer);
}
return imageData!;
};
4. Encoding Images
const encodeImage = async (imageData: ImageData, format: string, q: number): Promise<ArrayBuffer> => {
if (!codecs) throw new Error("Codecs not loaded");
switch (format) {
case "jpeg":
return codecs.encodeJpeg(imageData, { quality: q });
case "png":
return codecs.encodePng(imageData);
case "webp":
return codecs.encodeWebp(imageData, { quality: q });
case "avif":
return codecs.encodeAvif(imageData, { quality: q });
default:
return codecs.encodeWebp(imageData, { quality: q });
}
};
5. Compression Workflow
const compressImage = async (imageFile: ImageFile): Promise<ImageFile> => {
try {
// Read the original file as ArrayBuffer
const arrayBuffer = await imageFile.file.arrayBuffer();
// Decode the image to get raw pixel data
const imageData = await decodeImage(arrayBuffer, imageFile.originalType);
// Encode with the selected format and quality
const encodedBuffer = await encodeImage(imageData, compressFormat, quality);
// Create blob and object URL for the compressed image
const format = COMPRESS_FORMATS.find(f => f.value === compressFormat) || COMPRESS_FORMATS[2];
const mimeType = `image/${format.value}`;
const compressedBlob = new Blob([encodedBuffer], { type: mimeType });
const compressedUrl = URL.createObjectURL(compressedBlob);
return {
...imageFile,
compressedUrl,
compressedFileName: `${originalName}_compressed.${format.ext}`,
compressedSize: compressedBlob.size,
};
} catch (error) {
console.error("Compression failed:", error);
return imageFile;
}
};
6. User Interface Controls
The compression tool provides flexible options:
const COMPRESS_FORMATS = [
{ value: "jpeg", label: "JPEG", ext: "jpg" },
{ value: "png", label: "PNG", ext: "png" },
{ value: "webp", label: "WebP", ext: "webp" },
{ value: "avif", label: "AVIF", ext: "avif" },
];
// Format selection
<select
value={compressFormat}
onChange={(e) => setCompressFormat(e.target.value)}
>
{COMPRESS_FORMATS.map((f) => (
<option key={f.value} value={f.value}>{f.label}</option>
))}
</select>
// Quality slider (1-100)
<input
type="range"
min="1"
max="100"
value={quality}
onChange={(e) => setQuality(parseInt(e.target.value))}
/>
Service Worker for WASM Caching
The WebAssembly files for the codecs are cached by a Service Worker for faster subsequent loads:
const CACHE_NAME = 'compress-wasm-cache-v1';
const ESM_SH = 'https://esm.sh';
const WASM_FILES = [
`${ESM_SH}/@jsquash/jpeg@1.6.0/es2022/mozjpeg_enc.wasm`,
`${ESM_SH}/@jsquash/jpeg@1.6.0/es2022/mozjpeg_dec.wasm`,
`${ESM_SH}/@jsquash/png@2.0.0/es2022/png.wasm`,
`${ESM_SH}/@jsquash/webp@1.0.0/es2022/webp.wasm`,
`${ESM_SH}/@jsquash/webp@1.0.0/es2022/webp_enc.wasm`,
`${ESM_SH}/@jsquash/avif@1.0.0/es2022/avif.wasm`,
`${ESM_SH}/@jsquash/avif@1.0.0/es2022/avif_enc.wasm`,
];
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Only handle WASM file requests from esm.sh
if (url.hostname === 'esm.sh' && url.pathname.endsWith('.wasm')) {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then((networkResponse) => {
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return networkResponse;
});
})
);
}
});
Processing Flow
Key Technologies Used
| Technology | Purpose |
|---|---|
| @jsquash/jpeg | JPEG decode/encode using MozJPEG |
| @jsquash/png | PNG decode/encode using libpng |
| @jsquash/webp | WebP decode/encode using libwebp |
| @jsquash/avif | AVIF decode/encode using libavif |
| WebAssembly | High-performance codec execution |
| Service Worker | Cache WASM files for offline use |
| Blob URLs | Handle compressed images without server |
Supported Formats
| Format | Use Case |
|---|---|
| JPEG | Best for photos, good compression |
| PNG | Best for graphics, lossless |
| WebP | Modern format, excellent compression |
| AVIF | Latest format, best compression ratio |
Performance Characteristics
- Codec Loading: ~2-5 seconds for all four codec families
- Compression Speed: 1-3 seconds per image depending on size and format
- Memory Usage: Typically 2-3x the image size
- WASM Caching: After first load, codecs work offline
Compression Ratio Examples
| Original | Format | Quality | Typical Savings |
|---|---|---|---|
| 5MB JPEG | WebP | 75% | 60-80% |
| 3MB PNG | WebP | 75% | 70-90% |
| 4MB JPEG | AVIF | 75% | 80-95% |
Conclusion
Browser-based image compression is a practical solution that protects user privacy while providing excellent performance. Using WebAssembly-based codecs from the @jsquash family enables native-level compression quality entirely in the browser. The Service Worker ensures fast subsequent loads by caching the WASM modules.
Try it yourself at Free Image Tools
Note: When sharing or linking to our tool, you can help us understand your traffic source by linking from your site. This helps us improve our services! When you link to us, please include a Referer header pointing to your website so we can track where our visitors come from.
Experience the power of browser-based image compression. No upload required - your images stay on your device!


Top comments (0)