Blurkit
Universal image placeholder generation across Node, Deno, browser, edge, Cloudflare, and WASM runtimes. 🖼️

The Story
blurkit started with a simple frustration. Every time I built an app that loaded images, I wanted those blurry placeholder previews that make the experience feel polished. BlurHash and ThumbHash are the go-to algorithms for that, but hooking them up across different environments like Node, Deno, and the browser meant wiring platform-specific libraries for each target. The code was never portable and the setup was always fiddly. So I built the library I wished existed: one encode API that works everywhere, with the runtime internals handled under the hood.
What Makes It Special
Imagine writing the same encode call for a server-side batch job and a browser upload preview. That is the whole point. The library exposes a single encode function across seven runtimes including Node, Bun, Deno, browser, edge workers, Cloudflare Workers, and standalone WASM. It abstracts image decode, pixel processing, and hash computation behind a consistent interface. Swap the runtime, keep the code. ✨
The Technical Craft
Core Features
- 🔄 Universal API: One encode function across Node, Deno, browser, edge, and WASM
- 📦 Dual Algorithm: BlurHash and ThumbHash support with identical interfaces
- ⚙️ Optional WASM Codecs: Lightweight decode backend shipped as a separate optional package
- 🧩 Batch Processing: fail-fast and partial-success batch APIs for manifest pipelines
- 🔧 CLI and Manifest Generation: Generate placeholders from local files with glob support
The Secret Sauce
Runtime Abstraction:
Each runtime exposes the same public API from dedicated entrypoints (blurkit/node, blurkit/deno, blurkit/browser, etc.). Conditional exports in package.json resolve the right implementation at import time. There is no configuration, no environment sniffing, and no runtime checks.
Codec Strategy:
- Node and Bun use
sharpfor native-speed decode - Deno, edge, and WASM use a companion WASM codec package (optional peer dependency)
- Browser and edge prefer native
ImageDecoderplusOffscreenCanvas, falling back to WASM - Each backend implements the same encode contract
Cache and Batch Design:
- Cache interface is deliberately low-level (
getandset) with caller-owned invalidation encodeManyis fail-fast andencodeManySettledreturns per-item result envelopes- A bundled CLI wraps the pipeline for local file processing and manifest generation
Challenges and Solutions
The Multi-Runtime Puzzle 🧩
- Challenge: Each runtime has different decode capabilities, threading models, and filesystem access
- Solution: Designed a pluggable input resolution layer and condition-based exports that select the correct backend at build time
- Result: One import specifier works identically across seven runtimes
The WASM Codec Split 🔌
- Challenge: WASM codecs add bundle size that browser and edge consumers do not need
- Solution: Extracted WASM decode into a separate optional peer dependency (
blurkit-wasm-codecs) - Result: Core library stays small and WASM support loads only when required
The Cache Contract 💾
- Challenge: Cache invalidation strategies are domain-specific with no one-size-fits-all solution
- Solution: Kept cache interface minimal (
getandset) and shipped exactly one in-memory helper - Result: Callers own their persistence policy and the library stays unopinionated
Technical Deep Dive
Conditional Export Resolution
The core of the multi-runtime strategy is in the export map:
Code
Copied!
{
".": {
"deno": { "import": "./dist/root-deno.js" },
"browser": { "import": "./dist/root-browser.js" },
"worker": { "import": "./dist/root-edge.js" },
"node": { "import": "./dist/root-node.js" },
"default": { "import": "./dist/root-edge.js" }
}
}Each entrypoint imports only the runtime-specific modules it needs. The Deno entrypoint avoids Node.js builtins entirely. The browser entrypoint uses Web APIs. The edge entrypoint is optimised for worker environments.
Encode Pipeline
A simplified view of the flow:
Code
Copied!
async function encode(input: Input, options: Options): Promise<BlurResult> {
const reader = resolveInputReader(input) // runtime-aware
const raw = await reader.decode(options) // codec-specific
const hash = computeHash(raw, options) // BlurHash or ThumbHash
const dataURL = await renderPlaceholder(raw, options) // format-specific
return { algorithm, hash, dataURL, width, height }
}Input Resolution by Runtime
| Runtime | File Path | Buffer | Blob/File | URL |
|---|---|---|---|---|
| Node | ✅ | ✅ | ❌ | ✅ |
| Deno | ✅ | ✅ | ❌ | ✅ |
| Browser | ❌ | ❌ | ✅ | ✅ (CORS) |
| Cloudflare | ❌ | ❌ | ❌ | ✅ |
Behind the Scenes
This project was an exercise in disciplined abstraction. The hardest part was not implementing BlurHash or ThumbHash. Those algorithms are well-documented. The real challenge was designing a pipeline that could degrade gracefully across environments without leaking complexity to the caller. Every runtime-specific constraint is surfaced in the API rather than hidden: Node needs sharp, Deno needs the WASM codec package, the browser rejects file paths and depends on CORS. The library does not pretend these differences do not exist. It just makes them manageable.