javascript 39 lines · 8 steps

How lazy image loading works

Defer loading images until they near the viewport using IntersectionObserver, with a graceful fallback.

Explained by highlit
1function initLazyLoad(root = document) {
2 const images = root.querySelectorAll('img[data-src]');
3 
4 if (!('IntersectionObserver' in window)) {
5 images.forEach(loadImage);
6 return;
7 }
8 
9 const observer = new IntersectionObserver((entries, obs) => {
10 for (const entry of entries) {
11 if (!entry.isIntersecting) continue;
12 loadImage(entry.target);
13 obs.unobserve(entry.target);
14 }
15 }, {
16 rootMargin: '200px 0px',
17 threshold: 0.01
18 });
19 
20 images.forEach((img) => observer.observe(img));
21}
22 
23function loadImage(img) {
24 const { src, srcset } = img.dataset;
25 
26 img.addEventListener('load', () => {
27 img.classList.add('is-loaded');
28 }, { once: true });
29 
30 img.addEventListener('error', () => {
31 img.classList.add('is-error');
32 }, { once: true });
33 
34 if (srcset) img.srcset = srcset;
35 if (src) img.src = src;
36 
37 delete img.dataset.src;
38 delete img.dataset.srcset;
39}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1IntersectionObserver lets you react to viewport proximity without scroll listeners or manual math.
  2. 2Feature-detect and fall back so the page works even where the API is missing.
  3. 3Unobserving after a hit and using once listeners keeps work one-shot and avoids leaks.

Related explainers

javascript
const ROLE_PERMISSIONS = {
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write'],
  manager: ['users:read', 'billing:read'],
  member: ['users:read'],

Role-based permissions middleware in Express

authorization middleware rbac
Intermediate 9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
  const formatter = new Intl.NumberFormat(locale);
  const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
  const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';

Live thousand separators without losing the caret

dom intl caret-preservation
Advanced 8 steps
javascript
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 steps
javascript
const express = require('express');
const app = express();
 
app.get('/health', (req, res) => res.json({ status: 'ok' }));

Graceful shutdown in an Express server

graceful-shutdown signal-handling connection-tracking
Advanced 9 steps
javascript
import { useState, useEffect, useCallback } from 'react';
 
function getColumnCount(width) {
  if (width < 640) return 1;

A responsive column hook in React

custom-hooks debouncing responsive-design
Intermediate 7 steps
javascript
function initCharacterCounter(textarea, options = {}) {
  const maxLength = options.maxLength ?? 280;
  const warnThreshold = options.warnThreshold ?? 0.9;
 

A live character counter for textareas

dom closures accessibility
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

How lazy image loading works — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code