Skip to content

Commit

Permalink
fix: generate unique placeholder when no src present in image
Browse files Browse the repository at this point in the history
  • Loading branch information
johannschopplich committed Nov 12, 2024
1 parent a46898a commit 9d52f27
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 40 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/blurhash.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { decodeBlurHash } from 'fast-blurhash'
import { DEFAULT_PLACEHOLDER_SIZE } from './constants'
import { calculateProportionalSize } from './utils'
import { calculateAspectRatioDimensions } from './utils'
import { rgbaToDataUri } from './utils/dataUri'

export interface BlurHashOptions {
Expand All @@ -27,7 +27,7 @@ export function createPngDataUri(
size = DEFAULT_PLACEHOLDER_SIZE,
}: BlurHashOptions = {},
) {
const { width, height } = calculateProportionalSize(ratio, size)
const { width, height } = calculateAspectRatioDimensions(ratio, size)
const rgba = decodeBlurHash(hash, width, height)
return rgbaToDataUri(width, height, rgba)
}
2 changes: 1 addition & 1 deletion packages/core/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const DEFAULT_PLACEHOLDER_SIZE = 32
export const DEFAULT_IMAGE_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
export const DEFAULT_IMAGE_PLACEHOLDER = `data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1' data-i=''%3E%3C/svg%3E`
35 changes: 10 additions & 25 deletions packages/core/src/lazyLoad.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { UnLazyLoadOptions } from './types'
import { createPngDataUri as createPngDataUriFromBlurHash } from './blurhash'
import { DEFAULT_IMAGE_PLACEHOLDER, DEFAULT_PLACEHOLDER_SIZE } from './constants'
import { DEFAULT_PLACEHOLDER_SIZE } from './constants'
import { createPngDataUri as createPngDataUriFromThumbHash } from './thumbhash'
import { debounce, isCrawler, isLazyLoadingSupported, toElementArray } from './utils'
import { createIndexedImagePlaceholder, debounce, isCrawler, isLazyLoadingSupported, toElementArray } from './utils'

export function lazyLoad<T extends HTMLImageElement>(
/**
Expand All @@ -21,14 +21,12 @@ export function lazyLoad<T extends HTMLImageElement>(
) {
const cleanupFns = new Set<() => void>()

for (const image of toElementArray<T>(selectorsOrElements)) {
for (const [index, image] of toElementArray<T>(selectorsOrElements).entries()) {
// Calculate the image's `sizes` attribute if `data-sizes="auto"` is set
const onResizeCleanup = updateSizesAttribute(image, { updateOnResize: updateSizesOnResize })
if (updateSizesOnResize && onResizeCleanup)
cleanupFns.add(onResizeCleanup)

const hasValidSrc = Boolean(image.src)

// Generate the blurry placeholder from a Blurhash or ThumbHash string if applicable
if (
// @ts-expect-error: Compile-time flag
Expand Down Expand Up @@ -61,18 +59,15 @@ export function lazyLoad<T extends HTMLImageElement>(
continue
}

// Ensure that `loading="lazy"` works correctly by setting the `src`
// attribute to a transparent 1x1 pixel
// Ensure that `loading="lazy"` works correctly by setting a default placeholder.
// For Chrome, is is necessary to generate a unique placeholder. Otherwise, as
// soon as the first placeholder is loaded, the `load` event will be triggered
// for all subsequent images, even if they are not in the viewport.
if (!image.src)
image.src = DEFAULT_IMAGE_PLACEHOLDER
image.src = createIndexedImagePlaceholder(index)

// Load immediately if:
// 1. Either the image has a valid `src` attribute and it is already loaded
// 2. Or it is already in the viewport (even with placeholder)
if (
(hasValidSrc && image.complete && image.naturalWidth > 0)
|| isPartiallyInViewport(image)
) {
// Load immediately if the image is already in the viewport
if (image.complete && image.naturalWidth > 0) {
loadImage(image, onImageLoad)
continue
}
Expand Down Expand Up @@ -267,13 +262,3 @@ function getOffsetWidth(element: HTMLElement | HTMLSourceElement) {
? element.parentElement?.getElementsByTagName('img')[0]?.offsetWidth
: element.offsetWidth
}

function isPartiallyInViewport(element: HTMLElement) {
const rect = element.getBoundingClientRect()
return (
rect.top < window.innerHeight
&& rect.bottom >= 0
&& rect.left < window.innerWidth
&& rect.right >= 0
)
}
27 changes: 15 additions & 12 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DEFAULT_IMAGE_PLACEHOLDER } from '../constants'

export const isSSR = typeof window === 'undefined'
export const isLazyLoadingSupported = !isSSR && 'loading' in HTMLImageElement.prototype
export const isCrawler = !isSSR && (!('onscroll' in window) || /(?:gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent))
Expand All @@ -15,20 +17,21 @@ export function toElementArray<T extends HTMLElement>(
return [...target]
}

export function calculateProportionalSize(aspectRatio: number, referenceSize: number) {
let width: number
let height: number
export function createIndexedImagePlaceholder(index: number) {
return DEFAULT_IMAGE_PLACEHOLDER.replace('data-i=\'\'', `data-i='${index}'`)
}

if (aspectRatio >= 1) {
width = referenceSize
height = Math.round(referenceSize / aspectRatio)
}
else {
width = Math.round(referenceSize * aspectRatio)
height = referenceSize
}
export function calculateAspectRatioDimensions(aspectRatio: number, referenceSize: number) {
const isLandscapeOrSquare = aspectRatio >= 1

return { width, height }
return {
width: isLandscapeOrSquare
? referenceSize
: Math.round(referenceSize * aspectRatio),
height: isLandscapeOrSquare
? Math.round(referenceSize / aspectRatio)
: referenceSize,
}
}

export function debounce<T extends (...args: any[]) => void>(
Expand Down

0 comments on commit 9d52f27

Please sign in to comment.