Skip to content

Плейсхолдеры ​

Плейсхолдер HazeHash ​

HazeHash — предпочтительный плейсхолдер: строка в 7–48 байт (по умолчанию 28) из пакета hazehash, которая декодируется в размытое превью с правильными пропорциями и, если у изображения есть прозрачность, с альфа-каналом. При том же размере перцептивная ошибка у него ниже, чем у BlurHash и ThumbHash.

vue
<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-зависимость, которая подгружается по требованию, только когда хеш действительно показывается:

bash
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 — самый простой способ:

vue
<VImage src="/photo.png" alt="Photo with transparency" thumbhash="3OcRJYB4d3h/iIeHeEh3eIhw+j5n" />

VImage автоматически декодирует хэш и использует его как blur-up плейсхолдер. Ручное декодирование не нужно.

Использование декодера напрямую (для кастомной разметки или v-lazy-img):

ts
import { decodeThumbHash } from '@macrulez/vue-image-kit'

const dataUrl = decodeThumbHash('3OcRJYB4d3h/iIeHeEh3eIhw+j5n')
// → 'data:image/png;base64,...'

Средний цвет — самый дешёвый плейсхолдер из всех (декодируется из заголовка, без пикселей):

ts
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 уже есть:

vue
<VImage
  src="/photo.png"
  alt="Photo"
  :placeholder="decodeThumbHash('3OcRJYB4d3h/iIeHeEh3eIhw+j5n')"
/>

Если заданы и thumbhash, и placeholder, приоритет у placeholder.

Генерация хэшей ThumbHash на этапе сборки:

Используйте CLI с флагом --thumbhash (требует thumbhash как dev-зависимость):

bash
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:

ts
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-плейсхолдер:

vue
<VImage
  src="/photo.jpg"
  alt="Landscape"
  :width="1200"
  :height="800"
  blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
/>

Как это работает:

  1. На сервере — рендерится обычный <img loading="lazy">, без какой-либо плейсхолдер-механики (см. Компоновка без обёртки); готовое превью можно добавить к нему через ssrPlaceholder, см. Превью в серверном HTML
  2. На клиенте — вызывается decodeBlurhash(hash, width, height), пиксели рисуются в невидимый canvas в памяти (он никогда не попадает в DOM) через ImageData, и canvas.toDataURL() превращает их в CSS background-image прямо на элементе, который затем покажет фото
  3. Фон остаётся видимым, пока изображение загружается; как только фото декодировано и отрисовано, оно перекрывает фон — мгновенно, без анимации (если явно не включён проп fadeIn, см. Пропы VImage)

Видимая «размытость» — это увеличение крошечного 32-пиксельного превью через background-size: cover, без отдельного CSS-фильтра размытия.

useBlurhash() — отдельный API для случаев, когда нужен именно настоящий, прикреплённый к DOM <canvas>, который можно, например, рисовать поверх; <VImage> строит свой blurhash-плейсхолдер другим способом.

hazehash всегда побеждает blurhash. Если blurhash задан вместе с LQIP-плейсхолдером placeholder или thumbhash, приоритет имеет blurhash — в этом случае LQIP/ThumbHash blur-up вообще не рендерится. Они взаимоисключающие, а не накладываются друг на друга.

Использование декодера напрямую:

ts
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) показывает крошечную размытую версию изображения, пока грузится полное разрешение.

vue
<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):

ts
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:

vue
<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, а регистрируется он один раз:

ts
import { VImageKitPlugin } from '@macrulez/vue-image-kit'
import placeholders from './image-placeholders'

app.use(VImageKitPlugin, { placeholders })

В Nuxt вместо этого передайте модулю путь к файлу: vueImageKit: { placeholders: './image-placeholders.ts' }.

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; приложение без плагина может предоставить его так же:

ts
import { PLACEHOLDERS_KEY } from '@macrulez/vue-image-kit'

app.provide(PLACEHOLDERS_KEY, placeholders)

Манифест для папок и URL ​

Шаблон можно просканировать только на тот src, который в нём написан. Если путь собирается во время выполнения — :src="category.image", список из CMS или файла с данными, — передайте манифесту папки целиком, и каждое изображение в них будет найдено по своему URL:

bash
npx vue-image-kit placeholders --dir public/images

Плагин Vite собирает такой же манифест при каждой сборке, без команды и без файла в репозитории. Он отдаётся как виртуальный модуль virtual:vue-image-kit/placeholders:

ts
// vite.config.ts
import { vueImageKit } from '@macrulez/vue-image-kit/vite'

export default defineConfig({
  plugins: [vue(), vueImageKit({ generate: false, placeholders: { dirs: ['public/images'] } })],
})
ts
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.

ts
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.

vue
<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>