Compact image placeholders: a short string in place of an image that rebuilds a blurred preview with the right aspect ratio and, when needed, transparency in a fraction of a millisecond. 49% lower error than BlurHash and 20% lower than ThumbHash.

While an image loads, the visitor sees an empty frame and the page around it jumps. HazeHash turns an image into a short string that is easy to keep next to your data, and draws a blurred preview of the right shape and colour from it.
Instead of grey rectangles while the photos load, the API returns a short 38-character string for every image, and each card shows a blurred frame of the right proportions at once, with no layout jump.
The hash is computed once when the image is uploaded and stored in a column next to the record: 28 bytes as binary, about 40 characters as text. Decoding the hash into a preview happens on the client, and the image itself does not have to be downloaded for it.
The server sends only the average colour and the aspect ratio, and the blurred frame is drawn in the browser, so the markup before and after hydration is identical and there is no mismatch warning.
Hashes for every file in a folder are collected during the site build and cached by file size and modification time. Unchanged images are not encoded again, and a one-off job needs only a terminal command.

At 28 bytes the mean perceptual error is 49% lower than BlurHash and 20% lower than ThumbHash, measured on a set of 513 images: photos, screenshots, graphics and images with transparency.

The size is set by a budget: 16 bytes (22 characters) for the most compact hash, 28 (38 characters) by default, 36 and more for detail. The budget is a ceiling, and simple images come out smaller.

The string already carries the aspect ratio and, when it is needed, the alpha channel, so the space for the image is reserved without extra fields in your data.

The core never touches the DOM, window or Buffer, and runs in browsers, Node.js 20+ and Cloudflare Workers. The same input and options always give the same bytes, and the version 1 format will not change: a hash stored today decodes the same way in every future release.

A preview is rebuilt in about 0.3 ms. The decoder is about 2.6 KB gzip and the encoder about 6.3 KB, and a browser downloads only the part you import.

A Vue 3 component and a Nuxt module with build-time hashes, plus a hazehash command that hashes files and folders, draws previews and explains a string.
encodeToString() takes RGBA pixels and returns a hash of at most 28 bytes. decode() turns it back into a small blurred preview. Encode on the server or at build time, and ship only the string.
import { encodeToString } from 'hazehash/encode'
import { decode } from 'hazehash'
// rgba: straight sRGB RGBA pixels, for example an ImageData from a canvas
const hash = encodeToString({ data: rgba, width, height })
// "Ed7UwRWKKv5znm6a7sC1tziHDNpMuCikxrYpIg": 28 bytes, 38 characters
const preview = decode(hash)
// { width: 32, height: 25, data: Uint8ClampedArray(3200) }Reading the average colour and the aspect ratio only touches the header of the hash, so the box gets its colour and shape at once, and the blurred frame is drawn into a canvas when the script runs.
import { drawToCanvas } from 'hazehash/canvas'
import { getAspectRatio, getAverageColor } from 'hazehash'
const frame = document.querySelector('.frame') as HTMLElement
const { r, g, b } = getAverageColor(hash)
frame.style.backgroundColor = `rgb(${r}, ${g}, ${b})`
frame.style.aspectRatio = String(getAspectRatio(hash))
drawToCanvas(hash, document.querySelector('canvas') as HTMLCanvasElement)