DEV Community

monkeymore studio
monkeymore studio

Posted on

Building a Browser-Based Image Compression Tool

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 
  };
}
Enter fullscreen mode Exit fullscreen mode

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!;
};
Enter fullscreen mode Exit fullscreen mode

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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))}
/>
Enter fullscreen mode Exit fullscreen mode

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;
        });
      })
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

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

  1. Codec Loading: ~2-5 seconds for all four codec families
  2. Compression Speed: 1-3 seconds per image depending on size and format
  3. Memory Usage: Typically 2-3x the image size
  4. 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)