@unlazy/core
Version:
Universal lazy loading library for placeholder images leveraging native browser APIs
215 lines (214 loc) • 8.58 kB
JavaScript
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 };