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

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
sharpper decode a velocità nativa - Deno, edge e WASM usano un pacchetto companion di codec WASM (dipendenza peer opzionale)
- Browser e edge preferiscono
ImageDecodereOffscreenCanvasnativi, con fallback a WASM - Ogni backend implementa lo stesso contratto encode
Design Cache e Batch:
- L'interfaccia cache è volutamente di basso livello (
geteset) con invalidazione a carico del chiamante encodeManyè fail-fast eencodeManySettledrestituisce 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 (
geteset) 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
Copiato!
{
".": {
"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
Copiato!
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
| Runtime | Percorso File | Buffer | Blob/File | URL |
|---|---|---|---|---|
| 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.