A large photo being resized into a row of sharp thumbnails on a corkboard, with a gray-blue cat watching from a windowsill.

Pica: Crisp In-Browser Image Resizing Without the Mush

The Gray Cat
The Gray Cat

Browsers have always been able to resize images. Hand a <canvas> a 4000px photo, call drawImage() at a smaller size, and you get a thumbnail. The trouble is that thumbnail usually looks terrible: jagged edges, shimmering moiré patterns, and a soft, mushy quality that screams "I was scaled down badly." Native canvas downscaling skips the proper low-pass filtering step, so fine detail collapses into noise instead of dissolving cleanly.

Pica exists to fix exactly that. It does high-quality image resampling entirely on the client, using real convolution filters (Lanczos and friends) plus an optional sharpening pass, and it runs the heavy math on WebAssembly inside web workers so your UI never freezes. The result is thumbnails and avatars that actually look good, generated in the browser with zero server cost. It is a favorite for shrinking phone photos before upload, building thumbnail grids, and taking image-processing load off your backend entirely.

Why Pica Earns Its Keep

The whole point of Pica is quality, but it brings a few other things to the table that make it practical for real apps.

  • Proper resampling filters. Pica uses windowed-sinc convolution (Lanczos) and Magic Kernel Sharp by default, which preserve detail and suppress aliasing far better than the bilinear interpolation you get for free.
  • WebAssembly speed. The convolution math is compiled to WASM for near-native performance, with a pure-JS fallback that works anywhere canvas and typed arrays exist.
  • Web worker offloading. Heavy resizes run off the main thread in an auto-sized worker pool, so the page stays responsive while a big image crunches.
  • Built-in sharpening. An unsharp mask restores the crispness that downscaling inevitably softens, mirroring the controls you would find in Photoshop.
  • Memory-aware tiling. Large images are processed in tiles to stay within tight canvas memory budgets, which matters a lot on iOS Safari.
  • A lean footprint. Only two runtime dependencies, glur and multimath, keep the dependency tree small.

Pica autoselects the best available technology at runtime and degrades gracefully. The default feature set is ['js', 'wasm', 'ww'], and you can opt into createImageBitmap or request everything if you want.

Getting It Into Your Project

Install with your package manager of choice.

npm install pica
yarn add pica

The package ships ESM and CommonJS builds along with the bundled WASM blob, so there is nothing extra to configure for a typical bundler setup.

Your First Resize

As of v10, Pica's default export is a factory function. Call it once to create a resizer, then reuse that instance. There is also a named Pica class export if you prefer the constructor form.

import pica from "pica";

const resizer = pica();

// or, the class form
import { Pica } from "pica";
const resizer2 = new Pica();

A resize takes a source (a Canvas, Image, or ImageBitmap) and a destination canvas that you have already sized to your target dimensions. Pica writes the resampled pixels into the destination and resolves with it.

const from = document.querySelector("#source") as HTMLImageElement;
const to = document.querySelector("#target") as HTMLCanvasElement;

to.width = 200;
to.height = 200;

resizer.resize(from, to).then((result) => {
  console.log("resize done!", result);
});

That single call already beats native canvas scaling, because Pica is running a Magic Kernel Sharp 2013 filter under the hood rather than the browser's cheap interpolation. The destination canvas is the actual element you sized, so whatever dimensions you set are what you get.

One important habit: create exactly one resizer and reuse it. Each call to pica() spins up worker infrastructure, and v10 no longer shares worker pools between instances, so creating a new one per image is genuinely wasteful.

Shrink, Then Upload

The most common real-world use of Pica is trimming a large photo down before sending it to a server. After resizing into a canvas, toBlob() gives you a promise-based, polyfilled stand-in for the native canvas.toBlob(), ready to drop into a FormData upload.

resizer
  .resize(from, to)
  .then((result) => resizer.toBlob(result, "image/jpeg", 0.9))
  .then((blob) => {
    console.log("resized to canvas and created blob!", blob);
  });

Wiring that into a file input gives you a complete client-side upload optimizer. The user picks a photo, you load it, resize it to a sane resolution, and upload a fraction of the original bytes.

async function optimizeAndUpload(file: File) {
  const bitmap = await createImageBitmap(file);

  const to = document.createElement("canvas");
  to.width = 1280;
  to.height = Math.round((1280 / bitmap.width) * bitmap.height);

  await resizer.resize(bitmap, to);
  const blob = await resizer.toBlob(to, "image/jpeg", 0.9);

  const body = new FormData();
  body.append("photo", blob, "photo.jpg");
  await fetch("/upload", { method: "POST", body });
}

A modern phone photo can easily be several megabytes; pushing it through this flow before upload can cut the payload dramatically while still looking sharp on screen. Just note that Pica does not handle JPEG EXIF orientation, so a photo shot in portrait may come out rotated. If that matters, the same author maintains image-blob-reduce, a friendly wrapper around Pica that applies orientation for you.

Dialing In Sharpness

Downscaling softens an image no matter how good the filter is, simply because you are throwing away pixels. Pica's unsharp mask brings the crispness back, controlled by three options that behave just like a Photoshop USM.

resizer.resize(from, to, {
  unsharpAmount: 80,
  unsharpRadius: 0.6,
  unsharpThreshold: 2,
});

unsharpAmount is the strength (zero disables it, with 100 to 200 being a typical range), unsharpRadius is the Gaussian blur radius the mask is built from (usually 0.5 to 2.0), and unsharpThreshold only sharpens pixels that differ from their neighbors by more than the given amount, which keeps flat areas like skies from getting noisy. A little goes a long way; aggressive amounts produce crunchy halos around edges.

Choosing a Filter

The resampling filter is what determines the actual quality of the downscale. The v10 default is mks2013 (Magic Kernel Sharp 2013), which resizes and sharpens in a single pass, but you can pick others depending on the speed-versus-quality tradeoff you want.

resizer.resize(from, to, { filter: "lanczos3" });

The available filters, roughly from fastest to highest quality, are box, hamming, lanczos2, and lanczos3. Lanczos is the classic windowed-sinc filter that strikes an excellent balance of detail preservation and aliasing control, which is why it is the go-to for serious downscaling. If you used the old numeric quality argument in v9 or earlier, note that v10 removed it entirely in favor of filter (the old 0 through 3 presets mapped onto box, hamming, lanczos2, and lanczos3 respectively).

Tuning the Engine and Cancelling Work

The factory and constructor accept a config object that lets you shape how Pica uses the machine. The tile size (default 1024) controls region processing for large images, concurrency caps the worker pool (it defaults to your detected CPU count, capped at 4), features selects which technologies to use, and idle sets how long workers stick around before being released.

const resizer = pica({
  tile: 1024,
  concurrency: 4,
  features: ["js", "wasm", "ww"],
});

Individual resizes can also be cancelled, which is handy when a user navigates away or picks a different file mid-process. Pass a cancelToken, which is simply a promise; when it rejects, the resize aborts.

let cancel: (reason?: unknown) => void = () => {};
const cancelToken = new Promise((_, reject) => {
  cancel = reject;
});

resizer.resize(from, to, { cancelToken }).catch((err) => {
  console.log("resize cancelled or failed:", err);
});

// later, to abort:
cancel(new Error("user navigated away"));

When you are processing several images, resist the urge to fire them all off at once. Pica is CPU-bound work, and running dozens of resizes in parallel will thrash both CPU and memory, especially on mobile. Process them sequentially, one await at a time, and you get smooth, predictable behavior.

Worker Files and Bundler-Friendly Builds

By default Pica bundles its web worker as a string inside the combined build, which sidesteps side effects in external bundlers. But some setups, particularly strict Content Security Policy environments, prefer a separate worker file rather than an inlined string. v10 added split builds and a workerURL option for exactly this.

import createPica from "pica/dist/pica_main.mjs";

const resizer = createPica({
  workerURL: new URL("pica/dist/pica_worker.js", import.meta.url),
});

This keeps the worker as a real, fetchable asset that your bundler and CSP can reason about cleanly. v10.0.1 also added export subpaths like pica/pica_main and pica/pica_worker, plus an ESM build and a feature-detection module, making it much friendlier to modern toolchains than the older releases.

Knowing Where Pica Stops

Pica is sharp about its scope, and it is worth being honest about the edges. It works on 8-bit-per-channel sRGB data rather than a linear-light pipeline, so there is a minor quality loss compared to professional server tools. For genuinely pro-grade resizing the author explicitly points you toward sharp on Node, which is libvips-based, native, and runs in linear light. Pica's entire advantage is that it runs on the client with no server cost, so the calculus is about where the work should happen rather than which is strictly better.

In a typical stack, Pica is the resampling engine. Pair it with image-blob-reduce when you need EXIF orientation handled, or with browser-image-compression when your real goal is hitting a byte budget like "this file must be under 2 MB." Reach for sharp when you can afford to do the work on a server and want maximum throughput and fidelity. For everything in between, where you want sharp, alias-free thumbnails generated right in the browser without melting the main thread, Pica is the one that gets it right.

Wrapping Up

Pica is one of those quietly essential libraries: a focused tool that does a single thing, resampling images well, and does it better than the platform default. With a class-based v10 API, WebAssembly under the hood, web workers keeping the UI alive, and sensible controls for filters and sharpening, it turns the often-ugly problem of in-browser resizing into a solved one. Create a single resizer, feed it a canvas and a target size, sprinkle in a touch of unsharp mask, and you get thumbnails that look like someone cared. For client-side image work where the visual result matters, Pica is hard to beat.