Advanced JavaScript (ES6+)

Modern JavaScript APIs

13 min Lesson 39 of 40

Modern JavaScript APIs

Modern browsers provide powerful APIs that enable advanced functionality in web applications. In this lesson, we'll explore essential modern JavaScript APIs that every developer should know.

IntersectionObserver API

The IntersectionObserver API allows you to observe changes in the intersection of an element with an ancestor or viewport:

// Basic IntersectionObserver usage const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { console.log('Element is visible:', entry.target); // Perform action when element becomes visible entry.target.classList.add('visible'); // Optional: stop observing after first intersection observer.unobserve(entry.target); } }); }); // Observe elements const elements = document.querySelectorAll('.observe-me'); elements.forEach(el => observer.observe(el)); // Lazy loading images const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.add('loaded'); imageObserver.unobserve(img); } }); }); document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); // Advanced options const advancedObserver = new IntersectionObserver( (entries) => { entries.forEach(entry => { console.log('Intersection ratio:', entry.intersectionRatio); console.log('Is intersecting:', entry.isIntersecting); // Element is 50% visible if (entry.intersectionRatio >= 0.5) { entry.target.classList.add('half-visible'); } }); }, { root: null, // Use viewport as root rootMargin: '0px 0px -100px 0px', // Trigger 100px before element enters viewport threshold: [0, 0.25, 0.5, 0.75, 1] // Trigger at 0%, 25%, 50%, 75%, 100% visibility } ); // Infinite scroll implementation const loadMoreObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadMoreContent(); } }); }); const sentinel = document.querySelector('#load-more-sentinel'); loadMoreObserver.observe(sentinel); async function loadMoreContent() { const newItems = await fetchMoreItems(); renderItems(newItems); } // Animate elements on scroll const animateObserver = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.animation = 'fadeInUp 0.6s ease-out'; animateObserver.unobserve(entry.target); } }); }, { threshold: 0.1 } ); document.querySelectorAll('.animate-on-scroll').forEach(el => { animateObserver.observe(el); });
Tip: IntersectionObserver is much more performant than scroll event listeners for detecting element visibility. Always prefer it over scroll events when possible.

MutationObserver API

MutationObserver watches for changes to the DOM tree and notifies you when they occur:

// Basic MutationObserver const targetNode = document.getElementById('container'); const mutationObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { console.log('Type:', mutation.type); if (mutation.type === 'childList') { console.log('Added nodes:', mutation.addedNodes); console.log('Removed nodes:', mutation.removedNodes); } if (mutation.type === 'attributes') { console.log('Attribute changed:', mutation.attributeName); console.log('Old value:', mutation.oldValue); } if (mutation.type === 'characterData') { console.log('Text content changed'); } }); }); // Configuration const config = { childList: true, // Watch for child node additions/removals attributes: true, // Watch for attribute changes characterData: true, // Watch for text content changes subtree: true, // Watch all descendants attributeOldValue: true, // Record previous attribute values characterDataOldValue: true // Record previous text content }; // Start observing mutationObserver.observe(targetNode, config); // Stop observing // mutationObserver.disconnect(); // Practical example: Detect when element is added to DOM function waitForElement(selector) { return new Promise((resolve) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } // Usage waitForElement('#dynamic-element').then(element => { console.log('Element found:', element); }); // Monitor class changes function onClassChange(element, callback) { const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.attributeName === 'class') { callback(element.className); } }); }); observer.observe(element, { attributes: true, attributeFilter: ['class'] }); return observer; } // Usage const button = document.querySelector('#my-button'); onClassChange(button, (classes) => { console.log('Classes changed:', classes); }); // Auto-save content changes const editor = document.querySelector('#editor'); let saveTimeout; const saveObserver = new MutationObserver(() => { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { saveContent(editor.innerHTML); }, 1000); }); saveObserver.observe(editor, { childList: true, characterData: true, subtree: true });

ResizeObserver API

ResizeObserver reports changes to the dimensions of an element:

// Basic ResizeObserver const resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { const { width, height } = entry.contentRect; console.log(`Element resized: ${width}x${height}`); // Respond to size changes if (width < 600) { entry.target.classList.add('mobile'); } else { entry.target.classList.remove('mobile'); } }); }); const container = document.querySelector('#responsive-container'); resizeObserver.observe(container); // Responsive component class ResponsiveChart { constructor(element) { this.element = element; this.chart = null; this.resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { const { width, height } = entry.contentRect; this.updateChartSize(width, height); }); }); this.resizeObserver.observe(this.element); } updateChartSize(width, height) { if (this.chart) { this.chart.resize(width, height); } } destroy() { this.resizeObserver.disconnect(); } } // Responsive text sizing const textResizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { const { width } = entry.contentRect; const fontSize = Math.max(16, width / 30); entry.target.style.fontSize = `${fontSize}px`; }); }); document.querySelectorAll('.responsive-text').forEach(el => { textResizeObserver.observe(el); }); // Monitor element box size changes const boxObserver = new ResizeObserver((entries) => { entries.forEach(entry => { // Different box sizes const contentBox = entry.contentBoxSize[0]; const borderBox = entry.borderBoxSize[0]; console.log('Content box:', { width: contentBox.inlineSize, height: contentBox.blockSize }); console.log('Border box:', { width: borderBox.inlineSize, height: borderBox.blockSize }); }); }); // Cleanup // resizeObserver.disconnect();
Note: ResizeObserver is more efficient than window resize events for monitoring individual element sizes. It only fires when the observed element actually changes size.

Web Workers

Web Workers allow you to run JavaScript in background threads, keeping the UI responsive:

// Main thread (main.js) const worker = new Worker('worker.js'); // Send data to worker worker.postMessage({ action: 'calculate', data: [1, 2, 3, 4, 5] }); // Receive results from worker worker.onmessage = (event) => { console.log('Result from worker:', event.data); }; // Handle errors worker.onerror = (error) => { console.error('Worker error:', error); }; // Terminate worker when done worker.terminate(); // Worker file (worker.js) self.onmessage = (event) => { const { action, data } = event.data; if (action === 'calculate') { // Heavy computation const result = performHeavyCalculation(data); // Send result back to main thread self.postMessage(result); } }; function performHeavyCalculation(data) { // Simulate expensive operation let sum = 0; for (let i = 0; i < 1000000000; i++) { sum += data.reduce((a, b) => a + b, 0); } return sum; } // Practical example: Image processing in worker // main.js const imageWorker = new Worker('image-worker.js'); async function processImage(imageFile) { const imageBitmap = await createImageBitmap(imageFile); imageWorker.postMessage( { image: imageBitmap, filter: 'grayscale' }, [imageBitmap] // Transfer ownership ); } imageWorker.onmessage = (event) => { const processedImage = event.data; displayImage(processedImage); }; // Shared Workers (shared between tabs/windows) const sharedWorker = new SharedWorker('shared-worker.js'); sharedWorker.port.start(); sharedWorker.port.postMessage('Hello from tab'); sharedWorker.port.onmessage = (event) => { console.log('Message from shared worker:', event.data); };

LocalStorage and SessionStorage

Web Storage APIs provide simple key-value storage in the browser:

// LocalStorage (persists after browser closes) // Store data localStorage.setItem('username', 'john_doe'); localStorage.setItem('theme', 'dark'); // Store objects (must stringify) const user = { name: 'John', age: 30 }; localStorage.setItem('user', JSON.stringify(user)); // Retrieve data const username = localStorage.getItem('username'); const theme = localStorage.getItem('theme'); // Retrieve and parse object const storedUser = JSON.parse(localStorage.getItem('user')); // Remove item localStorage.removeItem('theme'); // Clear all localStorage.clear(); // Check if key exists if (localStorage.getItem('username')) { console.log('Username exists'); } // Get number of items console.log('Items in storage:', localStorage.length); // Iterate over items for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); const value = localStorage.getItem(key); console.log(`${key}: ${value}`); } // SessionStorage (cleared when tab closes) sessionStorage.setItem('sessionId', 'abc123'); const sessionId = sessionStorage.getItem('sessionId'); // Storage utility class class Storage { constructor(storage = localStorage) { this.storage = storage; } set(key, value) { try { this.storage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error('Storage error:', error); return false; } } get(key, defaultValue = null) { try { const item = this.storage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.error('Storage error:', error); return defaultValue; } } remove(key) { this.storage.removeItem(key); } clear() { this.storage.clear(); } has(key) { return this.storage.getItem(key) !== null; } } // Usage const store = new Storage(); store.set('user', { name: 'John', age: 30 }); const user = store.get('user'); // Listen for storage changes (from other tabs) window.addEventListener('storage', (event) => { console.log('Storage changed:', { key: event.key, oldValue: event.oldValue, newValue: event.newValue, url: event.url }); });
Warning: LocalStorage has a size limit (typically 5-10MB) and is synchronous, which can block the main thread. For large amounts of data, use IndexedDB instead.

IndexedDB Basics

IndexedDB is a low-level API for storing large amounts of structured data:

// Open database const request = indexedDB.open('MyDatabase', 1); // Handle database upgrade (first time or version change) request.onupgradeneeded = (event) => { const db = event.target.result; // Create object store const objectStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true }); // Create indexes objectStore.createIndex('email', 'email', { unique: true }); objectStore.createIndex('name', 'name', { unique: false }); }; // Handle successful open request.onsuccess = (event) => { const db = event.target.result; // Add data const transaction = db.transaction(['users'], 'readwrite'); const objectStore = transaction.objectStore('users'); const user = { name: 'John Doe', email: 'john@example.com', age: 30 }; const addRequest = objectStore.add(user); addRequest.onsuccess = () => { console.log('User added with ID:', addRequest.result); }; // Get data const getRequest = objectStore.get(1); getRequest.onsuccess = () => { console.log('User:', getRequest.result); }; // Update data const updateRequest = objectStore.put({ id: 1, name: 'Jane Doe', email: 'jane@example.com', age: 28 }); // Delete data const deleteRequest = objectStore.delete(1); // Get all data const getAllRequest = objectStore.getAll(); getAllRequest.onsuccess = () => { console.log('All users:', getAllRequest.result); }; // Search by index const emailIndex = objectStore.index('email'); const emailRequest = emailIndex.get('john@example.com'); emailRequest.onsuccess = () => { console.log('User by email:', emailRequest.result); }; }; // Error handling request.onerror = (event) => { console.error('Database error:', event.target.error); }; // IndexedDB wrapper class (simplified) class IDBStore { constructor(dbName, storeName) { this.dbName = dbName; this.storeName = storeName; this.db = null; } async open() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onsuccess = () => { this.db = request.result; resolve(); }; request.onerror = () => reject(request.error); }); } async add(data) { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); return new Promise((resolve, reject) => { const request = store.add(data); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async get(id) { const transaction = this.db.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); return new Promise((resolve, reject) => { const request = store.get(id); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } }

Service Workers Introduction

Service Workers enable offline functionality and background processing:

// Register service worker (in main.js) if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker registered:', registration); }) .catch(error => { console.error('Service Worker registration failed:', error); }); } // Service Worker file (sw.js) const CACHE_NAME = 'my-app-v1'; const urlsToCache = [ '/', '/styles.css', '/script.js', '/logo.png' ]; // Install event - cache resources self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Caching files'); return cache.addAll(urlsToCache); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME) { console.log('Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) ); }); // Fetch event - serve from cache or network self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then(response => { // Return cached version or fetch from network return response || fetch(event.request); }) ); }); // Update service worker navigator.serviceWorker.getRegistration().then(registration => { registration.update(); }); // Listen for updates navigator.serviceWorker.addEventListener('controllerchange', () => { console.log('New service worker activated'); window.location.reload(); });

Practice Exercise:

Create a lazy-loading image gallery with IntersectionObserver:

// HTML structure // <div class="gallery"> // <img data-src="image1.jpg" alt="Image 1"> // <img data-src="image2.jpg" alt="Image 2"> // <!-- More images --> // </div> class LazyGallery { constructor(selector) { this.images = document.querySelectorAll(`${selector} img[data-src]`); this.observer = null; this.init(); } init() { this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { this.loadImage(entry.target); } }); }, { rootMargin: '50px' // Start loading 50px before visible } ); this.images.forEach(img => { this.observer.observe(img); }); } loadImage(img) { const src = img.dataset.src; // Create temporary image to preload const tempImg = new Image(); tempImg.onload = () => { img.src = src; img.classList.add('loaded'); this.observer.unobserve(img); }; tempImg.onerror = () => { img.classList.add('error'); this.observer.unobserve(img); }; tempImg.src = src; } destroy() { this.observer.disconnect(); } } // Usage const gallery = new LazyGallery('.gallery');

Summary

In this lesson, you learned:

  • IntersectionObserver for visibility detection and lazy loading
  • MutationObserver for watching DOM changes
  • ResizeObserver for responsive element sizing
  • Web Workers for parallel processing
  • LocalStorage and SessionStorage for simple data persistence
  • IndexedDB basics for large-scale data storage
  • Service Workers for offline capabilities
Next Up: In the final lesson, we'll bring everything together with a comprehensive real-world project and explore your next steps in JavaScript development!