UNPKG

@unlazy/core

Version:

Universal lazy loading library for placeholder images leveraging native browser APIs

215 lines (214 loc) 8.58 kB
import { t as createPngDataUri } from "./blurhash-BCGE6nQK.js"; import { a as toElementArray, i as isSSR, n as debounce, o as rgbaToDataUri, r as isCrawler, t as createIndexedImagePlaceholder } from "./utils-BhdEU1vG.js"; import { t as createPngDataUri$1 } from "./thumbhash-C_GLB_pz.js"; //#region src/lcpWarning.ts let isInstalled = false; function installLcpWarning() { if (isInstalled) return; isInstalled = true; try { new PerformanceObserver((entries) => { for (const entry of entries.getEntries()) { const element = entry.element; if (!element || element.tagName !== "IMG") continue; const isLazy = element.loading === "lazy"; const hasDataSrc = element.hasAttribute("data-src") || element.hasAttribute("data-srcset"); if (isLazy || hasDataSrc) console.warn("[unlazy] LCP element is configured for lazy loading. Set `loading=\"eager\"` to improve Largest Contentful Paint.", element); } }).observe({ type: "largest-contentful-paint", buffered: true }); } catch {} } //#endregion //#region src/lazyLoad.ts const processedImages = /* @__PURE__ */ new WeakSet(); function lazyLoad(selectorsOrElements = "img[loading=\"lazy\"], img[loading=\"eager\"][data-src], img[loading=\"eager\"][data-srcset]", { hash = true, hashType = "blurhash", placeholderSize = 32, updateSizesOnResize = false, onImageLoad, onImageError } = {}) { const cleanups = /* @__PURE__ */ new Set(); if (typeof __UNLAZY_LOGGING__ === "undefined" || __UNLAZY_LOGGING__) installLcpWarning(); for (const [index, image] of toElementArray(selectorsOrElements).entries()) { if (processedImages.has(image)) continue; const isEager = image.loading === "eager"; if (isEager && !image.hasAttribute("fetchpriority")) image.setAttribute("fetchpriority", "high"); cleanups.add(autoSizes(image, { updateOnResize: updateSizesOnResize })); if ((typeof __UNLAZY_HASH_DECODING__ === "undefined" || __UNLAZY_HASH_DECODING__) && hash) { const placeholder = createPlaceholderFromHash({ image, hash: typeof hash === "string" ? hash : void 0, hashType, size: placeholderSize }); if (placeholder) image.src = placeholder; } if (!image.dataset.src && !image.dataset.srcset) { if (typeof __UNLAZY_LOGGING__ === "undefined" || __UNLAZY_LOGGING__) console.error("[unlazy] Missing `data-src` or `data-srcset` attribute", image); continue; } processedImages.add(image); if (isEager || isCrawler) { if (onImageLoad) { const loadHandler = () => onImageLoad(image); image.addEventListener("load", loadHandler, { once: true }); cleanups.add(() => image.removeEventListener("load", loadHandler)); } if (onImageError) { const errorHandler = (event) => onImageError(image, event); image.addEventListener("error", errorHandler, { once: true }); cleanups.add(() => image.removeEventListener("error", errorHandler)); } swapPictureSources(image); swapDataAttribute(image, "srcset"); swapDataAttribute(image, "src"); continue; } if (!image.src) image.src = createIndexedImagePlaceholder(index); if (image.complete && image.naturalWidth > 0) { cleanups.add(triggerLoad(image, { onImageLoad, onImageError })); continue; } const loadHandler = () => { cleanups.add(triggerLoad(image, { onImageLoad, onImageError })); }; image.addEventListener("load", loadHandler, { once: true }); cleanups.add(() => image.removeEventListener("load", loadHandler)); } return () => { for (const fn of cleanups) fn(); cleanups.clear(); }; } function autoSizes(selectorsOrElements = "img[data-sizes=\"auto\"], source[data-sizes=\"auto\"]", { updateOnResize = false } = {}) { const cleanups = []; for (const element of toElementArray(selectorsOrElements)) cleanups.push(observeAutoSizes(element, updateOnResize)); return () => { for (const fn of cleanups) fn(); cleanups.length = 0; }; } function triggerLoad(image, { onImageLoad, onImageError } = {}) { const cleanups = []; const cleanup = () => { for (const fn of cleanups) fn(); cleanups.length = 0; }; if (isDescendantOfPicture(image)) { if (onImageLoad) { const handler = () => onImageLoad(image); image.addEventListener("load", handler, { once: true }); cleanups.push(() => image.removeEventListener("load", handler)); } if (onImageError) { const handler = (event) => onImageError(image, event); image.addEventListener("error", handler, { once: true }); cleanups.push(() => image.removeEventListener("error", handler)); } swapPictureSources(image); swapDataAttribute(image, "srcset"); swapDataAttribute(image, "src"); return cleanup; } const { srcset: dataSrcset, src: dataSrc, sizes: dataSizes } = image.dataset; if (!dataSrcset && !dataSrc) return cleanup; const temporaryImage = new Image(); const loadHandler = () => { swapDataAttribute(image, "srcset"); swapDataAttribute(image, "src"); onImageLoad?.(image); }; const errorHandler = (event) => { image.dispatchEvent(new Event("error")); onImageError?.(image, event); }; temporaryImage.addEventListener("load", loadHandler, { once: true }); temporaryImage.addEventListener("error", errorHandler, { once: true }); cleanups.push(() => { temporaryImage.removeEventListener("load", loadHandler); temporaryImage.removeEventListener("error", errorHandler); temporaryImage.src = ""; }); if (dataSizes === "auto") { const width = getOffsetWidth(image); if (width) temporaryImage.sizes = `${width}px`; } else if (image.sizes) temporaryImage.sizes = image.sizes; if (dataSrcset) temporaryImage.srcset = dataSrcset; if (dataSrc) temporaryImage.src = dataSrc; return cleanup; } function createPlaceholderFromHash({ image, hash, hashType = "blurhash", size = 32, ratio } = {}) { if (image && !hash) { const { blurhash, thumbhash } = image.dataset; hash = thumbhash || blurhash; hashType = thumbhash ? "thumbhash" : "blurhash"; } if (!hash) return; try { if (hashType === "blurhash") { if (image && !ratio) ratio = (image.width || image.offsetWidth || size) / (image.height || image.offsetHeight || size); return createPngDataUri(hash, { ratio, size }); } return createPngDataUri$1(hash); } catch (error) { if (typeof __UNLAZY_LOGGING__ === "undefined" || __UNLAZY_LOGGING__) console.error(`[unlazy] Failed to generate ${hashType} placeholder:`, error); } } function observeAutoSizes(element, updateOnResize) { const targets = collectAutoSizeTargets(element); for (const target of targets) updateSizesAttribute(target); if (!updateOnResize || !targets.some(hasAutoSizes)) return noop; const observedImage = element instanceof HTMLImageElement ? element : element.parentElement?.getElementsByTagName("img")[0] ?? null; if (!observedImage) return noop; const update = debounce(() => { for (const target of collectAutoSizeTargets(element)) updateSizesAttribute(target); }, 500); const observer = new ResizeObserver(update); observer.observe(observedImage); return () => observer.disconnect(); } function hasAutoSizes(element) { return element.dataset.sizes === "auto"; } function collectAutoSizeTargets(element) { const targets = [element]; if (element instanceof HTMLImageElement && element.parentElement?.tagName.toLowerCase() === "picture") for (const source of element.parentElement.querySelectorAll("source[data-sizes=\"auto\"]")) targets.push(source); return targets; } function updateSizesAttribute(element) { if (element.dataset.sizes !== "auto") return; const width = getOffsetWidth(element); if (!width) return; const next = `${width}px`; if (element.sizes !== next) element.sizes = next; } function swapDataAttribute(element, attr) { const value = element.dataset[attr]; if (!value) return; element[attr] = value; element.removeAttribute(`data-${attr}`); } function swapPictureSources(image) { const picture = image.parentElement; if (picture?.tagName.toLowerCase() !== "picture") return; for (const source of picture.querySelectorAll("source[data-srcset]")) { updateSizesAttribute(source); swapDataAttribute(source, "srcset"); } } function getOffsetWidth(element) { return element instanceof HTMLSourceElement ? element.parentElement?.getElementsByTagName("img")[0]?.offsetWidth : element.offsetWidth; } function isDescendantOfPicture(element) { return element.parentElement?.tagName.toLowerCase() === "picture"; } function noop() {} //#endregion export { autoSizes, createIndexedImagePlaceholder, createPlaceholderFromHash, debounce, isCrawler, isSSR, lazyLoad, rgbaToDataUri, toElementArray, triggerLoad };