typescript
41 lines · 7 steps
Lazy-loading images with IntersectionObserver
Defer image loading until each element scrolls near the viewport, with a graceful fallback and a cleanup handle.
Explained by
highlit
1type LazyImageOptions = {
2 rootMargin?: string;
3 loadedClass?: string;
4};
5
6export function initLazyImages(
7 container: ParentNode = document,
8 { rootMargin = '200px 0px', loadedClass = 'is-loaded' }: LazyImageOptions = {}
9): () => void {
10 const targets = container.querySelectorAll<HTMLImageElement>('img[data-src]');
11
12 if (!('IntersectionObserver' in window)) {
13 targets.forEach((img) => loadImage(img, loadedClass));
14 return () => {};
15 }
16
17 const observer = new IntersectionObserver((entries, obs) => {
18 for (const entry of entries) {
19 if (!entry.isIntersecting) continue;
20 loadImage(entry.target as HTMLImageElement, loadedClass);
21 obs.unobserve(entry.target);
22 }
23 }, { rootMargin, threshold: 0.01 });
24
25 targets.forEach((img) => observer.observe(img));
26
27 return () => observer.disconnect();
28}
29
30function loadImage(img: HTMLImageElement, loadedClass: string): void {
31 const { src, srcset } = img.dataset;
32 if (!src) return;
33
34 img.addEventListener('load', () => img.classList.add(loadedClass), { once: true });
35
36 if (srcset) img.srcset = srcset;
37 img.src = src;
38
39 delete img.dataset.src;
40 delete img.dataset.srcset;
41}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1IntersectionObserver lets you react to viewport proximity without manual scroll listeners.
- 2Feature-detect browser APIs and provide an eager fallback so nothing silently fails.
- 3Returning a cleanup function makes side-effecting initializers safe to tear down.
Related explainers
typescript
import { Controller } from '@nestjs/common'; import { MessagePattern, Payload,
Manual RabbitMQ acks in a NestJS controller
microservices
message-queue
acknowledgement
Intermediate
8 steps
typescript
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
Type-safe deep merge in TypeScript
recursion
conditional-types
mapped-types
Advanced
7 steps
typescript
type CurrencyFormatOptions = { locale?: string; currency: string; showDecimals?: boolean;
Caching Intl.NumberFormat for currency
memoization
internationalization
caching
Intermediate
9 steps
typescript
import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-card',
Multi-slot content projection in Angular
content-projection
components
templates
Intermediate
7 steps
typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DeepPartial } from 'typeorm'; import { User } from './entities/user.entity';
Building a CRUD service in NestJS
crud
dependency-injection
repository-pattern
Intermediate
7 steps
typescript
type AsyncMethod = (...args: any[]) => Promise<any>; function LogExecutionTime(thresholdMs = 0) { return function <T extends AsyncMethod>(
A method decorator that times async calls
decorators
higher-order-functions
async
Advanced
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/lazy-loading-images-with-intersectionobserver-explained-typescript-63b9/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.