DEV Community

popo suke
popo suke

Posted on

I Built 18 Browser-Based Tools with Zero Server Calls — Here's How

The Problem

Every time you upload a PDF or image to an online tool, you're trusting a random server with your files. What if the tool could run entirely in your browser instead?

I built two sites that do exactly that:

  • PDF Tools — 8 PDF tools (split, merge, compress, sign, and more)

  • Image Tools — 10 image tools (compress, resize, crop, AI background removal, and more)


Zero file uploads. Everything runs in JavaScript on your device.

Tech Stack

React 19 + Vite 7
Enter fullscreen mode Exit fullscreen mode

PDF side: pdf-lib, pdfjs-dist, ExcelJS, jsPDF
Image side: Canvas API, @imgly/background-removal, TensorFlow.js + ESRGAN

1. Code Splitting: 2,775KB → 248KB Initial Load

The first build was huge. Every library loaded upfront, even if the user only needed one tool.

Fix: React.lazy() + Vite manualChunks

// Each tool loads only when selected
const SplitMode = lazy(() => import('./components/SplitMode'));
const MergeMode = lazy(() => import('./components/MergeMode'));
const CompressMode = lazy(() => import('./components/CompressMode'));
Enter fullscreen mode Exit fullscreen mode
// vite.config.js — separate heavy libs into their own chunks
manualChunks: {
  'bg-removal': ['@imgly/background-removal'],  // ONNX Runtime
  'tfjs': ['@tensorflow/tfjs'],                  // ~2MB
  'upscaler': ['upscaler', '@upscalerjs/esrgan-medium'],
  'pdfjs': ['pdfjs-dist'],
}
Enter fullscreen mode Exit fullscreen mode

Result: 91% reduction in initial load. Users only download what they use.

2. Canvas API Does More Than You Think

Most of Image Tools runs on Canvas API alone — no image processing libraries needed.

Compression — just toBlob() with quality parameter:

canvas.toBlob(blob => {
  // compressed blob ready
}, 'image/jpeg', 0.7); // 70% quality
Enter fullscreen mode Exit fullscreen mode

Free-angle rotation — the tricky part is calculating the expanded canvas size:

const rad = angle * Math.PI / 180;
const sin = Math.abs(Math.sin(rad));
const cos = Math.abs(Math.cos(rad));

// Canvas must be large enough to fit the rotated image
canvas.width = Math.ceil(w * cos + h * sin);
canvas.height = Math.ceil(w * sin + h * cos);

ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(rad);
ctx.drawImage(img, -w / 2, -h / 2);
Enter fullscreen mode Exit fullscreen mode

CSS Filters baked into images — apply filters and export as a file:

ctx.filter = `brightness(120%) contrast(110%) saturate(130%) grayscale(50%)`;
ctx.drawImage(img, 0, 0);
// Now export — filters are permanently applied to the pixels
Enter fullscreen mode Exit fullscreen mode

3. Custom Binary Encoders for BMP, GIF, and ICO

Canvas toBlob() supports JPEG, PNG, WebP, and AVIF. But not BMP, GIF, or ICO.

I wrote custom encoders from scratch.

BMP — requires BGR byte order and bottom-up pixel storage:

// RGBA from Canvas → BGR for BMP (plus row padding to 4-byte boundary)
const rowSize = Math.ceil((width * 3) / 4) * 4;

for (let y = height - 1; y >= 0; y--) { // bottom-up
  for (let x = 0; x < width; x++) {
    const src = (y * width + x) * 4;
    bytes[dst]     = data[src + 2]; // B
    bytes[dst + 1] = data[src + 1]; // G
    bytes[dst + 2] = data[src];     // R
  }
}
Enter fullscreen mode Exit fullscreen mode

GIF — includes LZW compression and 256-color quantization:

// 1. Quantize colors to fit 256-color palette
// 2. Build color lookup table
// 3. LZW encode with dictionary reset at 4096 entries
// 4. Pack into GIF89a format
Enter fullscreen mode Exit fullscreen mode

Writing a GIF encoder was the most challenging part — LZW compression with variable-width codes and dictionary management is not trivial.

4. AI in the Browser — No Server Needed

Background removal with ONNX Runtime (~50MB model, cached after first use):

const { removeBackground } = await import('@imgly/background-removal');

const blob = await removeBackground(file, {
  progress: (key, current, total) => {
    setProgress(Math.round((current / total) * 100));
  },
});
// Returns transparent PNG
Enter fullscreen mode Exit fullscreen mode


AI upscaling with TensorFlow.js + ESRGAN:

const upscaler = new Upscaler({ model });
const result = await upscaler.upscale(img, {
  patchSize: 64,  // Process in 64px tiles to avoid GPU OOM
  padding: 4,     // Overlap between tiles to prevent seams
});
Enter fullscreen mode Exit fullscreen mode

The patchSize: 64 trick is critical — without it, large images crash the browser's GPU memory.

5. Detecting AI-Generated Image Metadata

The Metadata tool parses PNG binary chunks to detect AI generation parameters:

// PNG files contain tEXt and iTXt chunks with metadata
// AI tools embed their parameters here:
// - "parameters" → Stable Diffusion (AUTOMATIC1111)
// - "prompt" / "workflow" → ComfyUI
// - "comment" / "source" → NovelAI
Enter fullscreen mode Exit fullscreen mode

This required writing a custom PNG chunk parser that reads the binary format byte by byte.

6. Lightweight i18n Without Libraries

Both sites support 5 languages (EN, JA, VI, ID, ZH) with a custom 30-line implementation:

const t = (key, ...args) => {
  const str = translations[lang]?.[key] || translations.en[key] || key;
  return str.replace(/\{(\d+)\}/g, (_, i) => args[Number(i)] ?? '');
};

// Auto-detect browser language on first visit
const browserLang = (navigator.language || '').toLowerCase();
if (browserLang.startsWith('ja')) return 'ja';
if (browserLang.startsWith('vi')) return 'vi';
// ...
Enter fullscreen mode Exit fullscreen mode

No react-i18next, no formatjs. Just a plain object lookup with placeholder substitution.

Cost: $1.75/month

Item Cost
Hosting (Vercel) x2 Free
Domains (Cloudflare) x2 ~$21/year
SSL Free (Cloudflare)
Libraries All OSS
AI models Browser-delivered
Total ~$1.75/month

Since all processing is client-side, server costs don't scale with users.

Links

If you're building browser-based tools, I hope some of these techniques are useful. Happy to answer questions!

Top comments (0)