Справочник
Совместимость с SSR
- Серверный рендер —
<VImage>— рендерит один-единственный<img loading="lazy">сsrc/alt/нативнымиwidth/height; без IO, без canvas, без обёртывающего элемента — вся плейсхолдер-механика живёт только в клиентской ветке. - Резервирование места — достигается нативными HTML-атрибутами
width/heightна самом<img>, а не отдельным<div>с CSSaspect-ratio. - Blurhash на сервере — код декодирования (canvas в памяти) не выполняется вообще; SSR-ветка ничего не знает про
hazehash/blurhash/thumbhash/placeholder. IntersectionObserverна сервере — не используется; сервер рендерит обычный<img>.- Гидратация — после монтирования
onMountedнастраивает IO (еслиlazy: true) либо сразу начинает загрузку (еслиlazy: false). v-lazy-imgна сервере — хуки директивы (mounted,unmounted) не вызываются во время SSR — IO не создаётся.useLazyLoadна сервере — немедленно возвращает{ isIntersecting: true }— вызывающий код действует так, будто уже во вьюпорте.
Использование в Nuxt:
Особая настройка не требуется. Компонент корректно рендерится и в SSR, и в клиентском режиме. Если нужно узнать, смонтировался ли клиент, используйте onMounted из Vue:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>
<template>
<VImage v-if="mounted" src="/photo.jpg" alt="Photo" blurhash="..." />
<div v-else style="aspect-ratio: 16/9; background: #e5e7eb;" />
</template>Архитектура
VImage.vue
│ props: src, image, alt, width, height,
│ hazehash, blurhash, thumbhash, placeholder, ssrPlaceholder,
│ placeholderMode, placeholderColor,
│ widths, densities, sizes, sources, breakpoints,
│ lazy, rootMargin, threshold, fit, focal,
│ maxRetries, retryDelay,
│ fetchpriority, decoding, priority, respectSaveData,
│ layout, cdn, loader, loaderRoute, fadeIn
│
├──▶ useImage(options)
│ │
│ ├── useLazyLoad({ rootMargin, threshold })
│ │ IntersectionObserver (SSR-safe)
│ │ isIntersecting: Ref<boolean>
│ │ observe(elRef) → starts watching
│ │
│ ├── State machine
│ │ idle → loading → loaded
│ │ → error (retryCount >= maxRetries)
│ │ → idle → loading (retry, exponential backoff)
│ │ lazy=true → watch(isIntersecting) → loading
│ │ lazy=false → onMounted → loading
│ │ respectSaveData + isSaveDataEnabled() → neutralizes priority,
│ │ downgrades src via pickSmallestSrcsetUrl
│ │
│ └── imgAttrs: ComputedRef
│ src = fallback URL (or cdn/loader-rewritten URL)
│ srcset = generateSrcset(src, widths) / adapter.srcset() / handler URLs
│ sizes = generateSizes(sizes)
│ style = { objectFit: fit }
│
├──▶ blurhashToDataUrl(hash, width, height) ← local helper, NOT useBlurhash()
│ decodeBlurhash(hash, w, h) → pixels
│ offscreen canvas (created via document.createElement, never mounted)
│ → ctx.putImageData(...) → canvas.toDataURL()
│ returns: string | undefined (data: URL, or undefined on decode
│ failure/SSR — invalid input silently leaves it blank)
│ useBlurhash() (below) is a separate, standalone composable —
│ see use-blurhash.md
│
├──▶ useBreakpoints(breakpoints?)
│ Merges local breakpoints prop with global plugin breakpoints
│ resolveMediaSources(sources) → sorted [{ media, src, type? }]
│
├──▶ cdn / loader resolution
│ cdn → autoLoader()/autoSrcset() (vue-image-kit/cdn)
│ loader → useServerRoute() + buildImageUrl() (vue-image-kit/server)
│ cdn wins if both are set
│
├──▶ effectivePlaceholder: ComputedRef<string | undefined>
│ placeholder prop → used as-is (LQIP base64)
│ thumbhash prop → decodeThumbHash(hash) → PNG data URL
│ placeholderMode='color' → thumbHashToAverageColor(thumbhash) / placeholderColor
│ placeholderMode='shimmer' → CSS-only skeleton, no hash needed
│ hazehash / blurhash renders → undefined (they win, see priority below)
│ neither → undefined (no blur-up placeholder)
│
├──▶ placeholderBackgroundStyle: ComputedRef<CSSProperties>
│ Same priority every placeholder always had (color → shimmer →
│ hazehash → blurhash → LQIP/ThumbHash), just expressed as CSS now instead of
│ separate DOM nodes:
│ isLoaded → {} (the photo itself is the content)
│ placeholderColor / mode=color → { backgroundColor }
│ mode='shimmer' → {} (the .vik-shimmer CSS class
│ below does the gradient+animation)
│ hazehashUrl / blurhashToDataUrl(...) result → { backgroundImage: url(...),
│ backgroundSize: 'cover',
│ backgroundPosition }
│ effectivePlaceholder (LQIP/ThumbHash) → same shape, as background-image
│ none of the above → { backgroundColor: '#f3f4f6' } (neutral fallback)
│
├──▶ Template structure (client) — exactly ONE of these renders at any
│ given moment, never nested inside another (see "Компоновка без
│ обёртки" in responsive-images.md):
│
│ isIdle
│ <img ref="observeTargetRef" :src="blankSrc" alt="" aria-hidden="true"
│ :width="mergedWidth" :height="mergedHeight"
│ :style="{ aspectRatio, ...fixedSizeStyle, ...placeholderBackgroundStyle }"
│ :class="[loadedBoxClasses, { 'vik-fit': usesDefaultFit, 'vik-shimmer': showShimmerClass }]" />
│ ← IntersectionObserver target; carries
│ whatever placeholderBackgroundStyle resolved to
│
│ isError ← retries exhausted
│ <span :style="{ ...boxStyle, display: 'flex', ... }">
│ <slot name="error"><svg .../></slot>
│ </span>
│
│ needsPicture ← art-direction/format sources needed
│ <picture :style="{ display: boxStyle.display }">
│ <source v-for media/srcset /> ← responsive art direction
│ <source type="image/avif" />
│ <source type="image/webp" />
│ <img v-bind="imgAttrs" :style="{ ...boxStyle, objectFit,
│ ...placeholderBackgroundStyle }"
│ :class="{ 'vik-shimmer': showShimmerClass }"
│ :decoding :fetchpriority @load @error />
│ </picture>
│
│ otherwise (loading | loaded, no picture needed)
│ <img v-bind="imgAttrs" :style="{ ...boxStyle, objectFit,
│ ...placeholderBackgroundStyle }"
│ :class="{ 'vik-shimmer': showShimmerClass }"
│ :decoding :fetchpriority @load @error />
│ ← simple img (no picture)
│
└──▶ Template structure (SSR)
<img :src :alt :width :height :decoding :fetchpriority
:loading="lazy ? 'lazy' : 'eager'" />
vLazyImg (Directive)
│ mounted(el, binding)
│ resolveOptions(binding) → { src, placeholder, rootMargin, ... }
│ getPooledObserver(rootMargin, threshold) ← shared observer-pool.ts, same pool
│ useLazyLoad/useImage/VImage/useBackgroundImage use
│ IntersectionObserver → on intersect:
│ if placeholder: el.style.backgroundImage = url(placeholder)
│ new Image()
│ onload → el.style.backgroundImage = url(src); onLoad()
│ onerror → onError(e)
│ updated → disconnect old observer, create new one
│ unmounted → observer.disconnect()
useBackgroundImage(src, options) ← srcset-capable counterpart of vLazyImg
│ useLazyLoad({ rootMargin, threshold })
│ style: ComputedRef<{ backgroundImage, backgroundSize, ... }>
│ plain src → url(src)
│ densities → image-set(url(src) 1x, url(src) 2x, ...)
useNetworkAware() / isSaveDataEnabled()
│ navigator.connection.saveData / .effectiveType (Chromium-only API)
│ Used by: VImage's respectSaveData prop, useImagePreloader's preload()
useImagePreloader()
│ preload(urls) → Promise<void>, skipped entirely when isSaveDataEnabled()
│ loaded / total / progress / isComplete / errors
useServerLoader — useServerRoute(localOverride?) / SERVER_ROUTE_KEY
│ Resolves the on-demand server route: localOverride → plugin/module
│ provide()'d default → '/_vik/image' (DEFAULT_SERVER_ROUTE)
vue-image-kit/cdn (12 adapters, zero deps)
│ cloudinary / imgix / bunny / sanity / storyblok / contentful /
│ vercel / cloudflare / imagekit / twicpics / netlify / gumlet
│ Shared interface: adapter.url(path, options?) / adapter.srcset(path, widths, options?)
│ autoLoader() / autoSrcset() — hostname fingerprinting for 8 of the 12
vue-image-kit/server
│ createImageHandler(options) → (req, res) => void — framework-agnostic
│ buildImageUrl(src, options) → request URL string
│ Disk-cached, sharp-based, one transform per request
Utils (pure functions, zero Vue deps)
│ blurhash-decode.ts
│ decodeBlurhash(hash, width, height) → Uint8ClampedArray ← RGBA pixels
│
│ thumbhash-decode.ts
│ decodeThumbHash(hash: string | Uint8Array) → string ← PNG data URL
│ thumbHashToAverageRGBA(hash) / thumbHashToAverageColor(hash)
│
│ encode.ts (browser-only, in-house — no `thumbhash` package dependency)
│ encodeBlurhash(source, options?) / encodeThumbHash(source, options?)
│
└── srcset.ts
generateSrcset(src, widths) → string
generateSizes(sizes?) → string
generateDensitySrcset(src, densities) → string
buildSizes(map, breakpoints) → string
generatePreloadLink(href, options) → string
pickSmallestSrcsetUrl(srcset) → string | undefined
cli/ + vite/plugin.ts (Node-only, sharp-based, not in the browser bundle)
│ bin.ts — `vue-image-kit generate` command + flag parsing
│ processor.ts — resize/convert/LQIP/BlurHash/ThumbHash, SVG/GIF special-casing
│ incremental.ts — mtime/hash-based skip cache
│ manifest.ts — writes the TypeScript images.ts manifest
│ vite/plugin.ts — same generate() on buildStart/handleHotUpdate,
│ ?vik/?thumbhash resolvers, dev.onDemand handler
nuxt/module.ts (Node-only)
│ Registers VImage/v-lazy-img as global, auto-imports composables/utils,
│ provides breakpoints/serverRoute, optional onDemandServer → addServerHandlerРазмер бандла и peer-зависимости
| Точка входа | Raw | Gzip | Peer-зависимости |
|---|---|---|---|
@macrulez/vue-image-kit ESM | 45.6 kB | 14.08 kB | vue ^3.0 |
@macrulez/vue-image-kit CJS | 34.4 kB | 12.36 kB | vue ^3.0 |
@macrulez/vue-image-kit/cdn ESM | 11.2 kB | 2.53 kB | — |
Измерено по реальному результату сборки (npm run build), не поддерживается вручную — CI падает, если это отклоняется от порогов в .github/workflows/ci.yml. CLI, плагин Vite, модуль Nuxt и самостоятельный сервер — это Node-инструментарий, никогда не бандлится для браузера, поэтому они вне охвата этой таблицы.
Поставляется как tree-shakeable ESM (vue-image-kit.js) и CommonJS (vue-image-kit.cjs). "sideEffects": false в package.json — неиспользуемые экспорты удаляются бандлером. Если вы импортируете только vLazyImg или один composable, бандлер исключит всё остальное (VImage, декодер blurhash и т. д.).
Пример tree-shaking — использование только директивы:
// В бандл попадает только vLazyImg и его логика IO.
// VImage, useBlurhash, decodeBlurhash не импортированы → не бандлятся.
import { vLazyImg } from '@macrulez/vue-image-kit'
app.directive('lazy-img', vLazyImg)Лицензия
MIT