Library / SDK
johannschopplich/unlazy avatar
johannschopplich/unlazy

unlazy: lazy loading for images that mostly leaves the browser to do it

🪧 Universal lazy loading library for placeholder images leveraging native browser APIs

889 stars18 forksTypeScriptMIT

At a glance

What is it?
unlazy is a small TypeScript library that builds on the native loading attribute rather than replacing it, adds BlurHash and ThumbHash placeholders, computes the sizes attribute, preloads for search engine bots, and warns you in development when a lazy-loaded image is your LCP element. The idea is that most of the work stays in the browser.
Who is it for?
unlazy suits a front-end team that wants lazy loading without hand-writing IntersectionObserver code or pulling in a framework specific image component, and that cares about the LCP element being correct. It does not suit a project that already has a working image pipeline, because the library deliberately stays close to the native attribute and adds little beyond it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 78 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

It is built on the native attribute, not instead of it

The first thing to understand is that unlazy does not implement lazy loading. It uses the loading attribute that browsers have shipped for years, and its job is to make that attribute work properly in a real page rather than to add a second mechanism on top of it. That decision shapes everything else about the library, including its small size and the fact that it works with any framework or with no framework at all. The feature list makes the position explicit: native, because it utilises the loading attribute, and framework-agnostic, because it works with any framework or none. A page written with the library degrades to a page where images load eagerly if the script never runs, which is the opposite of what a custom lazy loading implementation would do. The project calls itself a universal lazy loading library, and the primary language is TypeScript, with a bundle size badge pointing at bundlephobia for the minified and gzipped figure.

One function handles every image on the page

The basic usage is a single import and a call with no arguments. What that call does is process the images matching two selectors: img[loading="lazy"], and img[loading="eager"] that also carry a data-src or a data-srcset attribute:

ts
import { lazyLoad } from 'unlazy'

// Processes `img[loading="lazy"]` and `img[loading="eager"][data-src|data-srcset]`
lazyLoad()

The second selector is the one that explains the design. For a normal lazy image you write the real source straight into src and the browser handles it. For an image you want the library to swap in, you write the source into data-src or data-srcset and mark the tag eager, and the library moves the value across. The function also accepts a CSS selector, a DOM element, a list of DOM elements, or an array of DOM elements, so you can scope it to one gallery or to a single image. That flexibility is what keeps the library framework-agnostic, since a React or Svelte or Vue application can hand it whatever it has at hand.

The above-the-fold path is the one people get wrong

Most lazy loading mistakes come from marking the hero image lazy, because a lazy image that is the largest contentful paint is the slowest thing on the page. unlazy has a dedicated path for that case. For an above-the-fold image, your hero or LCP image, you use loading="eager", and unlazy swaps the real source immediately and applies fetchpriority="high" for you:

html
<img loading="eager" data-src="hero.jpg" width="1600" height="900" alt="Sunset">

The width and height in that example are not decoration either, because the library calculates the sizes attribute automatically, and reserving the space is what keeps the layout from shifting while the image arrives. The library also warns in development mode when a lazy-loaded image is your LCP element, which is the specific mistake that costs you a Core Web Vitals score. The dev-mode warning is the kind of feature that only makes sense in a library that has decided the browser API is the right one, because you cannot add that check to the native attribute itself.

BlurHash and ThumbHash placeholders, server and client side

The placeholder support covers two compact image hash formats, BlurHash and ThumbHash, and the library can decode them both on the server and on the client. The point of a hash placeholder is that you decode a short string into a tiny image representation and paint it immediately, so the user sees a blurred version of the image while the real file downloads, rather than an empty box. The library links to the BlurHash site and the ThumbHash repository for the formats themselves, and the documentation has hash-based placeholder sections for each. Decoding on the server matters for frameworks that render markup before the client script runs, because then the placeholder can already be in the initial HTML rather than appearing after hydration. The library describes the whole feature set as Core Web Vitals aware, which is the claim that ties the eager priority path, the automatic sizes calculation and the placeholder decoding together.

Search engine bots get everything preloaded

One feature is aimed squarely at SEO rather than at performance. The library detects search engine bots and preloads all images. The reasoning is that a crawler that never scrolls may never trigger a lazy load, so a page whose images only appear on scroll may be indexed with fewer images than it should have. Preloading for bots trades a little bandwidth for a more complete index. The feature list also calls out support for the picture element with multiple image tags, which matters for art direction where one picture holds a mobile and a desktop source, and the auto-initialize path, which means the library is usable without a build step at all: you can load it from a script tag and it will pick up the images on the page by itself.

A monorepo with a workspace build and four test projects

The repository is a pnpm workspace monorepo named @unlazy/monorepo, marked private, with the package manager pinned to pnpm 10.33.0. The scripts show the shape of the project. The build runs the build script across every package directory, the dev:prepare step does the same for a prepare step, and there are separate scripts for the documentation site, which lives in its own directory and can be developed, built and served. Linting is eslint with a fix variant, and there is a release script that runs bumpp in recursive mode. The test setup is the part worth noticing, because it is split by project: a plain vitest run for everything, a unit project, a browser project, and a types project that runs tsc in no-emit mode plus the type tests of every package. The browser tests use the vitest browser mode with Playwright, and happy-dom is in the dependency list, so there are two different DOM environments in play. There is also an arethetypeswrong cli in the dev dependencies, which is the check that catches packaging mistakes that break consumers, and publint for the same reason. A wrangler.toml at the root suggests the documentation site runs on Cloudflare Workers during development.

The local development loop is five steps

Contributing to the project is documented as a short list. You clone the repository, enable Corepack with corepack enable, install the dependencies with pnpm install, run pnpm run dev:prepare, and then start the development server with pnpm run dev inside one of the packages directories. The Corepack step matters more than it looks, because the repository pins a specific pnpm version in the packageManager field, and Corepack is what makes that pin authoritative rather than advisory. The dev:prepare step exists because the packages have a prepare phase that has to run before the development server can start, and the same script is wired to the root prepare hook so that it also happens on install. The last step, running dev inside one of the package directories rather than at the root, tells you the packages are meant to be worked on individually. The project is licensed MIT, with the copyright holder named in the README as Johann Schopplich.

Editorial conclusion

unlazy suits a front-end team that wants lazy loading without hand-writing IntersectionObserver code or pulling in a framework specific image component, and that cares about the LCP element being correct. It does not suit a project that already has a working image pipeline, because the library deliberately stays close to the native attribute and adds little beyond it. Before adopting it, check that the two APIs it exposes match what you need, since the whole library is a single lazyLoad function plus a priority path for above the fold images, and note that the package is installed as a development dependency because it is meant to run at build time or on the client as a small script.

Frequently asked questions

How do I install unlazy and apply it to my images?

Install it as a development dependency with pnpm add -D unlazy or npm i -D unlazy, then import lazyLoad from unlazy and call it. With no arguments it processes img[loading="lazy"] and img[loading="eager"] elements that carry a data-src or data-srcset attribute.

How do I handle the hero image with unlazy?

Mark it loading="eager" with the source in data-src. The library swaps the real source immediately and applies fetchpriority="high" for you, and in development it warns you when a lazy-loaded image is your LCP element.

Does unlazy work without a framework or a build step?

Yes. The library is described as framework-agnostic, and it auto-initializes, so it is usable without a build step. The lazyLoad function also accepts a CSS selector, a DOM element, a list of DOM elements, or an array of DOM elements.

Which placeholder formats does unlazy support?

BlurHash and ThumbHash, with decoding available on the server side and on the client side. The library also calculates the sizes attribute automatically and preloads all images when it detects a search engine bot.

Official sources

  1. johannschopplich/unlazy on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/johannschopplich-unlazy.svg)](https://hysenlabs.com/projects/johannschopplich-unlazy)

Was this useful?