A lightweight, framework-agnostic lazy loading image component built with Lit Element. Load images only when they're visible to improve page performance and user experience.
I was inspired by the popular react-lazy-load-image-component
, which works great but is limited to React applications. I wanted to create something with the same powerful lazy loading capabilities but that would work universally across all frameworks and vanilla JavaScript.
By building with Lit Element, this component:
- Works in any framework (React, Vue, Angular, Svelte) or no framework at all
- Stays lightweight with minimal dependencies
- Achieves excellent performance through efficient rendering
- Provides a clean, standards-based API
Images load only when they enter (or approach) the viewport, significantly reducing initial page load time and saving bandwidth.
Uses IntersectionObserver API for modern browsers with intelligent fallback to optimized scroll events for older browsers.
Smooth loading effects with multiple options:
- Blur Effect: Starts with a blurred placeholder that sharpens when loaded.
- Black and White Effect: Shows a grayscale version that transitions to full color.
- Control threshold distance for preloading images.
- Choose between debounce or throttle for scroll performance optimization.
- Configure with visible-by-default option for above-the-fold content.
- Set specific dimensions or use responsive sizing.
Works seamlessly across all device sizes and adapts to different viewport dimensions.
Only depends on Lit, which is included in the bundle.
Works natively in any environment:
- Vanilla HTML/JS projects.
- React applications with proper event handling.
- Vue.js with native custom element support.
- Angular with CUSTOM_ELEMENTS_SCHEMA.
- Any other modern framework.
Important: This package requires
lit
as a peer dependency. You must install it in your project:npm install lit
npm install lazy-load-image-lit
<!-- Import the component -->
<script type="module" src="node_modules/lazy-load-image-lit/dist/lazy-load-image-lit.es.js"></script>
<!-- Use it in your HTML -->
<lazy-img
src="high-quality-image.jpg"
placeholderSrc="low-quality-thumbnail.jpg"
effect="blur"
threshold="200"
></lazy-img>
import React, { useRef, useEffect } from 'react';
import 'lazy-load-image-lit';
function MyImage({ src, placeholder }) {
const imgRef = useRef(null);
useEffect(() => {
// Use event listener for React integration
if (imgRef.current) {
imgRef.current.addEventListener('image-loaded', (e) => {
console.log('Image loaded in React!', e.detail);
});
}
}, []);
return (
<lazy-img
ref={imgRef}
src={src}
placeholderSrc={placeholder}
effect="blur"
></lazy-img>
);
}
<template>
<lazy-img
:src="imageUrl"
:placeholderSrc="placeholderUrl"
@image-loaded="onImageLoaded"
></lazy-img>
</template>
<script>
import 'lazy-load-image-lit';
export default {
methods: {
onImageLoaded(e) {
console.log('Image loaded in Vue!', e.detail);
}
}
}
</script>
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import 'lazy-load-image-lit';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA] // Required for custom elements
})
<!-- component.html -->
<lazy-img
[attr.src]="image.src"
[attr.placeholderSrc]="image.placeholder"
(image-loaded)="onImageLoaded($event)"
></lazy-img>
Property | Type | Default | Description | Available Options |
---|---|---|---|---|
src |
String | '' |
The URL of the main image to be lazy-loaded. When the image is about to enter the viewport, this is set as the src of the <img> element. |
Any valid image URL |
placeholderSrc |
String | '' |
The URL of the placeholder image shown before the main image loads. This is always rendered until the main image is loaded and visible. | Any valid image URL |
effect |
String | 'blur' |
Visual effect applied to the image during loading. 'blur' applies a blur to the placeholder, 'black-and-white' shows a grayscale effect. The effect is removed when the main image loads. |
'blur' , 'black-and-white' |
threshold |
Number | 0 |
Distance in pixels from the viewport or scroll container at which the image should start loading. For IntersectionObserver, this is used as rootMargin . For scroll/resize fallback, the image loads when it is within this many pixels of entering the visible area, allowing for preloading before the image is actually visible. |
Any number (e.g., 200 for 200px) |
useIntersectionObserver |
Boolean | true |
If true , uses the IntersectionObserver API for efficient lazy loading. If false , falls back to scroll/resize event listeners for visibility detection. |
true , false |
visibleByDefault |
Boolean | false |
If true , the image loads immediately without lazy loading. Useful for above-the-fold images. If false , lazy loading is enabled. |
true , false |
delayMethod |
String | 'debounce' |
Method for handling scroll/resize events: 'debounce' waits for a pause in events, 'throttle' limits event frequency. Only used when not using IntersectionObserver. |
'debounce' , 'throttle' |
delayTime |
Number | 300 |
Time in milliseconds for throttling or debouncing scroll/resize events. Controls how often visibility checks are performed. | Any positive number |
width |
Number | 0 |
Width of the image in pixels. Passed directly to the <img> element. If 0 , the attribute is omitted and the image uses its natural width or CSS. |
Any valid pixel value |
height |
Number | 0 |
Height of the image in pixels. Passed directly to the <img> element. If 0 , the attribute is omitted and the image uses its natural height or CSS. |
Any valid pixel value |
imgStyle |
String | '' |
Additional inline styles to apply to the <img> element. Useful for custom styling or responsive images. |
Any valid CSS style string |
Event Name | When It Fires | Data Provided |
---|---|---|
image-loaded |
When the main image has finished loading | Original image load event in the detail property |
<!-- Start loading when image is 500px before entering viewport -->
<lazy-img
threshold="500"
src="large-image.jpg"
placeholderSrc="thumbnail.jpg"
></lazy-img>
By default, the lazy loading component uses the debounce method for handling scroll and resize events. This means images are only checked and loaded after scrolling has paused for a short period, reducing unnecessary work and improving performance. You can also use throttle if you prefer more frequent checks.
How it works:
- When you scroll quickly through a long list of images, debounce ensures that only the images currently in (or near) the viewport are loaded. For example, if you scroll rapidly to the bottom, only the last few images will load, and images in between are skipped until you scroll back up.
- As you scroll back up, images will load one by one as they come into view, rather than all at once. This makes the experience smoother and more efficient, especially for pages with many images.
Example (using debounce):
<lazy-img
delayMethod="debounce"
delayTime="150"
src="image.jpg"
placeholderSrc="small.jpg"
></lazy-img>
This approach helps keep your page fast and responsive, even with a large number of images, by only loading what the user is actually about to see.
A common issue when using this package is that all images load immediately, even if they are not visible. This usually happens because the package first checks for the nearest parent element with overflow: auto
or overflow: scroll
to determine which container should be used for lazy loading.
If you set overflow: auto
or overflow: scroll
on a parent element that isn't actually scrollable (for example, if it doesn't have a fixed height or enough content to scroll), the component may think all images are in view and load them right away. This defeats the purpose of lazy loading.
Best Practice:
- Only add
overflow: auto
oroverflow: scroll
to a parent container if you really need a scrollable area (e.g., a div with a fixed height and lots of images inside). - If you don't need scrolling, avoid setting these overflow properties. Let the package use the window as the scroll container, so images are only loaded as they enter the viewport.
This small adjustment can make a big difference in how well lazy loading works in your app!