Плейсхолдеры
Плейсхолдер HazeHash
HazeHash — предпочтительный плейсхолдер: строка в 7–48 байт (по умолчанию 28) из пакета hazehash, которая декодируется в размытое превью с правильными пропорциями и, если у изображения есть прозрачность, с альфа-каналом. При том же размере перцептивная ошибка у него ниже, чем у BlurHash и ThumbHash.
<VImage
src="/photo.jpg"
alt="Photo"
:width="1200"
:height="800"
hazehash="Ed7UwRWKKv5znndNd2Ba284jhm2TLgpUMa0UkQ"
/>Если задано несколько плейсхолдеров, побеждает hazehash: hazehash → blurhash → thumbhash → placeholder. То же для записи манифеста, image.hazehash, v-lazy-img и useBackgroundImage().
hazehash — опциональная peer-зависимость, которая подгружается по требованию, только когда хеш действительно показывается:
npm install hazehashБез пакета <VImage> один раз пишет предупреждение и откатывается к следующему имеющемуся плейсхолдеру (blurhash, thumbhash, placeholder, placeholderColor).
Инструменты сборки делают его сами. Если hazehash установлен, он — mode по умолчанию у команды placeholders, плагина Vite и generate; mode: 'blurhash' сохраняет прежнее поведение. Размер строки задаёт tuning.budget (байты, 7–48, по умолчанию 28) — см. Настройка хешей.
Размер превью
Превью получает тот же размер, что и изображение, вместо которого оно показано. До загрузки <VImage> рендерит <img> с теми же атрибутами width и height и теми же классами, что и у загруженной картинки (прозрачное изображение того же размера в src, хеш в фоне), поэтому браузер размеряет их по одним и тем же правилам и одному и тому же CSS: width: 50%, фиксированная height, max-width, родитель, сжимающийся по содержимому. Пока файл скачивается, настоящая <img> сохраняет этот размер через contain-intrinsic-size до прихода первых байт. В JavaScript ничего не считается, и при появлении фото ничего не прыгает. Инлайн задаётся только aspect-ratio из width/height; передайте <VImage> эти два пропа (или image, или запись манифеста), чтобы место было зарезервировано.
Плейсхолдер ThumbHash
ThumbHash — современная альтернатива BlurHash с поддержкой альфа-канала, лучшим визуальным качеством на фотографиях и более короткой строкой хэша. Декодируется в PNG data URL.
Проп thumbhash — самый простой способ:
<VImage src="/photo.png" alt="Photo with transparency" thumbhash="3OcRJYB4d3h/iIeHeEh3eIhw+j5n" />VImage автоматически декодирует хэш и использует его как blur-up плейсхолдер. Ручное декодирование не нужно.
Использование декодера напрямую (для кастомной разметки или v-lazy-img):
import { decodeThumbHash } from '@macrulez/vue-image-kit'
const dataUrl = decodeThumbHash('3OcRJYB4d3h/iIeHeEh3eIhw+j5n')
// → 'data:image/png;base64,...'Средний цвет — самый дешёвый плейсхолдер из всех (декодируется из заголовка, без пикселей):
import { thumbHashToAverageRGBA, thumbHashToAverageColor } from '@macrulez/vue-image-kit'
thumbHashToAverageRGBA('3OcRJYB4d3h/iIeHeEh3eIhw+j5n')
// → { r, g, b, a } (каждый канал 0–1)
thumbHashToAverageColor('3OcRJYB4d3h/iIeHeEh3eIhw+j5n')
// → 'rgba(150, 146, 104, 1.000)' — вставляйте прямо в background-colorИли доверьте это VImage через placeholder-mode="color" (см. Пропы).
Проп placeholder — эквивалент, когда data URL уже есть:
<VImage
src="/photo.png"
alt="Photo"
:placeholder="decodeThumbHash('3OcRJYB4d3h/iIeHeEh3eIhw+j5n')"
/>Если заданы и thumbhash, и placeholder, приоритет у placeholder.
Генерация хэшей ThumbHash на этапе сборки:
Используйте CLI с флагом --thumbhash (требует thumbhash как dev-зависимость):
npm install thumbhash --save-dev
npx vue-image-kit generate \
--input ./src/images \
--manifest ./src/assets/images.ts \
--thumbhashМанифест будет включать поле thumbhash для каждого изображения наряду с hazehash, blurhash и placeholder.
Или сгенерируйте вручную в Node.js:
import { rgbaToThumbHash } from 'thumbhash'
import sharp from 'sharp'
const { data, info } = await sharp('photo.jpg')
.resize(100, 100, { fit: 'inside' })
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
const hash = rgbaToThumbHash(info.width, info.height, new Uint8Array(data.buffer))
const hashBase64 = Buffer.from(hash).toString('base64')
// Сохраните в БД / манифест, передавайте как проп thumbhashПлейсхолдер Blurhash
<VImage> декодирует строку blurhash внутренне — внешний пакет не нужен. Декодер реализован с нуля по открытой спецификации blurhash.
Передайте blurhash вместе с width и height, чтобы включить blur-плейсхолдер:
<VImage
src="/photo.jpg"
alt="Landscape"
:width="1200"
:height="800"
blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
/>Как это работает:
- На сервере — рендерится обычный
<img loading="lazy">, без какой-либо плейсхолдер-механики (см. Компоновка без обёртки); готовое превью можно добавить к нему черезssrPlaceholder, см. Превью в серверном HTML - На клиенте — вызывается
decodeBlurhash(hash, width, height), пиксели рисуются в невидимый canvas в памяти (он никогда не попадает в DOM) черезImageData, иcanvas.toDataURL()превращает их в CSSbackground-imageпрямо на элементе, который затем покажет фото - Фон остаётся видимым, пока изображение загружается; как только фото декодировано и отрисовано, оно перекрывает фон — мгновенно, без анимации (если явно не включён проп
fadeIn, см. Пропы VImage)
Видимая «размытость» — это увеличение крошечного 32-пиксельного превью через background-size: cover, без отдельного CSS-фильтра размытия.
useBlurhash() — отдельный API для случаев, когда нужен именно настоящий, прикреплённый к DOM <canvas>, который можно, например, рисовать поверх; <VImage> строит свой blurhash-плейсхолдер другим способом.
hazehash всегда побеждает blurhash. Если blurhash задан вместе с LQIP-плейсхолдером placeholder или thumbhash, приоритет имеет blurhash — в этом случае LQIP/ThumbHash blur-up вообще не рендерится. Они взаимоисключающие, а не накладываются друг на друга.
Использование декодера напрямую:
import { decodeBlurhash } from '@macrulez/vue-image-kit'
const pixels = decodeBlurhash('LEHV6nWB2yk8pyo0adR*.7kCMdnj', 32, 32)
// pixels: Uint8ClampedArray<ArrayBuffer> — RGBA, по строкам
const canvas = document.createElement('canvas')
canvas.width = 32
canvas.height = 32
canvas.getContext('2d')!.putImageData(new ImageData(pixels, 32, 32), 0, 0)Генерация строк blurhash:
Декодер включён; хэши генерируются на сервере или на этапе сборки. Это умеет собственный CLI пакета: generate — для папки исходных изображений, placeholders — для использований <VImage>, уже имеющихся в шаблонах. Подойдёт и любой другой инструмент — передайте получившуюся строку в проп blurhash.
LQIP — base64 preview
LQIP (Low Quality Image Placeholder) показывает крошечную размытую версию изображения, пока грузится полное разрешение.
<VImage src="/photo.jpg" alt="Photo" placeholder="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAA..." />Как это работает:
- Base64-строка ставится CSS-фоном (
background-image) на тот же элемент, который затем покажет полное изображение - Когда полное изображение загружается, оно перекрывает фон мгновенно — фон красится под содержимым элемента, поэтому как только фото отрисовано, плейсхолдер физически скрыт (без анимации, если не включён
fadeIn) - До начала загрузки плейсхолдер живёт на отдельном элементе с
aria-hidden="true"— невидим для скринридеров; сам CSS-фон в принципе никогда не попадает в accessibility-дерево независимо от этого атрибута
Генерация LQIP на этапе сборки (пример на Node.js):
import sharp from 'sharp'
const buffer = await sharp('photo.jpg').resize(20).jpeg({ quality: 20 }).toBuffer()
const lqip = `data:image/jpeg;base64,${buffer.toString('base64')}`
// Передайте эту строку как проп placeholderПревью в серверном HTML
По умолчанию сервер отдаёт обычный <img>, а blur появляется, когда отработает JavaScript страницы: HazeHash, BlurHash или ThumbHash нужно декодировать через canvas, а на сервере его нет. Чтобы blur был виден с первой отрисовки, дайте серверу то, что не нужно декодировать, — готовое превью data:image/png;base64,… — и запросите его через ssrPlaceholder:
<VImage src="/hero.webp" alt="Hero" :placeholder="heroPreview" ssr-placeholder />Сервер отрендерит <img … style="background-image:url(data:…);background-size:cover">, и браузер покажет превью сразу. Это включается отдельно для каждой картинки: без ssr-placeholder ничего не меняется, и data-URL в HTML не попадает.
Это же решает и вторую половину проблемы. Обычный серверный <img src="…" loading="lazy"> скачивается встроенной ленивой загрузкой браузера, которая стартует за экран-два до картинки, а до гидрации страница короче, поэтому блок далеко внизу кажется близким. Тяжёлый файл может прийти задолго до того, как нужен, и blur тогда ни на что не влияет. С ssr-placeholder ленивая картинка отдаётся без настоящего src (вместо него прозрачный пиксель), а настоящий <img> лежит в <noscript>, поэтому поисковики и посетители без JavaScript получают полное изображение. После гидрации картинка загружается, когда приближается к области просмотра (rootMargin, по умолчанию 200 px), как любой другой ленивый <VImage>. Нетерпеливая картинка (lazy="false", priority) сохраняет настоящий src, а превью лежит под ней.
Откуда взять превью — все способы явные:
- Проп
placeholder— любой data-URL, который вы сделали сами (см. LQIP). - Импорт на этапе сборки —
import preview from './hero.webp?preview'(одно значение) или?placeholder=…,preview(список); передайте как:placeholder="preview". См. Импорты на этапе сборки. - Реестр —
placeholders: { imports: { preview: true } }заставляет плагин сделать превью для каждого импортируемого изображения и зарегистрировать его под импортированным значением, поэтому в шаблоне достаточно<VImage :src="hero" ssr-placeholder />.preview: ['src/assets/hero/', '**/cover-*.webp']ограничивает это перечисленными директориями, файлами или glob-шаблонами. См. Готовое превью для серверного рендеринга.
Что важно знать:
- Размер. Превью — небольшой PNG; на четырёх картинках 300×300 оно добавило по 3–5 КБ на каждую в HTML (и столько же в бандл, если превью идёт из
imports.preview). Включайте его для картинок на первом экране, а не для всех подряд. - Гидрация. Стиль зависит только от пропсов и реестра, поэтому первый рендер на сервере и на клиенте совпадают; hydration mismatch нет. После гидрации то же превью остаётся под картинкой, пока она не загрузится, без скачка к декодированному blur.
- Зависимость на этапе сборки. Превью рисуется из ThumbHash, поэтому для
imports.previewи?previewна этапе сборки нужен пакетthumbhash— даже еслиmodeравенblurhash. - Прозрачность. Превью — это фон самого изображения, поэтому PNG, WebP или AVIF с прозрачными областями покажет его сквозь себя.
- Не применяется вместе с
placeholderColor,placeholderMode="color"иplaceholderMode="shimmer"— они сознательно выбирают другой плейсхолдер.
Манифест плейсхолдеров
Манифест плейсхолдеров сопоставляет src изображения с данными его плейсхолдера, так что <VImage> получает плейсхолдер и размеры без пропсов на каждом использовании. Его генерирует npx vue-image-kit placeholders, а регистрируется он один раз:
import { VImageKitPlugin } from '@macrulez/vue-image-kit'
import placeholders from './image-placeholders'
app.use(VImageKitPlugin, { placeholders })В Nuxt вместо этого передайте модулю путь к файлу: vueImageKit: { placeholders: './image-placeholders.ts' }.
type PlaceholderManifest = Record<string, PlaceholderEntry>
interface PlaceholderEntry {
hazehash?: string
blurhash?: string
thumbhash?: string
color?: string
width?: number
height?: number
}<VImage> ищет свой src — или fallback источника { avif, webp, fallback }, или image.src — и использует запись так:
hazehash,blurhash,thumbhash,widthиheightприменяются везде, где соответствующий проп не задан, — ровно так, как если бы их передали пропсами: и для размера бокса плейсхолдера, и дляwidth/heightотрендеренного<img>, и для автоматическогоsizes.colorиспользуется сplaceholderMode="color"и сам по себе, когда нет ни хэша, ниplaceholder, — например, для SVG.- Явные пропсы и проп
imageвсегда важнее манифеста.
Поиск сравнивает строку src точно — /images/hero.jpg или полный URL CDN. Изображение, импортированное в компонент (import hero from './hero.jpg'), во время выполнения превращается в захешированный URL сборки и так не найдётся; для таких изображений команда placeholders записывает значения прямо в шаблон.
Плагин предоставляет манифест под ключом PLACEHOLDERS_KEY; приложение без плагина может предоставить его так же:
import { PLACEHOLDERS_KEY } from '@macrulez/vue-image-kit'
app.provide(PLACEHOLDERS_KEY, placeholders)Манифест для папок и URL
Шаблон можно просканировать только на тот src, который в нём написан. Если путь собирается во время выполнения — :src="category.image", список из CMS или файла с данными, — передайте манифесту папки целиком, и каждое изображение в них будет найдено по своему URL:
npx vue-image-kit placeholders --dir public/imagesПлагин Vite собирает такой же манифест при каждой сборке, без команды и без файла в репозитории. Он отдаётся как виртуальный модуль virtual:vue-image-kit/placeholders:
// vite.config.ts
import { vueImageKit } from '@macrulez/vue-image-kit/vite'
export default defineConfig({
plugins: [vue(), vueImageKit({ generate: false, placeholders: { dirs: ['public/images'] } })],
})import placeholders from 'virtual:vue-image-kit/placeholders'
app.use(VImageKitPlugin, { placeholders })В Nuxt модуль выполняет оба шага — см. Опции модуля. Каждый файл записывается под тем URL, по которому он отдаётся: файл из public/ — по пути от этой папки (public/images/a.jpg → /images/a.jpg), папка, которая отдаётся откуда-то ещё, — через urlPrefix ({ dir: 'src/img', urlPrefix: '/assets/img' }). Удалённые и CDN-изображения перечисляются в urls. Подробнее — Манифест плейсхолдеров из папок и Папки и URL.
v-lazy-img и useBackgroundImage() тоже читают манифест.
Кодирование на клиенте (пользовательский контент)
Когда пользователь загружает фото, кодируйте плейсхолдер прямо в браузере, чтобы мгновенно показать blur-up превью — ещё до того, как полное изображение загружено или обработано. Оба кодировщика не имеют зависимостей (кодировщик ThumbHash — точный порт референсной реализации, байт-в-байт идентичный пакету thumbhash) и принимают File/Blob, HTMLImageElement, HTMLCanvasElement, ImageBitmap или ImageData.
import { encodeThumbHash, encodeBlurhash, decodeThumbHash } from '@macrulez/vue-image-kit'
async function onFileSelected(file: File) {
const thumbhash = await encodeThumbHash(file)
// → base64-строка; передавайте прямо в <VImage :thumbhash="thumbhash">
// или decodeThumbHash(thumbhash) для превью в виде data URL.
const blurhash = await encodeBlurhash(file, { componentX: 4, componentY: 3 })
}encodeThumbHash(source, options?)→Promise<string>(base64). Опции:maxSize(по умолчанию/макс.100).encodeBlurhash(source, options?)→Promise<string>. Опции:componentX(1–9, по умолчанию4),componentY(1–9, по умолчанию3),maxSize(по умолчанию64).
Источник уменьшается до maxSize по длинной стороне перед кодированием (ThumbHash должен помещаться в 100×100). Требуют браузер/DOM — выбрасывают исключение в SSR.
<script setup lang="ts">
import { ref } from 'vue'
import { encodeThumbHash } from '@macrulez/vue-image-kit'
const hash = ref('')
async function handleUpload(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) hash.value = await encodeThumbHash(file)
}
</script>
<template>
<input type="file" accept="image/*" @change="handleUpload" />
<VImage v-if="hash" :src="previewUrl" alt="Preview" :thumbhash="hash" />
</template>