Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(draft) feat: use web workers for placeholder hash calculation #25

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions packages/core/src/blurhash.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { decodeBlurHash } from 'fast-blurhash'
import { DEFAULT_PLACEHOLDER_SIZE } from './constants'
import { getScaledDimensions } from './utils'
import { rgbaToDataUri } from './utils/dataUri'

export interface BlurHashOptions {
/**
Expand All @@ -27,7 +24,17 @@
size = DEFAULT_PLACEHOLDER_SIZE,
}: BlurHashOptions = {},
) {
const { width, height } = getScaledDimensions(ratio, size)
const rgba = decodeBlurHash(hash, width, height)
return rgbaToDataUri(width, height, rgba)
const worker = new Worker('./workers/blurhash-worker.ts')
worker.postMessage({
hash,
ratio,
size,
})

return new Promise<string>((resolve) => {
worker.onmessage = (event) => {
worker.terminate();

Check failure on line 36 in packages/core/src/blurhash.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
resolve(event.data)
}
})
}
15 changes: 9 additions & 6 deletions packages/core/src/lazyLoad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ export function lazyLoad<T extends HTMLImageElement>(

// Generate the blurry placeholder from a Blurhash or ThumbHash string if applicable
if (__ENABLE_HASH_DECODING__ && hash) {
const placeholder = createPlaceholderFromHash({
createPlaceholderFromHash({
image,
hash: typeof hash === 'string' ? hash : undefined,
hashType,
size: placeholderSize,
})
if (placeholder)
image.src = placeholder
.then((placeholder) => {
if (!placeholder)
return
image.src = placeholder
})
}

// Bail if the image doesn't provide a `data-src` or `data-srcset` attribute
Expand Down Expand Up @@ -117,7 +120,7 @@ export function loadImage(
})
}

export function createPlaceholderFromHash(
export async function createPlaceholderFromHash(
{
/** If given, the hash will be extracted from the image's `data-blurhash` or `data-thumbhash` attribute and ratio will be calculated from the image's actual dimensions */
image,
Expand Down Expand Up @@ -146,7 +149,7 @@ export function createPlaceholderFromHash(

try {
if (hashType === 'thumbhash') {
return createPngDataUriFromThumbHash(hash)
return await createPngDataUriFromThumbHash(hash)
}
else {
// Preserve the original image's aspect ratio
Expand All @@ -155,7 +158,7 @@ export function createPlaceholderFromHash(
const actualHeight = image.height || image.offsetHeight || size
ratio = actualWidth / actualHeight
}
return createPngDataUriFromBlurHash(hash, { ratio, size })
return await createPngDataUriFromBlurHash(hash, { ratio, size })
}
}
catch (error) {
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/thumbhash.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { thumbHashToRGBA } from 'thumbhash'
import { base64ToBytes } from './utils'
import { rgbaToDataUri } from './utils/dataUri'

export function createPngDataUri(hash: string) {
const { w, h, rgba } = thumbHashToRGBA(base64ToBytes(hash))
return rgbaToDataUri(w, h, rgba)
const worker = new Worker('./workers/thumbhash-worker.ts')
worker.postMessage({ hash })

return new Promise<string>((resolve) => {
worker.onmessage = (event) => {
worker.terminate();

Check failure on line 7 in packages/core/src/thumbhash.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
resolve(event.data)
}
})
}
19 changes: 19 additions & 0 deletions packages/core/src/workers/blurhash-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { decodeBlurHash } from 'fast-blurhash'
import { getScaledDimensions } from '../utils'
import { rgbaToDataUri } from '../utils/dataUri'

interface MessageEventData {
hash: string
ratio: number
size: number
}

onmessage = (event: MessageEvent<MessageEventData>) => {
const { hash, ratio, size } = event.data

const { width, height } = getScaledDimensions(ratio, size)
const rgba = decodeBlurHash(hash, width, height)
const dataURL = rgbaToDataUri(width, height, rgba)

postMessage(dataURL)
}
16 changes: 16 additions & 0 deletions packages/core/src/workers/thumbhash-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { thumbHashToRGBA } from 'thumbhash'
import { base64ToBytes } from '../utils'
import { rgbaToDataUri } from '../utils/dataUri'

interface MessageEventData {
hash: string
}

onmessage = (event: MessageEvent<MessageEventData>) => {
const { hash } = event.data

const { w, h, rgba } = thumbHashToRGBA(base64ToBytes(hash))
const dataURL = rgbaToDataUri(w, h, rgba)

postMessage(dataURL)
}
6 changes: 3 additions & 3 deletions packages/nuxt/src/runtime/components/UnLazyImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const hash = computed(() => props.thumbhash || props.blurhash)
// SSR-decoded BlurHash as PNG data URI placeholder image
// eslint-disable-next-line n/prefer-global/process
const pngPlaceholder = (process.server && (props.ssr ?? unlazy.ssr) && hash.value)
? createPlaceholderFromHash({
? await createPlaceholderFromHash({
hash: hash.value,
hashType: props.thumbhash ? 'thumbhash' : 'blurhash',
size: props.placeholderSize || unlazy.placeholderSize,
Expand All @@ -82,14 +82,14 @@ const target = ref<HTMLImageElement | undefined>()
let cleanup: (() => void) | undefined
let lastHash: string | undefined

watchEffect(() => {
watchEffect(async () => {
cleanup?.()

if (!target.value)
return

if (hash.value && hash.value !== lastHash) {
const placeholder = createPlaceholderFromHash({
const placeholder = await createPlaceholderFromHash({
image: target.value,
hash: hash.value,
hashType: props.thumbhash ? 'thumbhash' : 'blurhash',
Expand Down
Loading