diff --git a/src/components/images/blurhash.worker.ts b/src/components/images/blurhash.worker.ts new file mode 100644 index 000000000..d72e5b3ee --- /dev/null +++ b/src/components/images/blurhash.worker.ts @@ -0,0 +1,25 @@ +import { decode } from 'blurhash'; +import { expose } from 'comlink'; + +/** + * Decodes blurhash outside the main thread, in a web worker + * + * @param {string} hash - Hash to decode. + * @param {number} width - Width of the decoded pixel array + * @param {number} height - Height of the decoded pixel array. + * @param {number} punch - Contrast of the decoded pixels + * @returns {Uint8ClampedArray} - Returns the decoded pixels in the proxied response by Comlink + */ +function getPixels({ + hash, + width, + height +}): Uint8ClampedArray { + try { + return decode(hash, width, height); + } catch { + throw new TypeError(`Blurhash ${hash} is not valid`); + } +} + +expose(getPixels); diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index ef89cd1d0..dbd38de9e 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -1,6 +1,12 @@ import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; import * as userSettings from '../../scripts/settings/userSettings'; -import { decode, isBlurhashValid } from 'blurhash'; +import { wrap } from 'comlink'; +const getPixels = wrap( + new Worker( + // eslint-disable-next-line compat/compat + new URL('./blurhash.worker.ts', import.meta.url) + ) +); import './style.scss'; /* eslint-disable indent */ @@ -12,21 +18,18 @@ import './style.scss'; fillImageElement(elem, source); } - function itemBlurhashing(target, blurhashstr) { - if (isBlurhashValid(blurhashstr)) { + async function itemBlurhashing(target, hash) { + try { // Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us, // improving the performance and reducing the memory usage, while retaining almost full blur quality. // Lower values had more visible pixelation - const width = 18; - const height = 18; - let pixels; - try { - pixels = decode(blurhashstr, width, height); - } catch (err) { - console.error('Blurhash decode error: ', err); - target.classList.add('non-blurhashable'); - return; - } + const width = 32; + const height = 32; + const pixels = await getPixels({ + hash, + width, + height + }); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; @@ -48,6 +51,10 @@ import './style.scss'; target.classList.add('blurhashed'); target.removeAttribute('data-blurhash'); }); + } catch (err) { + console.log(err); + target.classList.add('non-blurhashable'); + return; } } @@ -142,7 +149,7 @@ import './style.scss'; for (const lazyElem of elem.querySelectorAll('.lazy')) { const blurhashstr = lazyElem.getAttribute('data-blurhash'); if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) { - itemBlurhashing(lazyElem, blurhashstr); + Promise.resolve(itemBlurhashing(lazyElem, blurhashstr)); } else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) { lazyElem.classList.add('non-blurhashable'); }