Browsers provide rich native APIs. This article explores the most practical browser APIs for modern web development.
Storage API
LocalStorage and SessionStorage
// Storage utility class
class StorageService {
private storage: Storage;
constructor(type: 'local' | 'session' = 'local') {
this.storage = type === 'local' ? localStorage : sessionStorage;
}
// Set value (supports objects)
set<T>(key: string, value: T): void {
try {
const serialized = JSON.stringify(value);
this.storage.setItem(key, serialized);
} catch (error) {
console.error('Storage set error:', error);
}
}
// Get value (auto-parse)
get<T>(key: string, defaultValue: T | null = null): T | null {
try {
const item = this.storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
}
// Remove
remove(key: string): void {
this.storage.removeItem(key);
}
// Clear all
clear(): void {
this.storage.clear();
}
// Storage with expiry
setWithExpiry<T>(key: string, value: T, ttlMs: number): void {
const item = {
value,
expiry: Date.now() + ttlMs,
};
this.set(key, item);
}
getWithExpiry<T>(key: string): T | null {
const item = this.get<{ value: T; expiry: number }>(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.remove(key);
return null;
}
return item.value;
}
}
// Usage
const storage = new StorageService('local');
storage.set('user', { name: 'John', age: 25 });
storage.setWithExpiry('token', 'abc123', 3600000); // Expires in 1 hour
IndexedDB
// IndexedDB wrapper
class IndexedDBService {
private dbName: string;
private version: number;
private db: IDBDatabase | null = null;
constructor(dbName: string, version = 1) {
this.dbName = dbName;
this.version = version;
}
async open(stores: { name: string; keyPath: string }[]): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
stores.forEach(({ name, keyPath }) => {
if (!db.objectStoreNames.contains(name)) {
db.createObjectStore(name, { keyPath });
}
});
};
});
}
async add<T>(storeName: string, data: T): Promise<IDBValidKey> {
return this.transaction(storeName, 'readwrite', (store) => {
return store.add(data);
});
}
async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
return this.transaction(storeName, 'readonly', (store) => {
return store.get(key);
});
}
async getAll<T>(storeName: string): Promise<T[]> {
return this.transaction(storeName, 'readonly', (store) => {
return store.getAll();
});
}
async put<T>(storeName: string, data: T): Promise<IDBValidKey> {
return this.transaction(storeName, 'readwrite', (store) => {
return store.put(data);
});
}
async delete(storeName: string, key: IDBValidKey): Promise<void> {
return this.transaction(storeName, 'readwrite', (store) => {
return store.delete(key);
});
}
private transaction<T>(
storeName: string,
mode: IDBTransactionMode,
callback: (store: IDBObjectStore) => IDBRequest
): Promise<T> {
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(storeName, mode);
const store = transaction.objectStore(storeName);
const request = callback(store);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Usage
const db = new IndexedDBService('myApp', 1);
await db.open([{ name: 'users', keyPath: 'id' }]);
await db.add('users', { id: 1, name: 'John' });
Intersection Observer
Lazy Loading Images
// Image lazy loading
function lazyLoadImages() {
const images = document.querySelectorAll<HTMLImageElement>('img[data-src]');
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
img.removeAttribute('data-src');
obs.unobserve(img);
}
});
},
{
rootMargin: '50px 0px', // Load 50px before visible
threshold: 0.01,
}
);
images.forEach((img) => observer.observe(img));
return () => observer.disconnect();
}
Infinite Scroll
// Infinite scroll loading
function useInfiniteScroll(
callback: () => Promise<void>,
options?: IntersectionObserverInit
) {
const sentinelRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
async ([entry]) => {
if (entry.isIntersecting && !isLoading) {
setIsLoading(true);
await callback();
setIsLoading(false);
}
},
{ threshold: 0, ...options }
);
observer.observe(sentinel);
return () => observer.disconnect();
}, [callback, isLoading, options]);
return { sentinelRef, isLoading };
}
// Usage
function PostList() {
const [posts, setPosts] = useState([]);
const loadMore = async () => {
const newPosts = await fetchPosts(posts.length);
setPosts((prev) => [...prev, ...newPosts]);
};
const { sentinelRef, isLoading } = useInfiniteScroll(loadMore);
return (
<div>
{posts.map((post) => <PostCard key={post.id} post={post} />)}
<div ref={sentinelRef} />
{isLoading && <Spinner />}
</div>
);
}
Scroll Animations
// Fade in on scroll
function scrollReveal() {
const elements = document.querySelectorAll('[data-reveal]');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
}
});
},
{ threshold: 0.1 }
);
elements.forEach((el) => observer.observe(el));
}
// CSS
// [data-reveal] { opacity: 0; transform: translateY(20px); transition: all 0.6s; }
// [data-reveal].revealed { opacity: 1; transform: translateY(0); }
Web Workers
Basic Worker
// worker.ts
self.onmessage = (event: MessageEvent) => {
const { type, payload } = event.data;
switch (type) {
case 'HEAVY_CALCULATION':
const result = heavyCalculation(payload);
self.postMessage({ type: 'RESULT', payload: result });
break;
case 'PROCESS_DATA':
const processed = processLargeData(payload);
self.postMessage({ type: 'PROCESSED', payload: processed });
break;
}
};
function heavyCalculation(data: number[]): number {
// Simulate expensive computation
return data.reduce((acc, val) => acc + Math.sqrt(val), 0);
}
function processLargeData(data: any[]): any[] {
return data.map((item) => ({
...item,
processed: true,
timestamp: Date.now(),
}));
}
Worker Manager
// Main thread
class WorkerManager {
private worker: Worker;
private callbacks = new Map<string, (data: any) => void>();
constructor(workerPath: string) {
this.worker = new Worker(workerPath, { type: 'module' });
this.worker.onmessage = this.handleMessage.bind(this);
}
private handleMessage(event: MessageEvent) {
const { type, payload } = event.data;
const callback = this.callbacks.get(type);
if (callback) {
callback(payload);
this.callbacks.delete(type);
}
}
send<T>(type: string, payload: any): Promise<T> {
return new Promise((resolve) => {
this.callbacks.set(type.replace('_', '') + '_RESULT', resolve);
this.worker.postMessage({ type, payload });
});
}
terminate() {
this.worker.terminate();
}
}
// Usage
const worker = new WorkerManager('/worker.js');
const result = await worker.send('HEAVY_CALCULATION', [1, 2, 3, 4, 5]);
Geolocation API
// Get current position
async function getCurrentPosition(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation not supported'));
return;
}
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000, // 5 minute cache
});
});
}
// Watch position continuously
function watchPosition(
onUpdate: (position: GeolocationPosition) => void,
onError: (error: GeolocationPositionError) => void
): () => void {
const watchId = navigator.geolocation.watchPosition(onUpdate, onError, {
enableHighAccuracy: true,
});
return () => navigator.geolocation.clearWatch(watchId);
}
// React Hook
function useGeolocation() {
const [position, setPosition] = useState<GeolocationPosition | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const cleanup = watchPosition(setPosition, (err) => {
setError(err.message);
});
return cleanup;
}, []);
return { position, error };
}
Clipboard API
// Copy to clipboard
async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
}
}
// Read from clipboard
async function readFromClipboard(): Promise<string> {
try {
return await navigator.clipboard.readText();
} catch (error) {
throw new Error('Cannot access clipboard');
}
}
// Copy button component
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
const success = await copyToClipboard(text);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<button onClick={handleCopy}>
{copied ? 'Copied!' : 'Copy'}
</button>
);
}
Notification API
// Request notification permission
async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!('Notification' in window)) {
throw new Error('Notifications not supported');
}
if (Notification.permission === 'granted') {
return 'granted';
}
return await Notification.requestPermission();
}
// Send notification
async function sendNotification(
title: string,
options?: NotificationOptions
): Promise<Notification | null> {
const permission = await requestNotificationPermission();
if (permission !== 'granted') {
return null;
}
const notification = new Notification(title, {
icon: '/icon.png',
badge: '/badge.png',
...options,
});
notification.onclick = () => {
window.focus();
notification.close();
};
return notification;
}
// Usage
sendNotification('New Message', {
body: 'You have a new message',
tag: 'message',
requireInteraction: true,
});
ResizeObserver
// Watch element size changes
function useResizeObserver(
ref: RefObject<HTMLElement>,
callback: (entry: ResizeObserverEntry) => void
) {
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver((entries) => {
entries.forEach(callback);
});
observer.observe(element);
return () => observer.disconnect();
}, [ref, callback]);
}
// Responsive component
function ResponsiveComponent() {
const containerRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useResizeObserver(containerRef, (entry) => {
const { width, height } = entry.contentRect;
setSize({ width, height });
});
return (
<div ref={containerRef}>
<p>Width: {size.width}px</p>
<p>Height: {size.height}px</p>
</div>
);
}
Best Practices Summary
Browser API Best Practices:
┌─────────────────────────────────────────────────────┐
│ Compatibility │
│ ├── Check API support before use │
│ ├── Provide fallback solutions │
│ ├── Use polyfills when needed │
│ └── Progressive enhancement │
│ │
│ Performance │
│ ├── Use Workers for heavy tasks │
│ ├── Leverage Observer APIs wisely │
│ ├── Avoid frequent storage operations │
│ └── Clean up listeners promptly │
│ │
│ Security │
│ ├── Validate user input │
│ ├── Handle permission requests │
│ ├── Encrypt sensitive data │
│ └── Follow same-origin policy │
└─────────────────────────────────────────────────────┘
| API | Use Case |
|---|---|
| LocalStorage | Small persistent data |
| IndexedDB | Large structured data |
| Intersection Observer | Lazy loading, infinite scroll |
| Web Workers | CPU-intensive tasks |
| Geolocation | Location-based features |
Browser APIs are the infrastructure of web applications. Leverage native capabilities to reduce third-party dependencies.