Created
October 28, 2025 13:35
-
-
Save SebiBasti/a5c3c7378059507ab7c8d3fa28a7e942 to your computer and use it in GitHub Desktop.
Optimized CloudImage Component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script setup lang="ts"> | |
| import { Cloudinary } from '@cloudinary/url-gen' | |
| import { sharpen } from '@cloudinary/url-gen/actions/adjust' | |
| import { dpr, format, quality } from '@cloudinary/url-gen/actions/delivery' | |
| import { limitFit } from '@cloudinary/url-gen/actions/resize' | |
| import { auto as fAuto } from '@cloudinary/url-gen/qualifiers/format' | |
| import { auto as qAuto } from '@cloudinary/url-gen/qualifiers/quality' | |
| import { AdvancedImage, lazyload } from '@cloudinary/vue' | |
| import { useDebounce, useResizeObserver } from '@vueuse/core' | |
| import { nextTick, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue' | |
| /** | |
| * Component: CloudImage | |
| * | |
| * Purpose: | |
| * Renders an adaptive Cloudinary image that automatically requests | |
| * the correct width and device pixel ratio (DPR) based on the element’s | |
| * rendered size. | |
| * | |
| * Implementation details: | |
| * - Uses a ResizeObserver to track the <img> element width. | |
| * - Debounces width updates to avoid spamming Cloudinary requests. | |
| * - Dynamically adjusts DPR for sharpness and efficiency. | |
| * - Applies auto format (f_auto) and quality (q_auto) transformations. | |
| * | |
| * Notes: | |
| * We intentionally omit Cloudinary’s `responsive()` plugin to avoid | |
| * overlapping ResizeObservers. This component fully owns its own | |
| * responsiveness. | |
| */ | |
| /** | |
| * Props | |
| * - imageName: Public ID of the image in Cloudinary. | |
| * - alt: Alternative text for accessibility. | |
| */ | |
| const props = defineProps<{ | |
| imageName: string | |
| alt: string | |
| }>() | |
| const cloud = new Cloudinary({ | |
| cloud: { cloudName: import.meta.env.VITE_CLOUDINARY_NAME } | |
| }) | |
| /** | |
| * Layout and DPR thresholds | |
| * - BASE_MIN_WIDTH: Prevents very small width requests during layout shifts. | |
| * - MAX_W: Caps the requested width to avoid massive derivatives. | |
| * - DPR_SWITCH_AT: Switches from DPR=2 to DPR=auto beyond this width. | |
| */ | |
| const BASE_MIN_WIDTH = 320 | |
| const MAX_W = 3440 | |
| const DPR_SWITCH_AT = 1400 | |
| /** | |
| * Element + Resize tracking | |
| * - imageRef: Reference to the <AdvancedImage> component (its $el is the <img> element). | |
| * - rawWidth: Immediate width reported by ResizeObserver. | |
| * - debouncedWidth: Smoothed width value used for transformations. | |
| */ | |
| const imageRef = ref() | |
| const rawWidth = ref(0) | |
| const debouncedWidth = useDebounce(rawWidth, 100, { maxWait: 200 }) | |
| /** | |
| * Handle to stop ResizeObserver when the component unmounts. | |
| * VueUse’s `useResizeObserver()` returns an object with a `.stop()` method. | |
| */ | |
| let stopResizeObserver: (() => void) | undefined | |
| /** | |
| * Reactive Cloudinary image instance. | |
| * Updated whenever debounced width or DPR conditions change. | |
| */ | |
| const cldImage = ref(cloud.image(props.imageName)) | |
| /** | |
| * onMounted() | |
| * Wait for DOM render → locate <img> element → start ResizeObserver. | |
| * Seeds an initial width measurement for the first transformation. | |
| */ | |
| onMounted(async () => { | |
| await nextTick() | |
| const imgEl: HTMLElement | undefined = imageRef.value?.$el | |
| if (!imgEl) { | |
| return | |
| } | |
| /** | |
| * Initial measurement (rounded to nearest pixel) | |
| */ | |
| rawWidth.value = Math.round(imgEl.getBoundingClientRect().width || 0) | |
| /** | |
| * Start observing the <img> element for size changes | |
| */ | |
| const { stop } = useResizeObserver(imgEl, (entries) => { | |
| const entry = entries[0] | |
| rawWidth.value = Math.round(entry.contentRect.width || 0) | |
| }) | |
| stopResizeObserver = stop | |
| }) | |
| /** | |
| * Disconnect the ResizeObserver to prevent memory leaks. | |
| */ | |
| onBeforeUnmount(() => { | |
| stopResizeObserver?.() | |
| }) | |
| /** | |
| * Reactive transformation logic | |
| * | |
| * Recomputes the Cloudinary URL whenever the debounced width changes. | |
| * Skips rebuilding the URL if effective parameters (width, DPR, sharpen) | |
| * are identical to the previous transformation. | |
| */ | |
| type DprMode = '2' | 'auto' | |
| let lastApplied = { width: 0, dpr: '2' as DprMode, sharpened: false } | |
| let lastUrl = '' | |
| watchEffect(() => { | |
| /** | |
| * Fallback width for SSR / early render | |
| */ | |
| const w = debouncedWidth.value || 800 | |
| /** | |
| * Clamp the measured width to safe limits | |
| */ | |
| const safeWidth = Math.min(MAX_W, Math.max(BASE_MIN_WIDTH, w)) | |
| /** | |
| * Determine DPR strategy | |
| */ | |
| const dprMode: DprMode = w >= DPR_SWITCH_AT ? 'auto' : '2' | |
| /** | |
| * Apply gentle sharpening for small images | |
| */ | |
| const shouldSharpen = w < 600 | |
| /** | |
| * Skip rebuild if nothing effectively changed | |
| */ | |
| if ( | |
| lastApplied.width === safeWidth && | |
| lastApplied.dpr === dprMode && | |
| lastApplied.sharpened === shouldSharpen | |
| ) { | |
| return | |
| } | |
| /** | |
| * Construct base Cloudinary transformation | |
| */ | |
| const base = cloud | |
| .image(props.imageName) | |
| .delivery(format(fAuto())) | |
| .delivery(quality(qAuto())) | |
| .resize(limitFit().width(safeWidth)) | |
| /** | |
| * Apply DPR mode | |
| */ | |
| const withDpr = dprMode === 'auto' ? base.delivery(dpr('auto')) : base.delivery(dpr(2)) | |
| /** | |
| * Optionally apply sharpening | |
| */ | |
| if (shouldSharpen) withDpr.adjust(sharpen()) | |
| /** | |
| * Assign only if the resulting URL is new | |
| */ | |
| const url = withDpr.toURL() | |
| if (url !== lastUrl) { | |
| cldImage.value = withDpr | |
| lastUrl = url | |
| lastApplied = { width: safeWidth, dpr: dprMode, sharpened: shouldSharpen } | |
| } | |
| }) | |
| </script> | |
| <template> | |
| <AdvancedImage | |
| ref="imageRef" | |
| :cldImg="cldImage" | |
| :alt="alt || imageName" | |
| :plugins="[lazyload()]" | |
| /> | |
| </template> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment