logo

Blurkit

Generazione universale di placeholder per immagini su runtime Node, Deno, browser, edge, Cloudflare e WASM. 🖼️

BlurHashThumbHashWASM
post_image
16/05/2026
0
—

La Storia

blurkit nasce da una semplice frustrazione. Ogni volta che costruivo un'applicazione che caricava immagini, volevo quelle anteprime sfocate che rendono l'esperienza utente più raffinata. BlurHash e ThumbHash sono gli algoritmi di riferimento per questo scopo, ma integrarli in ambienti diversi come Node, Deno e browser richiedeva librerie specifiche per ogni piattaforma. Il codice non era mai portabile e la configurazione era sempre macchinosa. Così ho costruito la libreria che avrei voluto esistesse: una API di encode che funziona ovunque, con i dettagli interni del runtime gestiti sotto il cofano.

Cosa Lo Rende Speciale

Immagina di scrivere la stessa chiamata encode per un job batch lato server e per un'anteprima di upload nel browser. Questo è il punto centrale. La libreria espone una singola funzione encode su sette runtime tra cui Node, Bun, Deno, browser, edge workers, Cloudflare Workers e WASM standalone. Astrae il decode delle immagini, l'elaborazione dei pixel e il calcolo dell'hash dietro un'interfaccia coerente. Cambia il runtime, mantieni il codice. ✨

L'Arte Tecnica

Funzionalità Principali

  • 🔄 API Universale: Una funzione encode per Node, Deno, browser, edge e WASM
  • 📦 Doppio Algoritmo: Supporto per BlurHash e ThumbHash con interfacce identiche
  • ⚙️ Codec WASM Opzionali: Backend di decode leggero distribuito come pacchetto opzionale separato
  • 🧩 Elaborazione Batch: API batch fail-fast e a successo parziale per pipeline di manifest
  • 🔧 CLI e Generazione Manifest: Genera placeholder da file locali con supporto glob

Il Segreto del Successo

Astrazione del Runtime:

Ogni runtime espone la stessa API pubblica da entrypoint dedicati (blurkit/node, blurkit/deno, blurkit/browser, ecc.). Le conditional exports in package.json risolvono l'implementazione corretta al momento dell'import. Non c'è configurazione, environment sniffing o controllo a runtime.

Strategia dei Codec:

  • Node e Bun usano sharp per decode a velocità nativa
  • Deno, edge e WASM usano un pacchetto companion di codec WASM (dipendenza peer opzionale)
  • Browser e edge preferiscono ImageDecoder e OffscreenCanvas nativi, con fallback a WASM
  • Ogni backend implementa lo stesso contratto encode

Design Cache e Batch:

  • L'interfaccia cache è volutamente di basso livello (get e set) con invalidazione a carico del chiamante
  • encodeMany è fail-fast e encodeManySettled restituisce envelope di risultato per singolo elemento
  • Una CLI integrata wrappa il pipeline per elaborazione file locali e generazione manifest

Sfide e Soluzioni

Il Puzzle Multi-Runtime 🧩

  • Sfida: Ogni runtime ha capacità di decode, modelli di threading e accesso al filesystem diversi
  • Soluzione: Progettato un layer di risoluzione dell'input pluggable e conditional exports che selezionano il backend corretto in fase di build
  • Risultato: Uno stesso specifier di import funziona identicamente su sette runtime

La Separazione dei Codec WASM 🔌

  • Sfida: I codec WASM aggiungono dimensione al bundle che i consumatori browser e edge non necessitano
  • Soluzione: Estratto il decode WASM in una dipendenza peer opzionale separata (blurkit-wasm-codecs)
  • Risultato: La libreria core rimane leggera e il supporto WASM si carica solo quando necessario

Il Contratto di Cache 💾

  • Sfida: Le strategie di invalidazione della cache sono specifiche del dominio e non esiste una soluzione universale
  • Soluzione: Mantenuta l'interfaccia cache minimale (get e set) e distribuito esattamente un helper in memoria
  • Risultato: I chiamanti gestiscono la propria politica di persistenza e la libreria rimane non opinionata

Approfondimento Tecnico

Risoluzione tramite Conditional Exports

Il cuore della strategia multi-runtime è nella mappa di export:

Code
{
  ".": {
    "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" }
  }
}

Ogni entrypoint importa solo i moduli specifici del runtime di cui ha bisogno. L'entrypoint Deno evita completamente i built-in di Node.js. L'entrypoint browser utilizza Web API. L'entrypoint edge è ottimizzato per ambienti worker.

Pipeline di Encode

Una vista semplificata del flusso:

Code
async function encode(input: Input, options: Options): Promise<BlurResult> {
  const reader = resolveInputReader(input)    // consapevole del runtime
  const raw = await reader.decode(options)     // specifico del codec
  const hash = computeHash(raw, options)       // BlurHash o ThumbHash
  const dataURL = await renderPlaceholder(raw, options) // specifico del formato
  return { algorithm, hash, dataURL, width, height }
}

Risoluzione dell'Input per Runtime

RuntimePercorso FileBufferBlob/FileURL
Node✅✅❌✅
Deno✅✅❌✅
Browser❌❌✅✅ (CORS)
Cloudflare❌❌❌✅

Dietro le Quinte

Questo progetto è stato un esercizio di astrazione disciplinata. La parte più difficile non è stata implementare BlurHash o ThumbHash. Quegli algoritmi sono ben documentati. La vera sfida è stata progettare un pipeline che potesse degradare gracefulmente attraverso ambienti diversi senza far trapelare complessità al chiamante. Ogni vincolo specifico del runtime è esposto nell'API piuttosto che nascosto: Node richiede sharp, Deno richiede il pacchetto codec WASM, il browser rifiuta percorsi file e dipende da CORS. La libreria non finge che queste differenze non esistano. Le rende semplicemente gestibili.