logo
HomeSkillsCareerPortfolioBlogContacts
  • Home
  • Skills
  • Career
  • Portfolio
  • Blog
  • Contacts

Blurkit

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

BlurHashThumbHashWASM
post_image
Source Code
Live Demo
Made with ❤️ by Okazakee | Source Code
CMS|Privacy Policy
16/05/2026
1
12
Source Code
Live Demo

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 sharp for native-speed decode
  • Deno, edge, and WASM use a companion WASM codec package (optional peer dependency)
  • Browser and edge prefer native ImageDecoder plus OffscreenCanvas, falling back to WASM
  • Each backend implements the same encode contract

Cache and Batch Design:

  • Cache interface is deliberately low-level (get and set) with caller-owned invalidation
  • encodeMany is fail-fast and encodeManySettled returns 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 (get and set) 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

RuntimeFile PathBufferBlob/FileURL
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.