1. DOM Manipulation and Selection APIs

1.1 Document and Element Selection Methods

Method Syntax Description Browser Support
getElementById document.getElementById(id) Returns element with specified ID. Returns null if not found. Most performant selector. All Browsers
getElementsByClassName element.getElementsByClassName(names) Returns live HTMLCollection of elements with specified class names (space-separated). Updates automatically when DOM changes. All Browsers
getElementsByTagName element.getElementsByTagName(name) Returns live HTMLCollection of elements with specified tag name. Use "*" for all elements. All Browsers
querySelector element.querySelector(selector) Returns first element matching CSS selector. Returns null if no match. Static snapshot. All Browsers
querySelectorAll element.querySelectorAll(selector) Returns static NodeList of all matching elements. Use forEach() to iterate. Non-live collection. All Browsers
getElementsByName document.getElementsByName(name) Returns live NodeList of elements with specified name attribute. Primarily for form elements. All Browsers
closest element.closest(selector) Traverses element and ancestors, returns first matching element. Returns null if no match. Modern Browsers
matches element.matches(selector) Returns boolean indicating if element matches CSS selector. Useful for event delegation. Modern Browsers

Example: Selection methods comparison

// Get by ID - fastest
const header = document.getElementById("header");

// Query selector - flexible CSS selectors
const firstButton = document.querySelector(".btn-primary");
const allButtons = document.querySelectorAll("button.active");

// Live collections - auto-update
const items = document.getElementsByClassName("item");
console.log(items.length); // 5
document.body.appendChild(newItem); // Adds .item element
console.log(items.length); // 6 - auto-updated

// Closest for ancestor lookup
const card = button.closest(".card");

// Matches for filtering
if (element.matches("[data-active='true']")) {
  // Element has attribute
}
Note: querySelector returns static NodeList while getElementsByClassName returns live HTMLCollection. Live collections automatically update when DOM changes. Use querySelector for static snapshots to avoid performance issues.

1.2 DOM Node Creation and Manipulation

Method Syntax Description Use Case
createElement document.createElement(tagName) Creates new element node with specified tag name. Element not in DOM until appended. Dynamic content generation
createTextNode document.createTextNode(data) Creates text node with specified string. Automatically escapes HTML entities. Safe text insertion
createDocumentFragment document.createDocumentFragment() Creates lightweight document fragment. Batch DOM operations for performance. Bulk insertions
appendChild parent.appendChild(child) Appends node as last child. Moves node if already in DOM. Returns appended node. Add to end of parent
insertBefore parent.insertBefore(new, ref) Inserts node before reference node. Use null as ref to append at end. Insert at specific position
replaceChild parent.replaceChild(new, old) Replaces old child with new node. Returns replaced node. Swap elements
removeChild parent.removeChild(child) Removes child node from parent. Returns removed node. Throws error if not child. Remove specific child
cloneNode node.cloneNode(deep) Creates copy of node. deep=true clones descendants. IDs are duplicated. Duplicate elements
append NEW parent.append(...nodes) Appends multiple nodes or strings. Accepts DOMString. No return value. Modern bulk append
prepend NEW parent.prepend(...nodes) Inserts nodes before first child. Accepts multiple arguments. Add to beginning
before NEW element.before(...nodes) Inserts nodes before element in parent's child list. Insert before sibling
after NEW element.after(...nodes) Inserts nodes after element in parent's child list. Insert after sibling
replaceWith NEW element.replaceWith(...nodes) Replaces element with nodes/strings. More convenient than replaceChild. Modern replacement
remove NEW element.remove() Removes element from DOM tree. No parent reference needed. Self-removal

Example: Creating and inserting elements

// Traditional approach
const div = document.createElement("div");
div.textContent = "Hello";
document.body.appendChild(div);

// Modern approach - multiple nodes
const container = document.querySelector("#container");
container.append("Text", document.createElement("span"), "More text");

// Document fragment for performance
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const item = document.createElement("li");
  item.textContent = `Item ${i}`;
  fragment.appendChild(item);
}
list.appendChild(fragment); // Single reflow

// Clone with deep copy
const original = document.querySelector(".template");
const copy = original.cloneNode(true);
copy.id = "new-id"; // Change duplicate ID
document.body.appendChild(copy);

// Modern insertion methods
element.before("Before"); // Insert before
element.after("After");   // Insert after
element.replaceWith("Replacement"); // Replace
element.remove(); // Remove from DOM
Warning: Using appendChild on an existing DOM node will move it, not copy it. Use cloneNode() first to duplicate. Document fragments are destroyed after insertion - create new one for each batch operation.

1.3 Element Attributes and Properties

Method/Property Syntax Description Use Case
getAttribute element.getAttribute(name) Returns attribute value as string. Returns null if attribute doesn't exist. Read custom attributes
setAttribute element.setAttribute(name, value) Sets attribute to specified value. Creates attribute if doesn't exist. Converts value to string. Set any attribute
removeAttribute element.removeAttribute(name) Removes attribute completely. No error if attribute doesn't exist. Remove attributes
hasAttribute element.hasAttribute(name) Returns boolean indicating if element has specified attribute. Check attribute existence
toggleAttribute NEW element.toggleAttribute(name, force) Toggles boolean attribute. Optional force parameter adds/removes explicitly. Toggle boolean attributes
attributes element.attributes Returns live NamedNodeMap of all attributes. Use .name and .value properties. Iterate all attributes
dataset element.dataset DOMStringMap of data-* attributes. Converts kebab-case to camelCase. Read/write custom data. Custom data attributes
id element.id Direct property access to element ID. Faster than getAttribute. Reflects in HTML. ID manipulation
className element.className Gets/sets class attribute as string. Space-separated for multiple classes. Bulk class replacement
innerHTML element.innerHTML Gets/sets HTML content as string. Parses HTML. XSS risk with user input. Set HTML content
outerHTML element.outerHTML Gets/sets element and its content. Setting replaces entire element. Replace element with HTML
textContent element.textContent Gets/sets text content. Strips HTML tags. Safe from XSS. Faster than innerText. Safe text manipulation
innerText element.innerText Gets/sets rendered text. Respects CSS styling. Triggers reflow. Slower than textContent. Visible text only
value input.value Gets/sets form input value. For inputs, textareas, selects. Live property. Form field values
checked input.checked Boolean for checkbox/radio state. Use property, not attribute for current state. Checkbox/radio state

Example: Attribute manipulation

// Get/set attributes
const link = document.querySelector("a");
link.getAttribute("href"); // Get
link.setAttribute("href", "/new"); // Set
link.removeAttribute("target"); // Remove

// Has attribute check
if (button.hasAttribute("disabled")) {
  console.log("Button is disabled");
}

// Toggle attribute
button.toggleAttribute("disabled");
button.toggleAttribute("hidden", true); // Force add

// Data attributes
element.setAttribute("data-user-id", "123");
console.log(element.dataset.userId); // "123"
element.dataset.userName = "John"; // Creates data-user-name

Example: Content manipulation

// innerHTML - parses HTML (XSS risk)
div.innerHTML = "<strong>Bold</strong>";

// textContent - safe, no parsing
div.textContent = "<script>alert('safe')</script>";
// Displays as literal text

// innerText vs textContent
div.innerHTML = "<span style='display:none'>Hidden</span>Text";
console.log(div.textContent); // "HiddenText"
console.log(div.innerText);   // "Text" (respects CSS)

// Form values
input.value = "New value";
checkbox.checked = true;
select.value = "option2";
Warning: Never use innerHTML with unsanitized user input - creates XSS vulnerability. Use textContent or createTextNode() for user-generated content. Properties like checked and value reflect current state, while attributes show initial HTML values.

1.4 CSS Classes and Styling Manipulation

Property/Method Syntax Description Use Case
classList.add element.classList.add(...tokens) Adds one or more class names. No duplicates. Does nothing if class exists. Add classes
classList.remove element.classList.remove(...tokens) Removes one or more class names. No error if class doesn't exist. Remove classes
classList.toggle element.classList.toggle(token, force) Toggles class. Returns boolean indicating if class is present after operation. Optional force parameter. Toggle states
classList.contains element.classList.contains(token) Returns boolean indicating if class exists. Case-sensitive check. Check class existence
classList.replace element.classList.replace(old, new) Replaces old class with new class. Returns boolean indicating if replacement occurred. Swap classes
classList.item element.classList.item(index) Returns class name at specified index. Returns null if out of bounds. Access by index
classList.length element.classList.length Returns number of classes. Read-only property. Count classes
style element.style.property Inline styles as CSSStyleDeclaration. Use camelCase for CSS properties. High specificity. Inline styles
style.cssText element.style.cssText Gets/sets all inline styles as string. Replaces existing inline styles when set. Bulk style setting
style.setProperty style.setProperty(prop, value, priority) Sets CSS property. Accepts kebab-case. Optional "important" priority. Set with priority
style.removeProperty style.removeProperty(property) Removes inline style property. Returns removed value. Remove inline styles
style.getPropertyValue style.getPropertyValue(property) Gets CSS property value. Accepts kebab-case property names. Read property value
getComputedStyle getComputedStyle(element, pseudo) Returns live CSSStyleDeclaration of computed styles. Includes inherited/default values. Read-only. Read final styles
attributeStyleMap NEW element.attributeStyleMap CSS Typed OM for type-safe style manipulation. Returns StylePropertyMap. Modern typed styles

Example: Class manipulation with classList

// Add single or multiple classes
element.classList.add("active");
element.classList.add("highlight", "bold", "large");

// Remove classes
element.classList.remove("inactive");

// Toggle class
const isActive = element.classList.toggle("active");
console.log(isActive); // true if added, false if removed

// Force toggle
element.classList.toggle("active", true);  // Always add
element.classList.toggle("active", false); // Always remove

// Check if class exists
if (element.classList.contains("active")) {
  console.log("Element is active");
}

// Replace class
element.classList.replace("old-theme", "new-theme");

// Iterate classes
for (let className of element.classList) {
  console.log(className);
}

Example: Style manipulation

// Direct style property (camelCase)
element.style.backgroundColor = "#ff0000";
element.style.fontSize = "16px";
element.style.marginTop = "20px";

// Set multiple styles at once
element.style.cssText = "color: blue; font-size: 14px; padding: 10px;";

// Set with priority
element.style.setProperty("color", "red", "important");

// Remove inline style
element.style.removeProperty("background-color");

// Get computed styles (read-only)
const computed = getComputedStyle(element);
console.log(computed.color); // "rgb(255, 0, 0)"
console.log(computed.fontSize); // "16px"

// Get pseudo-element styles
const beforeStyles = getComputedStyle(element, "::before");
console.log(beforeStyles.content);

// CSS Typed OM (modern)
element.attributeStyleMap.set("opacity", 0.5);
const opacity = element.attributeStyleMap.get("opacity");
Note: Prefer classList over className for class manipulation - it's more reliable and prevents accidentally removing other classes. Use CSS classes for styling instead of inline styles when possible for better separation of concerns and maintainability.

1.5 DOM Traversal and Navigation Methods

Property Description Returns Notes
parentNode Returns parent node of element Node or null Includes non-element nodes
parentElement Returns parent element of element Element or null Element nodes only
childNodes Live NodeList of all child nodes NodeList Includes text, comment nodes
children Live HTMLCollection of child elements HTMLCollection Element nodes only
firstChild First child node Node or null May be text/comment node
lastChild Last child node Node or null May be text/comment node
firstElementChild First child element Element or null Skips text/comment nodes
lastElementChild Last child element Element or null Skips text/comment nodes
nextSibling Next sibling node Node or null May be text/comment node
previousSibling Previous sibling node Node or null May be text/comment node
nextElementSibling Next sibling element Element or null Skips text/comment nodes
previousElementSibling Previous sibling element Element or null Skips text/comment nodes
childElementCount Number of child elements Number Equivalent to children.length
ownerDocument Document object that contains node Document Useful in iframes
nodeType Type of node Number (1-12) 1=Element, 3=Text, 8=Comment
nodeName Name of node String Uppercase for elements
nodeValue Value of node String or null null for elements

Example: DOM traversal patterns

// Parent navigation
const parent = element.parentElement;
const grandparent = element.parentElement.parentElement;

// Find closest ancestor with class
const container = element.closest(".container");

// Child navigation
const allChildren = parent.children; // HTMLCollection of elements
const firstChild = parent.firstElementChild;
const lastChild = parent.lastElementChild;

// Sibling navigation
const next = element.nextElementSibling;
const previous = element.previousElementSibling;

// Iterate all child elements
for (let child of parent.children) {
  console.log(child.tagName);
}

// Node type checking
if (node.nodeType === Node.ELEMENT_NODE) {
  console.log("Element node");
} else if (node.nodeType === Node.TEXT_NODE) {
  console.log("Text node");
}

// Walk entire tree
function walkDOM(node, callback) {
  callback(node);
  for (let child of node.children) {
    walkDOM(child, callback);
  }
}

walkDOM(document.body, (element) => {
  console.log(element.tagName);
});
Note: Use Element variants (firstElementChild, nextElementSibling) instead of node variants to skip text and comment nodes. This is especially important when HTML has whitespace between elements. children is often more useful than childNodes for the same reason.

1.6 Mutation Observer for DOM Changes

Feature Syntax/Property Description Use Case
MutationObserver Constructor new MutationObserver(callback) Creates new observer with callback function. Callback receives array of MutationRecord objects. Initialize observer
observe observer.observe(target, options) Starts observing target node. Options configure what changes to watch. Can observe multiple targets. Start watching changes
disconnect observer.disconnect() Stops observing all targets. Clears pending notifications. Re-observe to resume. Stop watching
takeRecords observer.takeRecords() Returns array of pending mutations not yet processed. Clears mutation queue. Get pending changes
Option Type Description Required
childList boolean Watch for addition/removal of child nodes At least one must be true
attributes boolean Watch for attribute changes on target Optional
characterData boolean Watch for text content changes in target Optional
subtree boolean Extend observation to entire subtree. Applies all options to descendants. Optional
attributeOldValue boolean Record previous attribute value. Requires attributes: true. Optional
characterDataOldValue boolean Record previous text value. Requires characterData: true. Optional
attributeFilter string[] Array of attribute names to watch. Omit to watch all attributes. Optional
MutationRecord Property Type Description
type string Type of mutation: "attributes", "characterData", or "childList"
target Node Node affected by mutation
addedNodes NodeList Nodes added (for childList mutations)
removedNodes NodeList Nodes removed (for childList mutations)
previousSibling Node Previous sibling of added/removed nodes
nextSibling Node Next sibling of added/removed nodes
attributeName string Name of changed attribute
attributeNamespace string Namespace of changed attribute
oldValue string Previous value (if oldValue option enabled)

Example: Basic mutation observer usage

// Create observer
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    console.log("Mutation type:", mutation.type);
    console.log("Target:", mutation.target);
    
    if (mutation.type === "childList") {
      console.log("Added:", mutation.addedNodes);
      console.log("Removed:", mutation.removedNodes);
    }
    
    if (mutation.type === "attributes") {
      console.log("Attribute:", mutation.attributeName);
      console.log("Old value:", mutation.oldValue);
    }
  });
});

// Configure and start observing
const config = {
  "childList": true,
  "attributes": true,
  "attributeOldValue": true,
  "subtree": true
};

observer.observe(document.body, config);

// Later: stop observing
observer.disconnect();

Example: Practical use cases

// Watch for specific attribute changes
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.attributeName === "data-status") {
      console.log("Status changed to:", mutation.target.dataset.status);
    }
  });
});

observer.observe(element, {
  "attributes": true,
  "attributeFilter": ["data-status", "class"]
});

// Detect when element is added to DOM
function whenElementAdded(selector, callback) {
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.matches && node.matches(selector)) {
          callback(node);
        }
      });
    });
  });
  
  observer.observe(document.body, {
    "childList": true,
    "subtree": true
  });
  
  return observer;
}

// Usage
const obs = whenElementAdded(".dynamic-content", (element) => {
  console.log("Dynamic content appeared:", element);
  obs.disconnect(); // Stop after first match
});

// React to DOM changes with debouncing
let timeoutId;
const debouncedObserver = new MutationObserver(() => {
  clearTimeout(timeoutId);
  timeoutId = setTimeout(() => {
    console.log("DOM stable after changes");
    updateUI();
  }, 300);
});

debouncedObserver.observe(container, {
  "childList": true,
  "subtree": true
});
Note: MutationObserver is asynchronous and batches mutations for performance. The callback receives all mutations since last callback. Always disconnect observers when no longer needed to prevent memory leaks. Use attributeFilter to limit observations to specific attributes for better performance.
Warning: Be careful not to modify the DOM inside the observer callback in ways that trigger the same observer - this creates infinite loops. Use disconnect() before making changes and observe() after, or use a flag to prevent recursion.

2. Event Handling and Interaction APIs

2.1 Event Listener Registration and Removal

Method Syntax Description Browser Support
addEventListener target.addEventListener(type, listener, options) Registers event listener on target. Supports multiple listeners per event. Third parameter is options object or boolean (useCapture). All Browsers
removeEventListener target.removeEventListener(type, listener, options) Removes event listener. Must match exact function reference and options used in addEventListener. All Browsers
dispatchEvent target.dispatchEvent(event) Dispatches event to target. Returns false if event is cancelable and any handler called preventDefault(). All Browsers
Option Type Description Default
capture boolean If true, listener fires during capture phase before target phase false
once NEW boolean If true, listener automatically removed after first invocation false
passive NEW boolean If true, listener will never call preventDefault(). Improves scroll performance. false
signal NEW AbortSignal AbortSignal to remove listener when signal is aborted. Modern alternative to removeEventListener. undefined

Example: Event listener registration patterns

// Basic event listener
button.addEventListener("click", (event) => {
  console.log("Button clicked!", event);
});

// Options object - once option
button.addEventListener("click", handleClick, { "once": true });
// Automatically removed after first click

// Passive listener for scroll performance
document.addEventListener("scroll", handleScroll, { "passive": true });
// Cannot call event.preventDefault()

// Capture phase
parent.addEventListener("click", handleCapture, { "capture": true });
// Fires before child's event

// Remove event listener
const handler = (e) => console.log(e);
button.addEventListener("click", handler);
button.removeEventListener("click", handler); // Must use same function reference

// AbortController for easy removal
const controller = new AbortController();
button.addEventListener("click", handleClick, { "signal": controller.signal });
button2.addEventListener("click", handleClick2, { "signal": controller.signal });
// Remove all listeners at once
controller.abort();
Note: Use passive: true for scroll and touch events to improve performance - browser won't wait for listener to finish before scrolling. The once option is perfect for one-time events like splash screens or initial setup. AbortController provides a clean way to remove multiple listeners.

2.2 Event Object Properties and Methods

Property/Method Type Description Use Case
type string Event type (e.g., "click", "keydown"). Read-only. Identify event type
target EventTarget Element that originated event. Remains same through bubbling. Read-only. Get original target
currentTarget EventTarget Element with listener attached. Changes during bubbling/capture. Read-only. Get current handler element
eventPhase number Current phase: 0=NONE, 1=CAPTURING, 2=AT_TARGET, 3=BUBBLING. Read-only. Determine event phase
bubbles boolean Indicates if event bubbles up DOM tree. Read-only. Check if event bubbles
cancelable boolean Indicates if event can be canceled. Read-only. Check if cancelable
defaultPrevented boolean True if preventDefault() was called. Read-only. Check if default prevented
timeStamp number Time event was created (milliseconds since page load). Read-only. Measure timing
isTrusted boolean True if event initiated by user action, false if created by script. Read-only. Verify user interaction
preventDefault() void Cancels default browser action if event is cancelable. Does nothing if not cancelable. Prevent default behavior
stopPropagation() void Stops event from bubbling/capturing to other elements. Current element's listeners still fire. Stop event propagation
stopImmediatePropagation() void Stops propagation and prevents other listeners on current element from firing. Stop all propagation
composedPath() EventTarget[] Returns array of elements event will/did traverse. Includes shadow DOM elements. Get event path

Example: Event object properties

document.addEventListener("click", (event) => {
  // Event identification
  console.log("Type:", event.type); // "click"
  console.log("Target:", event.target); // Element clicked
  console.log("Current:", event.currentTarget); // document
  
  // Event characteristics
  console.log("Bubbles:", event.bubbles); // true
  console.log("Cancelable:", event.cancelable); // true
  console.log("Trusted:", event.isTrusted); // true (user action)
  console.log("Time:", event.timeStamp); // 1234.567
  
  // Event path
  const path = event.composedPath();
  console.log("Path:", path); // [target, parent, ..., window]
  
  // Check phase
  if (event.eventPhase === Event.AT_TARGET) {
    console.log("At target element");
  }
});

Example: Controlling event behavior

// Prevent default link navigation
link.addEventListener("click", (event) => {
  event.preventDefault();
  console.log("Navigation prevented");
});

// Stop event bubbling
child.addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("Won't bubble to parent");
});

// Stop all propagation
element.addEventListener("click", (event) => {
  event.stopImmediatePropagation();
  console.log("No other listeners will fire");
});

// Check if default was prevented
form.addEventListener("submit", (event) => {
  if (!event.defaultPrevented) {
    console.log("Form will submit");
  }
});
Warning: stopPropagation() prevents parent handlers from executing, which can break event delegation patterns. Use with caution. event.target is the original element while event.currentTarget changes during propagation - common source of bugs.

2.3 Custom Event Creation and Dispatching

Constructor Syntax Description Use Case
Event new Event(type, options) Creates basic event. Options: bubbles, cancelable, composed. Simple custom events
CustomEvent new CustomEvent(type, options) Creates event with custom data. Adds detail property to options. Events with data payload
MouseEvent new MouseEvent(type, options) Creates mouse event with coordinates, buttons, modifiers. Includes clientX, clientY, button, etc. Simulate mouse events
KeyboardEvent new KeyboardEvent(type, options) Creates keyboard event. Options: key, code, keyCode, ctrlKey, shiftKey, altKey, metaKey. Simulate keyboard events
FocusEvent new FocusEvent(type, options) Creates focus event. Includes relatedTarget for element losing/gaining focus. Simulate focus events
InputEvent new InputEvent(type, options) Creates input event. Options: data, inputType, isComposing. For text input simulation. Simulate text input
Event Option Type Description Default
bubbles boolean Whether event bubbles up DOM tree false
cancelable boolean Whether event can be canceled with preventDefault() false
composed boolean Whether event propagates across shadow DOM boundary false
detail any Custom data for CustomEvent. Accessible via event.detail. null

Example: Creating and dispatching custom events

// Simple custom event
const simpleEvent = new Event("userLogin", {
  "bubbles": true,
  "cancelable": true
});
element.dispatchEvent(simpleEvent);

// CustomEvent with data payload
const customEvent = new CustomEvent("dataUpdate", {
  "bubbles": true,
  "detail": {
    "userId": 123,
    "timestamp": Date.now(),
    "changes": ["name", "email"]
  }
});
element.dispatchEvent(customEvent);

// Listen for custom event
element.addEventListener("dataUpdate", (event) => {
  console.log("User ID:", event.detail.userId);
  console.log("Changes:", event.detail.changes);
});

// Dispatch with return value check
const event = new CustomEvent("beforeSave", {
  "cancelable": true,
  "detail": { "data": formData }
});
const allowed = element.dispatchEvent(event);
if (!allowed) {
  console.log("Save was prevented by listener");
}

Example: Simulating native events

// Simulate mouse click
const clickEvent = new MouseEvent("click", {
  "bubbles": true,
  "cancelable": true,
  "clientX": 100,
  "clientY": 200,
  "button": 0 // Left button
});
button.dispatchEvent(clickEvent);

// Simulate keyboard event
const keyEvent = new KeyboardEvent("keydown", {
  "key": "Enter",
  "code": "Enter",
  "keyCode": 13,
  "bubbles": true,
  "cancelable": true,
  "ctrlKey": false,
  "shiftKey": false
});
input.dispatchEvent(keyEvent);

// Simulate input event
const inputEvent = new InputEvent("input", {
  "bubbles": true,
  "data": "text",
  "inputType": "insertText"
});
textarea.dispatchEvent(inputEvent);
Note: Use CustomEvent instead of Event when you need to pass data - the detail property is the standard way to attach custom data. Always set bubbles: true unless you specifically want to prevent event propagation. For component communication, custom events are cleaner than callbacks.

2.4 Touch and Pointer Event APIs

Event Type Description Fires When Browser Support
pointerdown Pointer becomes active (mouse, touch, pen) Button pressed, screen touched, pen contact Modern Browsers
pointerup Pointer becomes inactive Button released, touch ended, pen lifted Modern Browsers
pointermove Pointer position changes Mouse/touch/pen moves Modern Browsers
pointercancel Pointer event canceled by browser Touch interrupted, too many touches Modern Browsers
pointerenter Pointer enters element boundary Cursor enters, touch starts in element. Doesn't bubble. Modern Browsers
pointerleave Pointer leaves element boundary Cursor leaves, touch ends outside. Doesn't bubble. Modern Browsers
touchstart Touch point placed on surface Finger touches screen Mobile Browsers
touchmove Touch point moves along surface Finger drags on screen Mobile Browsers
touchend Touch point removed from surface Finger lifted from screen Mobile Browsers
touchcancel Touch point disrupted Touch interrupted by system (call, alert) Mobile Browsers
PointerEvent Property Type Description
pointerId number Unique identifier for pointer. Same pointer keeps same ID through interaction.
pointerType string Type of pointer: "mouse", "touch", "pen"
isPrimary boolean True if primary pointer of type (first touch, main mouse)
pressure number Pressure normalized to 0-1. 0.5 for devices without pressure.
tiltX / tiltY number Angle of pen/stylus in degrees (-90 to 90)
width / height number Contact geometry in CSS pixels
clientX / clientY number Coordinates relative to viewport
pageX / pageY number Coordinates relative to document
screenX / screenY number Coordinates relative to screen
TouchEvent Property Type Description
touches TouchList All current touches on surface
targetTouches TouchList Touches on current target element
changedTouches TouchList Touches that triggered this event

Example: Pointer events for unified input handling

// Unified pointer handling (mouse, touch, pen)
element.addEventListener("pointerdown", (event) => {
  console.log("Pointer type:", event.pointerType); // "mouse", "touch", "pen"
  console.log("Pointer ID:", event.pointerId);
  console.log("Primary:", event.isPrimary);
  console.log("Pressure:", event.pressure);
  console.log("Position:", event.clientX, event.clientY);
  
  // Capture pointer for drag operation
  element.setPointerCapture(event.pointerId);
});

element.addEventListener("pointermove", (event) => {
  if (event.pointerType === "touch") {
    // Handle touch-specific behavior
  }
});

element.addEventListener("pointerup", (event) => {
  element.releasePointerCapture(event.pointerId);
});

// Prevent touch scrolling while drawing
canvas.addEventListener("touchstart", (event) => {
  event.preventDefault();
}, { "passive": false });

Example: Multi-touch handling

// Track multiple simultaneous touches
element.addEventListener("touchstart", (event) => {
  console.log("Active touches:", event.touches.length);
  console.log("Target touches:", event.targetTouches.length);
  console.log("Changed touches:", event.changedTouches.length);
  
  // Access individual touches
  for (let touch of event.touches) {
    console.log("Touch ID:", touch.identifier);
    console.log("Position:", touch.clientX, touch.clientY);
  }
});

// Pinch-to-zoom detection
let initialDistance = 0;
element.addEventListener("touchstart", (event) => {
  if (event.touches.length === 2) {
    const touch1 = event.touches[0];
    const touch2 = event.touches[1];
    initialDistance = Math.hypot(
      touch2.clientX - touch1.clientX,
      touch2.clientY - touch1.clientY
    );
  }
});

element.addEventListener("touchmove", (event) => {
  if (event.touches.length === 2) {
    const touch1 = event.touches[0];
    const touch2 = event.touches[1];
    const distance = Math.hypot(
      touch2.clientX - touch1.clientX,
      touch2.clientY - touch1.clientY
    );
    const scale = distance / initialDistance;
    console.log("Zoom scale:", scale);
  }
});
Note: Prefer Pointer Events over touch/mouse events - they unify all input types. Use setPointerCapture() to ensure pointer events continue firing even if pointer moves outside element during drag. Set touch-action CSS property to control touch behaviors.

2.5 Keyboard and Mouse Event Patterns

Event Type Description Fires When Cancelable
keydown Key pressed down Key initially pressed. Repeats while held. Yes
keyup Key released Key released after press Yes
keypress DEPRECATED Character key pressed (deprecated) Use keydown instead Yes
click Element clicked Mouse down + up on same element Yes
dblclick Element double-clicked Two clicks in quick succession Yes
mousedown Mouse button pressed Button pressed on element Yes
mouseup Mouse button released Button released Yes
mousemove Mouse pointer moved Pointer moves over element Yes
mouseenter Mouse enters element Pointer enters boundary. Doesn't bubble. No
mouseleave Mouse leaves element Pointer leaves boundary. Doesn't bubble. No
mouseover Mouse enters element or child Pointer enters. Bubbles. Yes
mouseout Mouse leaves element or child Pointer leaves. Bubbles. Yes
contextmenu Context menu triggered Right-click or long-press Yes
wheel Mouse wheel scrolled Wheel rotated over element Yes
KeyboardEvent Property Type Description Example Values
key string Value of key pressed. Preferred modern property. "a", "Enter", "ArrowLeft"
code string Physical key code. Independent of keyboard layout. "KeyA", "Enter", "ArrowLeft"
keyCode DEPRECATED number Numeric key code (deprecated). Use key or code instead. 65, 13, 37
ctrlKey boolean True if Ctrl/Command key pressed during event true / false
shiftKey boolean True if Shift key pressed during event true / false
altKey boolean True if Alt/Option key pressed during event true / false
metaKey boolean True if Meta key pressed (Windows/Command key) true / false
repeat boolean True if key held down (auto-repeat) true / false
MouseEvent Property Type Description Values
button number Which button was pressed 0=Left, 1=Middle, 2=Right, 3=Back, 4=Forward
buttons number Bitmask of currently pressed buttons 1=Left, 2=Right, 4=Middle (can combine)
clientX / clientY number Coordinates relative to viewport Pixel values
pageX / pageY number Coordinates relative to document Pixel values
screenX / screenY number Coordinates relative to screen Pixel values
offsetX / offsetY number Coordinates relative to target element Pixel values
movementX / movementY number Distance moved since last mousemove Delta in pixels
relatedTarget Element Secondary target (for mouseenter/mouseleave) Element reference

Example: Keyboard event handling

// Modern key detection
document.addEventListener("keydown", (event) => {
  // Use 'key' property (preferred)
  if (event.key === "Enter") {
    console.log("Enter pressed");
  }
  
  // Use 'code' for physical key position
  if (event.code === "KeyA") {
    console.log("A key pressed (QWERTY position)");
  }
  
  // Modifier keys
  if (event.ctrlKey && event.key === "s") {
    event.preventDefault();
    console.log("Ctrl+S (Save)");
  }
  
  // Check for key combinations
  if (event.shiftKey && event.altKey && event.key === "K") {
    console.log("Shift+Alt+K pressed");
  }
  
  // Arrow keys
  switch(event.key) {
    case "ArrowUp": moveUp(); break;
    case "ArrowDown": moveDown(); break;
    case "ArrowLeft": moveLeft(); break;
    case "ArrowRight": moveRight(); break;
  }
  
  // Prevent auto-repeat
  if (event.repeat) return;
});

Example: Mouse event handling

// Mouse button detection
element.addEventListener("mousedown", (event) => {
  switch(event.button) {
    case 0: console.log("Left click"); break;
    case 1: console.log("Middle click"); break;
    case 2: console.log("Right click"); break;
  }
});

// Mouse position
element.addEventListener("click", (event) => {
  console.log("Viewport:", event.clientX, event.clientY);
  console.log("Document:", event.pageX, event.pageY);
  console.log("Element:", event.offsetX, event.offsetY);
  console.log("Screen:", event.screenX, event.screenY);
});

// Drag detection
let isDragging = false;
element.addEventListener("mousedown", () => {
  isDragging = true;
});
document.addEventListener("mousemove", (event) => {
  if (isDragging) {
    console.log("Dragging:", event.movementX, event.movementY);
  }
});
document.addEventListener("mouseup", () => {
  isDragging = false;
});

// Prevent context menu
element.addEventListener("contextmenu", (event) => {
  event.preventDefault();
  console.log("Custom context menu");
});
Note: Always use event.key instead of deprecated keyCode. Use mouseenter/mouseleave instead of mouseover/mouseout when you don't want events from child elements. For keyboard shortcuts, check modifier keys with ctrlKey, shiftKey, altKey, metaKey.

2.6 Event Delegation and Event Bubbling

Concept Description Benefits Use Case
Event Bubbling Events propagate from target element up through ancestors to document root Enables event delegation. Single listener handles multiple elements. Default event flow in DOM
Event Capturing Events propagate from document root down to target element (before bubbling) Intercept events before target. Use capture: true option. Special interception needs
Event Delegation Single listener on parent handles events from multiple children via bubbling Better performance. Works with dynamic content. Less memory. Lists, tables, dynamic UI
Event Target event.target is element that triggered event (child) Identify actual clicked element in delegation pattern Determine source element
Current Target event.currentTarget is element with listener attached (parent) Reference to delegating element Access parent in delegation
Event Phase Value Description Direction
NONE 0 Event not being processed N/A
CAPTURING_PHASE 1 Event traveling from root to target Top-down
AT_TARGET 2 Event reached target element At target
BUBBLING_PHASE 3 Event traveling from target to root Bottom-up

Example: Event delegation pattern

// Instead of attaching listener to each item
// ❌ Bad: Many listeners, doesn't work with dynamic content
document.querySelectorAll(".item").forEach((item) => {
  item.addEventListener("click", handleClick);
});

// ✅ Good: Single listener on parent
const list = document.querySelector("#list");
list.addEventListener("click", (event) => {
  // Check if clicked element matches selector
  if (event.target.matches(".item")) {
    console.log("Item clicked:", event.target);
    handleItem(event.target);
  }
  
  // Or use closest for nested elements
  const item = event.target.closest(".item");
  if (item && list.contains(item)) {
    console.log("Item found:", item);
  }
});

// Works automatically with dynamically added items
const newItem = document.createElement("div");
newItem.className = "item";
list.appendChild(newItem); // Click handler works immediately!

Example: Event propagation and phases

// Event flow: Capture → Target → Bubble
const parent = document.querySelector("#parent");
const child = document.querySelector("#child");

// Capture phase (top-down)
parent.addEventListener("click", (event) => {
  console.log("Parent - Capture phase");
  console.log("Phase:", event.eventPhase); // 1 (CAPTURING_PHASE)
}, { "capture": true });

// Target phase
child.addEventListener("click", (event) => {
  console.log("Child - Target phase");
  console.log("Phase:", event.eventPhase); // 2 (AT_TARGET)
  console.log("Target:", event.target); // child
  console.log("Current:", event.currentTarget); // child
});

// Bubble phase (bottom-up)
parent.addEventListener("click", (event) => {
  console.log("Parent - Bubble phase");
  console.log("Phase:", event.eventPhase); // 3 (BUBBLING_PHASE)
  console.log("Target:", event.target); // child (original)
  console.log("Current:", event.currentTarget); // parent
});

// Click child outputs:
// "Parent - Capture phase"
// "Child - Target phase"
// "Parent - Bubble phase"

Example: Advanced delegation patterns

// Multi-action delegation
document.querySelector("#toolbar").addEventListener("click", (event) => {
  const button = event.target.closest("button");
  if (!button) return;
  
  const action = button.dataset.action;
  switch(action) {
    case "save":
      handleSave();
      break;
    case "delete":
      handleDelete();
      break;
    case "edit":
      handleEdit();
      break;
  }
});

// Table row delegation
table.addEventListener("click", (event) => {
  const row = event.target.closest("tr");
  if (!row || row.parentElement.tagName === "THEAD") return;
  
  const cell = event.target.closest("td");
  const columnIndex = Array.from(row.cells).indexOf(cell);
  
  console.log("Row:", row.rowIndex);
  console.log("Column:", columnIndex);
  console.log("Data:", row.dataset.id);
});

// Form delegation
form.addEventListener("input", (event) => {
  const input = event.target;
  if (input.matches("[data-validate]")) {
    validateField(input);
  }
});

// Performance: Delegate to document for global handlers
document.addEventListener("click", (event) => {
  // Close dropdowns when clicking outside
  if (!event.target.closest(".dropdown")) {
    closeAllDropdowns();
  }
});
Note: Event delegation is essential for performance with large lists (hundreds/thousands of items). It automatically handles dynamically added elements without re-attaching listeners. Use event.target.closest(selector) to handle clicks on nested elements within delegated items.
Warning: Not all events bubble! Events like focus, blur, mouseenter, mouseleave don't bubble. Use their bubbling alternatives: focusin, focusout, mouseover, mouseout for delegation. Always check if event.target matches your selector before acting.

Event Handling Best Practices

  • Use addEventListener with options object for modern features (once, passive, signal)
  • Prefer Pointer Events over separate mouse/touch handlers for unified input
  • Use event delegation for lists and dynamic content - better performance and automatic handling of new elements
  • Set passive: true for scroll/touch events to improve performance
  • Use event.key instead of deprecated keyCode for keyboard events
  • Remember: event.target = original element, event.currentTarget = element with listener
  • Call preventDefault() to stop default browser actions, stopPropagation() to prevent bubbling
  • Use CustomEvent with detail property for component communication
  • Clean up listeners with AbortController or removeEventListener to prevent memory leaks

3. Network Communication APIs

3.1 Fetch API and Request/Response Objects

Method/Property Syntax Description Browser Support
fetch fetch(url, options) Makes HTTP request, returns Promise<Response>. Modern replacement for XMLHttpRequest. Modern Browsers
Request Constructor new Request(url, options) Creates Request object that can be reused. Options include method, headers, body, mode, credentials. Modern Browsers
Response Constructor new Response(body, options) Creates Response object. Options include status, statusText, headers. Used in Service Workers. Modern Browsers
Headers Constructor new Headers(init) Creates Headers object for request/response headers. Methods: get, set, append, delete, has. Modern Browsers
Fetch Option Type Description Default
method string HTTP method: GET, POST, PUT, DELETE, PATCH, etc. "GET"
headers Headers | object Request headers as Headers object or plain object {}
body string | FormData | Blob Request body. Not allowed for GET/HEAD. Auto-sets Content-Type. undefined
mode string Request mode: "cors", "no-cors", "same-origin" "cors"
credentials string Cookie handling: "omit", "same-origin", "include" "same-origin"
cache string Cache mode: "default", "no-store", "reload", "no-cache", "force-cache" "default"
redirect string Redirect mode: "follow", "error", "manual" "follow"
referrer string Referrer URL or "no-referrer" Document URL
integrity string Subresource Integrity hash for verification ""
signal AbortSignal AbortSignal to cancel request undefined
Response Property Type Description
ok boolean True if status is 200-299. Convenient for checking success.
status number HTTP status code (200, 404, 500, etc.)
statusText string HTTP status message ("OK", "Not Found", etc.)
headers Headers Response headers as Headers object
url string Final URL after redirects
redirected boolean True if response is result of redirect
type string Response type: "basic", "cors", "error", "opaque"
bodyUsed boolean True if body has been read. Body can only be read once.
Response Body Method Returns Description
json() Promise<any> Parses response body as JSON
text() Promise<string> Reads response body as text
blob() Promise<Blob> Reads response body as Blob (files, images)
arrayBuffer() Promise<ArrayBuffer> Reads response body as binary data
formData() Promise<FormData> Parses response body as FormData

Example: Basic fetch usage

// Simple GET request
fetch("/api/users")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log("Users:", data);
  })
  .catch((error) => {
    console.error("Fetch error:", error);
  });

// Async/await pattern (preferred)
async function fetchUsers() {
  try {
    const response = await fetch("/api/users");
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

// POST request with JSON
async function createUser(userData) {
  const response = await fetch("/api/users", {
    "method": "POST",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": JSON.stringify(userData)
  });
  return response.json();
}

Example: Headers and credentials

// Using Headers object
const headers = new Headers();
headers.append("Authorization", "Bearer token123");
headers.append("Content-Type", "application/json");

const response = await fetch("/api/protected", {
  "method": "GET",
  "headers": headers,
  "credentials": "include" // Send cookies
});

// Check response headers
console.log("Content-Type:", response.headers.get("content-type"));
console.log("Has header:", response.headers.has("x-custom"));

// Iterate headers
for (let [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

// FormData for file upload
const formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("description", "Profile picture");

await fetch("/api/upload", {
  "method": "POST",
  "body": formData // Content-Type auto-set to multipart/form-data
});
Note: Fetch doesn't reject on HTTP errors (404, 500) - check response.ok or response.status. Body can only be read once - use response.clone() if you need to read it multiple times. Default credentials: "same-origin" doesn't send cookies to different origins.

3.2 Fetch API Advanced Patterns (AbortController, Streams)

Feature Syntax Description Use Case
AbortController new AbortController() Creates controller for aborting fetch requests. Has signal property and abort() method. Cancel requests
signal controller.signal AbortSignal to pass to fetch. Automatically aborts when controller.abort() called. Link abort to fetch
abort() controller.abort(reason) Aborts associated request. Optional reason passed to error handler. Trigger cancellation
ReadableStream response.body Response body as ReadableStream. Allows progressive reading of large responses. Stream large data
getReader() stream.getReader() Gets ReadableStreamDefaultReader to read chunks. Locks stream. Read stream chunks
read() reader.read() Returns Promise<{value, done}>. Value is Uint8Array chunk. Read next chunk

Example: Aborting fetch requests

// Basic abort pattern
const controller = new AbortController();
const signal = controller.signal;

fetch("/api/data", { signal })
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Fetch aborted");
    } else {
      console.error("Fetch error:", error);
    }
  });

// Abort after 5 seconds
setTimeout(() => controller.abort(), 5000);

// Abort on user action
cancelButton.addEventListener("click", () => {
  controller.abort("User cancelled");
});

// Timeout utility
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  
  return fetch(url, {
    ...options,
    "signal": controller.signal
  }).finally(() => clearTimeout(id));
}

// Usage
try {
  const response = await fetchWithTimeout("/api/slow", {}, 3000);
  const data = await response.json();
} catch (error) {
  if (error.name === "AbortError") {
    console.error("Request timed out");
  }
}

Example: Streaming response data

// Stream large file download with progress
async function downloadWithProgress(url) {
  const response = await fetch(url);
  const contentLength = response.headers.get("content-length");
  const total = parseInt(contentLength, 10);
  let loaded = 0;
  
  const reader = response.body.getReader();
  const chunks = [];
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    chunks.push(value);
    loaded += value.length;
    const progress = (loaded / total) * 100;
    console.log(`Progress: ${progress.toFixed(2)}%`);
  }
  
  // Concatenate chunks into single Uint8Array
  const chunksAll = new Uint8Array(loaded);
  let position = 0;
  for (let chunk of chunks) {
    chunksAll.set(chunk, position);
    position += chunk.length;
  }
  
  return chunksAll;
}

// Stream text data line by line
async function streamText(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const text = decoder.decode(value, { "stream": true });
    console.log("Chunk:", text);
  }
}

Example: Advanced patterns

// Request deduplication
const requestCache = new Map();

async function fetchOnce(url) {
  if (requestCache.has(url)) {
    return requestCache.get(url);
  }
  
  const promise = fetch(url).then((r) => r.json());
  requestCache.set(url, promise);
  
  try {
    return await promise;
  } catch (error) {
    requestCache.delete(url); // Remove failed request
    throw error;
  }
}

// Retry with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok && i < maxRetries - 1) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

// Parallel requests with Promise.all
async function fetchMultiple(urls) {
  const promises = urls.map((url) => 
    fetch(url).then((r) => r.json())
  );
  return Promise.all(promises);
}

// Race condition - first to respond wins
async function fetchFastest(urls) {
  const promises = urls.map((url) => 
    fetch(url).then((r) => r.json())
  );
  return Promise.race(promises);
}
Warning: AbortController.abort() throws an error in the fetch promise - always catch it. Streams can only be read once - lock is not released until reader is closed. When streaming, make sure to handle errors and cleanup properly to avoid memory leaks.

3.3 XMLHttpRequest and Legacy AJAX Patterns

Property/Method Description Modern Alternative
new XMLHttpRequest() Creates XHR object for AJAX requests Use fetch()
open(method, url, async) Initializes request. Third parameter is async flag (default true). fetch(url, {method})
send(body) Sends request. Body for POST/PUT. fetch(url, {body})
abort() Aborts request in progress AbortController
setRequestHeader(name, value) Sets HTTP request header. Call after open(), before send(). fetch(url, {headers})
getResponseHeader(name) Gets response header value response.headers.get()
readyState Request state: 0=UNSENT, 1=OPENED, 2=HEADERS_RECEIVED, 3=LOADING, 4=DONE Promise states
status HTTP status code (200, 404, etc.) response.status
statusText HTTP status message response.statusText
responseText Response body as text response.text()
responseXML Response body as XML Document Parse with DOMParser
response Response body (type depends on responseType) Body reading methods
responseType Expected response type: "", "text", "json", "blob", "arraybuffer", "document" Body reading methods
timeout Timeout in milliseconds. 0 means no timeout. AbortSignal.timeout()
withCredentials Include cookies in cross-origin requests credentials: "include"
Event Description When Fired
loadstart Request started When request begins
progress Data transfer in progress Periodically while loading
load Request completed successfully When response received
error Request failed Network error occurred
abort Request was aborted When abort() called
timeout Request timed out Exceeded timeout duration
loadend Request finished (success or failure) After load/error/abort

Example: XMLHttpRequest basic usage (legacy)

// GET request
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/users", true);

xhr.onload = function() {
  if (xhr.status === 200) {
    const data = JSON.parse(xhr.responseText);
    console.log("Success:", data);
  } else {
    console.error("Error:", xhr.status);
  }
};

xhr.onerror = function() {
  console.error("Network error");
};

xhr.send();

// POST request with JSON
const xhr2 = new XMLHttpRequest();
xhr2.open("POST", "/api/users", true);
xhr2.setRequestHeader("Content-Type", "application/json");

xhr2.onreadystatechange = function() {
  if (xhr2.readyState === 4) { // DONE
    if (xhr2.status === 201) {
      console.log("Created:", JSON.parse(xhr2.responseText));
    }
  }
};

xhr2.send(JSON.stringify({ "name": "John" }));

// Upload with progress
const formData = new FormData();
formData.append("file", file);

const xhr3 = new XMLHttpRequest();
xhr3.upload.addEventListener("progress", (event) => {
  if (event.lengthComputable) {
    const percent = (event.loaded / event.total) * 100;
    console.log(`Upload: ${percent.toFixed(2)}%`);
  }
});

xhr3.open("POST", "/api/upload", true);
xhr3.send(formData);
Note: XMLHttpRequest is legacy API - use Fetch API for new projects. XHR still useful for upload progress tracking (Fetch doesn't support this natively yet). XHR requires more boilerplate code and callback-based error handling compared to Promise-based Fetch.

3.4 WebSocket API for Real-time Communication

Feature Syntax Description Browser Support
WebSocket Constructor new WebSocket(url, protocols) Creates WebSocket connection. URL must start with ws:// or wss://. Optional protocols array. All Browsers
send(data) ws.send(data) Sends data to server. Accepts string, Blob, ArrayBuffer, ArrayBufferView. All Browsers
close(code, reason) ws.close(code, reason) Closes connection. Optional close code and reason string. All Browsers
Property Type Description Values
readyState number Connection state 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
url string WebSocket URL Read-only
protocol string Negotiated subprotocol Read-only
bufferedAmount number Bytes queued but not yet transmitted Read-only
binaryType string Binary data type received "blob" or "arraybuffer"
Event Description Event Object Properties
open Connection established successfully Standard Event object
message Message received from server data: message content (string, Blob, ArrayBuffer)
error Error occurred Standard Event object
close Connection closed code: close code, reason: close reason, wasClean: boolean

Example: WebSocket basic usage

// Create WebSocket connection
const ws = new WebSocket("wss://example.com/socket");

// Connection opened
ws.addEventListener("open", (event) => {
  console.log("Connected to server");
  ws.send("Hello Server!");
});

// Listen for messages
ws.addEventListener("message", (event) => {
  console.log("Message from server:", event.data);
  
  // Handle different data types
  if (typeof event.data === "string") {
    const message = JSON.parse(event.data);
    handleMessage(message);
  } else if (event.data instanceof Blob) {
    // Handle binary data
    event.data.text().then((text) => console.log(text));
  }
});

// Handle errors
ws.addEventListener("error", (event) => {
  console.error("WebSocket error:", event);
});

// Connection closed
ws.addEventListener("close", (event) => {
  console.log("Connection closed:", event.code, event.reason);
  if (event.wasClean) {
    console.log("Clean close");
  } else {
    console.log("Connection died");
  }
});

// Send different data types
ws.send("Text message");
ws.send(JSON.stringify({ "type": "chat", "message": "Hello" }));
ws.send(new Blob(["binary data"]));
ws.send(new Uint8Array([1, 2, 3, 4]));

// Close connection gracefully
ws.close(1000, "Normal closure");

Example: WebSocket reconnection pattern

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.reconnectInterval = 1000;
    this.maxReconnectInterval = 30000;
    this.reconnectDecay = 1.5;
    this.connect();
  }
  
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      console.log("WebSocket connected");
      this.reconnectInterval = 1000; // Reset on successful connection
    };
    
    this.ws.onmessage = (event) => {
      this.handleMessage(JSON.parse(event.data));
    };
    
    this.ws.onerror = (error) => {
      console.error("WebSocket error:", error);
    };
    
    this.ws.onclose = (event) => {
      console.log("WebSocket closed:", event.code);
      this.reconnect();
    };
  }
  
  reconnect() {
    console.log(`Reconnecting in ${this.reconnectInterval}ms...`);
    setTimeout(() => {
      this.connect();
      this.reconnectInterval = Math.min(
        this.reconnectInterval * this.reconnectDecay,
        this.maxReconnectInterval
      );
    }, this.reconnectInterval);
  }
  
  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.warn("WebSocket not ready");
    }
  }
  
  handleMessage(data) {
    console.log("Received:", data);
  }
  
  close() {
    this.ws.close(1000, "Client closing");
  }
}

// Usage
const client = new WebSocketClient("wss://example.com/socket");
client.send({ "type": "subscribe", "channel": "updates" });
Note: Always use wss:// (secure WebSocket) in production, not ws://. Check readyState before calling send() to avoid errors. Implement reconnection logic for production applications. Use bufferedAmount to check if send buffer is full.

3.5 Server-Sent Events (EventSource) API

Feature Syntax Description Browser Support
EventSource Constructor new EventSource(url, options) Creates server-sent events connection. One-way: server to client only. Modern Browsers
close() eventSource.close() Closes connection. Cannot be reopened. Modern Browsers
Property Type Description Values
readyState number Connection state 0=CONNECTING, 1=OPEN, 2=CLOSED
url string Source URL Read-only
withCredentials boolean Whether to send credentials (cookies) Set in constructor options
Event Description When Fired
open Connection opened When connection established
message Message received from server Server sends data without event name
error Error occurred or connection lost Network error or server closed connection
Custom events Named events from server Server sends data with event: field

Example: EventSource basic usage

// Create EventSource connection
const eventSource = new EventSource("/api/events");

// Connection opened
eventSource.addEventListener("open", () => {
  console.log("SSE connection opened");
});

// Receive messages
eventSource.addEventListener("message", (event) => {
  console.log("Message:", event.data);
  const data = JSON.parse(event.data);
  updateUI(data);
});

// Custom named events from server
eventSource.addEventListener("notification", (event) => {
  const notification = JSON.parse(event.data);
  showNotification(notification);
});

eventSource.addEventListener("update", (event) => {
  const update = JSON.parse(event.data);
  console.log("Update received:", update);
});

// Handle errors
eventSource.addEventListener("error", (event) => {
  console.error("SSE error");
  if (eventSource.readyState === EventSource.CLOSED) {
    console.log("Connection closed");
  }
});

// Close connection when done
// eventSource.close();

Example: Server-side SSE format (Node.js)

// Server sends events in this format:
// event: eventName
// data: JSON data
// id: unique id (optional)
// retry: milliseconds (optional)
// (blank line)

// Node.js server example
app.get("/api/events", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  
  // Send message without event name (uses 'message' event)
  res.write("data: " + JSON.stringify({ "message": "Hello" }) + "\n\n");
  
  // Send named event
  res.write("event: notification\n");
  res.write("data: " + JSON.stringify({ "title": "New message" }) + "\n");
  res.write("id: 123\n");
  res.write("\n");
  
  // Send periodic updates
  const interval = setInterval(() => {
    res.write(`event: update\n`);
    res.write(`data: ${JSON.stringify({ "time": Date.now() })}\n\n`);
  }, 1000);
  
  req.on("close", () => {
    clearInterval(interval);
    res.end();
  });
});
Note: EventSource automatically reconnects on connection loss (WebSocket doesn't). SSE is one-way only (server to client) - use WebSocket for bidirectional communication. SSE uses regular HTTP, works through most proxies and firewalls. Default retry timeout is 3 seconds.

3.6 Navigator.sendBeacon for Analytics

Feature Syntax Description Browser Support
sendBeacon navigator.sendBeacon(url, data) Sends asynchronous POST request that completes even if page is unloading. Returns boolean indicating if queued successfully. Modern Browsers
Data Type Description Content-Type
ArrayBuffer Binary data No Content-Type set
Blob Binary data with type Uses Blob's type property
FormData Form data multipart/form-data
URLSearchParams URL-encoded data application/x-www-form-urlencoded
String Text data text/plain

Example: sendBeacon usage patterns

// Track page unload
window.addEventListener("beforeunload", () => {
  const data = JSON.stringify({
    "event": "page_exit",
    "timestamp": Date.now(),
    "duration": performance.now()
  });
  
  const blob = new Blob([data], { "type": "application/json" });
  const queued = navigator.sendBeacon("/api/analytics", blob);
  
  if (!queued) {
    console.warn("Beacon not queued - data limit exceeded");
  }
});

// Track user actions
function trackEvent(eventName, properties) {
  const data = new URLSearchParams({
    "event": eventName,
    "timestamp": Date.now(),
    ...properties
  });
  
  navigator.sendBeacon("/api/track", data);
}

// Usage
trackEvent("button_click", { "button": "submit", "page": "/checkout" });

// Track errors
window.addEventListener("error", (event) => {
  const errorData = new FormData();
  errorData.append("message", event.message);
  errorData.append("filename", event.filename);
  errorData.append("lineno", event.lineno);
  errorData.append("colno", event.colno);
  
  navigator.sendBeacon("/api/errors", errorData);
});

// Track visibility changes
document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    const sessionData = JSON.stringify({
      "sessionId": sessionStorage.getItem("sessionId"),
      "duration": performance.now(),
      "interactionCount": getInteractionCount()
    });
    
    navigator.sendBeacon(
      "/api/session-end",
      new Blob([sessionData], { "type": "application/json" })
    );
  }
});
Note: sendBeacon is designed for analytics and diagnostics that must not be lost when page unloads. Request completes even if user closes tab/window. Cannot read response or handle errors. Has size limits (typically 64KB). Uses POST method only.
Warning: Don't use sendBeacon for critical data that requires confirmation - there's no way to know if request succeeded. Browsers may limit total beacon data size. Use Blob with proper Content-Type for JSON data, not plain string.

Network Communication Best Practices

  • Use Fetch API for modern HTTP requests - cleaner syntax and Promise-based
  • Always check response.ok or response.status - fetch doesn't reject on HTTP errors
  • Use AbortController for request cancellation and timeouts
  • Implement proper error handling with try/catch for async/await patterns
  • Use WebSocket for bidirectional real-time communication (chat, live updates)
  • Use EventSource for one-way server-to-client streaming (notifications, live feeds)
  • Implement reconnection logic for WebSocket connections in production
  • Use credentials: "include" to send cookies with cross-origin requests
  • Use navigator.sendBeacon for analytics during page unload - guaranteed delivery
  • Stream large responses with ReadableStream for memory efficiency and progress tracking

4. Storage and Persistence APIs

4.1 localStorage and sessionStorage Operations

Method Syntax Description Scope
setItem storage.setItem(key, value) Stores key-value pair. Value converted to string. Throws QuotaExceededError if storage full. Both
getItem storage.getItem(key) Retrieves value by key. Returns null if key doesn't exist. Both
removeItem storage.removeItem(key) Removes key-value pair. No error if key doesn't exist. Both
clear storage.clear() Removes all key-value pairs from storage. Both
key storage.key(index) Returns key name at index position. Returns null if out of range. Both
length storage.length Number of stored key-value pairs. Read-only property. Both
Storage Type Lifetime Capacity Use Case
localStorage Persists until explicitly deleted. Survives browser restart. ~5-10MB per origin User preferences, settings, cached data
sessionStorage Cleared when page session ends (tab/window closed). Survives page reload. ~5-10MB per origin Temporary data, form state, single-session cache
Event Property Type Description
storage event StorageEvent Fires on other tabs/windows when storage changes. Doesn't fire in tab that made change.
key string Key that was changed. null if clear() called.
oldValue string Previous value. null if new key.
newValue string New value. null if item removed.
url string URL of page that made the change.
storageArea Storage Reference to localStorage or sessionStorage affected.

Example: Basic storage operations

// Store data
localStorage.setItem("username", "john_doe");
localStorage.setItem("theme", "dark");

// Store objects (must stringify)
const user = { "id": 123, "name": "John" };
localStorage.setItem("user", JSON.stringify(user));

// Retrieve data
const username = localStorage.getItem("username");
console.log(username); // "john_doe"

// Retrieve and parse objects
const userStr = localStorage.getItem("user");
const userData = JSON.parse(userStr);
console.log(userData.name); // "John"

// Remove item
localStorage.removeItem("theme");

// Clear all
localStorage.clear();

// Check if key exists
if (localStorage.getItem("username") !== null) {
  console.log("Username exists");
}

// Iterate all keys
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key);
  console.log(`${key}: ${value}`);
}

Example: Helper functions and patterns

// Safe JSON storage wrapper
const storage = {
  "set": (key, value) => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error) {
      console.error("Storage error:", error);
      return false;
    }
  },
  "get": (key, defaultValue = null) => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      return defaultValue;
    }
  },
  "remove": (key) => {
    localStorage.removeItem(key);
  }
};

// Usage
storage.set("settings", { "theme": "dark", "lang": "en" });
const settings = storage.get("settings", {});

// Storage with expiration
function setWithExpiry(key, value, ttl) {
  const item = {
    "value": value,
    "expiry": Date.now() + ttl
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null;
  
  const item = JSON.parse(itemStr);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

// Set with 1 hour expiry
setWithExpiry("token", "abc123", 3600000);

Example: Storage event for cross-tab communication

// Listen for storage changes from other tabs
window.addEventListener("storage", (event) => {
  console.log("Storage changed in another tab");
  console.log("Key:", event.key);
  console.log("Old value:", event.oldValue);
  console.log("New value:", event.newValue);
  console.log("URL:", event.url);
  console.log("Storage area:", event.storageArea === localStorage);
  
  // React to specific changes
  if (event.key === "theme") {
    applyTheme(event.newValue);
  }
  
  // Handle logout in all tabs
  if (event.key === "isLoggedIn" && event.newValue === "false") {
    redirectToLogin();
  }
});

// Trigger storage event in other tabs
localStorage.setItem("notification", JSON.stringify({
  "message": "New update available",
  "timestamp": Date.now()
}));
Note: Web Storage only stores strings - use JSON.stringify() and JSON.parse() for objects. Storage is synchronous and can block UI - use IndexedDB for large data. Data is scoped to origin (protocol + domain + port). Storage event doesn't fire in the tab that made the change.
Warning: Storage can throw QuotaExceededError when full - always wrap in try/catch. Private browsing modes may have reduced or disabled storage. Never store sensitive data (passwords, tokens) in localStorage - vulnerable to XSS. Use sessionStorage for temporary data that shouldn't persist.

4.2 IndexedDB Database Operations and Transactions

Method Syntax Description Browser Support
open indexedDB.open(name, version) Opens database. Returns IDBOpenDBRequest. Triggers upgradeneeded if version changes. All Browsers
deleteDatabase indexedDB.deleteDatabase(name) Deletes entire database. Returns IDBOpenDBRequest. All Browsers
databases NEW indexedDB.databases() Returns Promise with array of available databases and versions. Modern Browsers
IDBDatabase Method Description Use Case
createObjectStore Creates object store (table). Only in upgradeneeded event. Options: keyPath, autoIncrement. Database schema creation
deleteObjectStore Deletes object store. Only in upgradeneeded event. Schema migration
transaction Creates transaction. Parameters: storeNames (string/array), mode ("readonly"/"readwrite"). Access data
close Closes database connection. Good practice when done. Cleanup
IDBObjectStore Method Description Returns
add Adds new record. Fails if key exists. Returns IDBRequest. IDBRequest
put Adds or updates record. Overwrites if key exists. IDBRequest
get Gets record by key. Returns undefined if not found. IDBRequest
getAll Gets all records. Optional query and count parameters. IDBRequest
delete Deletes record by key or key range. IDBRequest
clear Deletes all records in object store. IDBRequest
count Counts records. Optional key or key range parameter. IDBRequest
openCursor Opens cursor for iteration. Optional query and direction. IDBRequest
createIndex Creates index for queries. Only in upgradeneeded event. IDBIndex
index Gets existing index for querying. IDBIndex

Example: Opening database and creating schema

// Open database (creates if doesn't exist)
const request = indexedDB.open("MyDatabase", 1);

// Handle errors
request.onerror = (event) => {
  console.error("Database error:", event.target.error);
};

// Create schema (only runs when version changes)
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // Create object store with auto-incrementing key
  const userStore = db.createObjectStore("users", {
    "keyPath": "id",
    "autoIncrement": true
  });
  
  // Create indexes for searching
  userStore.createIndex("email", "email", { "unique": true });
  userStore.createIndex("age", "age", { "unique": false });
  userStore.createIndex("city", "city", { "unique": false });
  
  // Create another object store
  const productStore = db.createObjectStore("products", {
    "keyPath": "sku"
  });
  
  productStore.createIndex("category", "category");
  productStore.createIndex("price", "price");
};

// Success - database ready
request.onsuccess = (event) => {
  const db = event.target.result;
  console.log("Database opened successfully");
  
  // Store db reference for later use
  window.db = db;
};

Example: CRUD operations

// Add data
function addUser(userData) {
  const transaction = db.transaction(["users"], "readwrite");
  const store = transaction.objectStore("users");
  const request = store.add(userData);
  
  request.onsuccess = () => {
    console.log("User added:", request.result); // Returns key
  };
  
  request.onerror = () => {
    console.error("Add error:", request.error);
  };
}

addUser({ "name": "John", "email": "john@example.com", "age": 30 });

// Get data by key
function getUser(id) {
  const transaction = db.transaction(["users"], "readonly");
  const store = transaction.objectStore("users");
  const request = store.get(id);
  
  request.onsuccess = () => {
    if (request.result) {
      console.log("User found:", request.result);
    } else {
      console.log("User not found");
    }
  };
}

getUser(1);

// Update data (put overwrites)
function updateUser(userData) {
  const transaction = db.transaction(["users"], "readwrite");
  const store = transaction.objectStore("users");
  const request = store.put(userData); // Use put for update
  
  request.onsuccess = () => {
    console.log("User updated");
  };
}

updateUser({ "id": 1, "name": "John Doe", "email": "john@example.com", "age": 31 });

// Delete data
function deleteUser(id) {
  const transaction = db.transaction(["users"], "readwrite");
  const store = transaction.objectStore("users");
  const request = store.delete(id);
  
  request.onsuccess = () => {
    console.log("User deleted");
  };
}

deleteUser(1);

// Get all data
function getAllUsers() {
  const transaction = db.transaction(["users"], "readonly");
  const store = transaction.objectStore("users");
  const request = store.getAll();
  
  request.onsuccess = () => {
    console.log("All users:", request.result);
  };
}

getAllUsers();

Example: Indexes and cursors

// Query by index
function getUserByEmail(email) {
  const transaction = db.transaction(["users"], "readonly");
  const store = transaction.objectStore("users");
  const index = store.index("email");
  const request = index.get(email);
  
  request.onsuccess = () => {
    console.log("User:", request.result);
  };
}

getUserByEmail("john@example.com");

// Range queries with IDBKeyRange
function getUsersByAge(minAge, maxAge) {
  const transaction = db.transaction(["users"], "readonly");
  const store = transaction.objectStore("users");
  const index = store.index("age");
  const range = IDBKeyRange.bound(minAge, maxAge);
  const request = index.getAll(range);
  
  request.onsuccess = () => {
    console.log("Users in age range:", request.result);
  };
}

getUsersByAge(25, 35);

// Cursor for iteration
function iterateUsers() {
  const transaction = db.transaction(["users"], "readonly");
  const store = transaction.objectStore("users");
  const request = store.openCursor();
  
  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      console.log("User:", cursor.value);
      cursor.continue(); // Move to next record
    } else {
      console.log("Done iterating");
    }
  };
}

iterateUsers();

// Cursor with filtering
function filterUsers(callback) {
  const transaction = db.transaction(["users"], "readonly");
  const store = transaction.objectStore("users");
  const request = store.openCursor();
  const results = [];
  
  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      if (callback(cursor.value)) {
        results.push(cursor.value);
      }
      cursor.continue();
    } else {
      console.log("Filtered results:", results);
    }
  };
}

filterUsers((user) => user.age > 25);

Example: Promise wrapper for easier async/await

// Helper to promisify IndexedDB
function promisifyRequest(request) {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Async wrapper class
class Database {
  constructor(dbName, version) {
    this.dbName = dbName;
    this.version = version;
  }
  
  async open(upgradeCallback) {
    const request = indexedDB.open(this.dbName, this.version);
    request.onupgradeneeded = upgradeCallback;
    this.db = await promisifyRequest(request);
    return this.db;
  }
  
  async add(storeName, data) {
    const tx = this.db.transaction([storeName], "readwrite");
    const store = tx.objectStore(storeName);
    return promisifyRequest(store.add(data));
  }
  
  async get(storeName, key) {
    const tx = this.db.transaction([storeName], "readonly");
    const store = tx.objectStore(storeName);
    return promisifyRequest(store.get(key));
  }
  
  async getAll(storeName) {
    const tx = this.db.transaction([storeName], "readonly");
    const store = tx.objectStore(storeName);
    return promisifyRequest(store.getAll());
  }
  
  async delete(storeName, key) {
    const tx = this.db.transaction([storeName], "readwrite");
    const store = tx.objectStore(storeName);
    return promisifyRequest(store.delete(key));
  }
}

// Usage with async/await
async function useDatabase() {
  const db = new Database("MyDB", 1);
  
  await db.open((event) => {
    const database = event.target.result;
    database.createObjectStore("users", { "keyPath": "id", "autoIncrement": true });
  });
  
  const id = await db.add("users", { "name": "John", "email": "john@example.com" });
  console.log("Added user:", id);
  
  const user = await db.get("users", id);
  console.log("User:", user);
  
  const allUsers = await db.getAll("users");
  console.log("All users:", allUsers);
}
Note: IndexedDB is asynchronous and event-based. Schema changes (createObjectStore, createIndex) only allowed in upgradeneeded event. Use put() for upsert (insert or update), add() fails if key exists. IndexedDB can store large amounts of data (hundreds of MB to GB depending on browser).
Warning: All IndexedDB operations are transactional. Transaction auto-commits when all requests complete. Don't perform async operations (fetch, setTimeout) inside transaction handlers - transaction will close. Use indexes for queries - full table scans with cursors are slow on large datasets.

4.3 Cache API for Service Worker Integration

Method Syntax Description Browser Support
caches.open caches.open(cacheName) Opens named cache. Creates if doesn't exist. Returns Promise<Cache>. Modern Browsers
caches.match caches.match(request, options) Searches all caches for matching request. Returns Promise<Response> or undefined. Modern Browsers
caches.has caches.has(cacheName) Checks if named cache exists. Returns Promise<boolean>. Modern Browsers
caches.delete caches.delete(cacheName) Deletes named cache. Returns Promise<boolean> indicating if cache existed. Modern Browsers
caches.keys caches.keys() Returns Promise with array of cache names. Modern Browsers
Cache Method Description Use Case
add Fetches URL and stores response. Single request. Cache single resource
addAll Fetches array of URLs and stores responses. Atomic - fails if any fetch fails. Cache multiple resources
put Stores request-response pair directly. Full control over what's cached. Cache custom responses
match Searches cache for matching request. Returns Promise<Response> or undefined. Retrieve cached response
matchAll Returns all matching responses. Optional request parameter for filtering. Get multiple cached items
delete Removes cached request-response pair. Returns Promise<boolean>. Invalidate cache entry
keys Returns all request keys in cache. Optional request parameter for filtering. List cached URLs

Example: Basic Cache API usage

// Open cache and add resources
async function cacheResources() {
  const cache = await caches.open("my-cache-v1");
  
  // Add single resource
  await cache.add("/api/data");
  
  // Add multiple resources (atomic operation)
  await cache.addAll([
    "/",
    "/styles.css",
    "/script.js",
    "/logo.png"
  ]);
  
  console.log("Resources cached");
}

// Check cache before fetching
async function fetchWithCache(url) {
  // Try cache first
  const cachedResponse = await caches.match(url);
  if (cachedResponse) {
    console.log("Cache hit:", url);
    return cachedResponse;
  }
  
  // Cache miss - fetch from network
  console.log("Cache miss:", url);
  const response = await fetch(url);
  
  // Store in cache for next time
  const cache = await caches.open("my-cache-v1");
  cache.put(url, response.clone());
  
  return response;
}

// Usage
const response = await fetchWithCache("/api/users");
const data = await response.json();

Example: Cache strategies

// Cache First (good for static assets)
async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  
  const response = await fetch(request);
  const cache = await caches.open("static-v1");
  cache.put(request, response.clone());
  return response;
}

// Network First (good for dynamic data)
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open("dynamic-v1");
    cache.put(request, response.clone());
    return response;
  } catch (error) {
    const cached = await caches.match(request);
    if (cached) return cached;
    throw error;
  }
}

// Stale While Revalidate (best of both)
async function staleWhileRevalidate(request) {
  const cached = await caches.match(request);
  
  const fetchPromise = fetch(request).then((response) => {
    const cache = caches.open("swr-v1");
    cache.then((c) => c.put(request, response.clone()));
    return response;
  });
  
  return cached || fetchPromise;
}

// Cache with expiration
async function cacheWithExpiry(request, cacheName, maxAge) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  
  if (cached) {
    const cachedDate = new Date(cached.headers.get("date"));
    const age = Date.now() - cachedDate.getTime();
    
    if (age < maxAge) {
      return cached; // Still fresh
    }
  }
  
  // Expired or missing - fetch fresh
  const response = await fetch(request);
  cache.put(request, response.clone());
  return response;
}

Example: Cache management

// List all caches
async function listCaches() {
  const cacheNames = await caches.keys();
  console.log("Caches:", cacheNames);
  return cacheNames;
}

// Delete old cache versions
async function deleteOldCaches(currentVersion) {
  const cacheNames = await caches.keys();
  const deletePromises = cacheNames
    .filter((name) => name !== currentVersion)
    .map((name) => caches.delete(name));
  
  await Promise.all(deletePromises);
  console.log("Old caches deleted");
}

// Usage in Service Worker activate event
self.addEventListener("activate", (event) => {
  event.waitUntil(
    deleteOldCaches("my-cache-v2")
  );
});

// List cached URLs
async function listCachedUrls(cacheName) {
  const cache = await caches.open(cacheName);
  const requests = await cache.keys();
  const urls = requests.map((req) => req.url);
  console.log("Cached URLs:", urls);
  return urls;
}

// Remove specific cached item
async function removeCachedItem(url) {
  const cache = await caches.open("my-cache-v1");
  const deleted = await cache.delete(url);
  console.log(`Cache entry deleted: ${deleted}`);
}

// Clear all caches
async function clearAllCaches() {
  const cacheNames = await caches.keys();
  await Promise.all(
    cacheNames.map((name) => caches.delete(name))
  );
  console.log("All caches cleared");
}
Note: Cache API stores Request-Response pairs, not just data. Primarily used with Service Workers for offline functionality. Responses must be clone()d before caching because Response body can only be read once. Cache is persistent and separate from HTTP cache.
Property Description Format
document.cookie Gets all cookies as semicolon-separated string. Setting adds/updates single cookie. "name=value; name2=value2"
Cookie Attribute Description Example
expires Expiration date (GMT format). Cookie deleted after this date. expires=Wed, 01 Jan 2025 00:00:00 GMT
max-age Lifetime in seconds. Overrides expires. Negative value deletes cookie. max-age=3600
path URL path where cookie is accessible. Defaults to current path. path=/
domain Domain where cookie is accessible. Includes subdomains if specified. domain=.example.com
secure Cookie only sent over HTTPS. Essential for sensitive data. secure
httpOnly Cookie inaccessible to JavaScript (document.cookie). Only server-side. Set by server. httpOnly
samesite CSRF protection. Values: Strict, Lax, None (requires Secure). samesite=Strict
// Set cookie
function setCookie(name, value, days = 7, options = {}) {
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
  
  // Expiration
  if (days) {
    const date = new Date();
    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
    cookie += `; expires=${date.toUTCString()}`;
  }
  
  // Path (default to root)
  cookie += `; path=${options.path || "/"}`;
  
  // Domain
  if (options.domain) {
    cookie += `; domain=${options.domain}`;
  }
  
  // Secure
  if (options.secure) {
    cookie += "; secure";
  }
  
  // SameSite
  if (options.sameSite) {
    cookie += `; samesite=${options.sameSite}`;
  }
  
  document.cookie = cookie;
}

// Get cookie
function getCookie(name) {
  const cookies = document.cookie.split("; ");
  for (let cookie of cookies) {
    const [cookieName, cookieValue] = cookie.split("=");
    if (decodeURIComponent(cookieName) === name) {
      return decodeURIComponent(cookieValue);
    }
  }
  return null;
}

// Delete cookie
function deleteCookie(name, options = {}) {
  setCookie(name, "", -1, options);
}

// Get all cookies as object
function getAllCookies() {
  const cookies = {};
  document.cookie.split("; ").forEach((cookie) => {
    const [name, value] = cookie.split("=");
    cookies[decodeURIComponent(name)] = decodeURIComponent(value);
  });
  return cookies;
}

// Usage
setCookie("username", "john_doe", 30, {
  "path": "/",
  "secure": true,
  "sameSite": "Strict"
});

const username = getCookie("username");
console.log(username); // "john_doe"

deleteCookie("username");
Note: Cookies have 4KB size limit per cookie. Always use encodeURIComponent() for names and values. HttpOnly cookies can't be accessed via JavaScript - only set by server. Use SameSite=Strict or Lax for CSRF protection.
Warning: Cookies are sent with every HTTP request to the domain - impacts performance. Don't store sensitive data in client-accessible cookies. Secure flag is mandatory for SameSite=None. Deleting cookie requires matching path and domain of original cookie.

4.5 Origin Private File System API

Method Syntax Description Browser Support
getDirectory NEW navigator.storage.getDirectory() Returns Promise<FileSystemDirectoryHandle> for origin's private file system root. Modern Browsers
getFileHandle dir.getFileHandle(name, options) Gets file handle. Options: create: true to create if missing. Modern Browsers
getDirectoryHandle dir.getDirectoryHandle(name, options) Gets subdirectory handle. Options: create: true to create if missing. Modern Browsers
removeEntry dir.removeEntry(name, options) Deletes file or directory. Options: recursive: true for directories. Modern Browsers
getFile fileHandle.getFile() Returns Promise<File> object with file data. Modern Browsers
createWritable fileHandle.createWritable() Returns Promise<FileSystemWritableFileStream> for writing. Modern Browsers

Example: Origin Private File System usage

// Get root directory
const root = await navigator.storage.getDirectory();

// Create/get file
const fileHandle = await root.getFileHandle("data.txt", { "create": true });

// Write to file
const writable = await fileHandle.createWritable();
await writable.write("Hello, File System!");
await writable.write({ "type": "write", "data": "\nNew line" });
await writable.close();

// Read from file
const file = await fileHandle.getFile();
const contents = await file.text();
console.log(contents);

// Create directory
const dirHandle = await root.getDirectoryHandle("documents", { "create": true });

// Create file in subdirectory
const subFileHandle = await dirHandle.getFileHandle("note.txt", { "create": true });

// List directory contents
for await (const entry of root.values()) {
  console.log(entry.kind, entry.name); // "file" or "directory"
}

// Delete file
await root.removeEntry("data.txt");

// Delete directory recursively
await root.removeEntry("documents", { "recursive": true });
Note: Origin Private File System is private to origin and not accessible to user. Good for app-specific data, cache, temporary files. Not for user documents - use File System Access API for that. Storage is persistent and survives browser restarts.

4.6 Storage Quota and Usage Estimation

Method Syntax Description Browser Support
estimate navigator.storage.estimate() Returns Promise with quota and usage info. Properties: quota, usage. Modern Browsers
persist navigator.storage.persist() Requests persistent storage (won't be cleared under pressure). Returns Promise<boolean>. Modern Browsers
persisted navigator.storage.persisted() Checks if storage is persistent. Returns Promise<boolean>. Modern Browsers

Example: Checking storage quota

// Check storage quota and usage
async function checkStorageQuota() {
  if (navigator.storage && navigator.storage.estimate) {
    const estimate = await navigator.storage.estimate();
    const usage = estimate.usage; // Bytes used
    const quota = estimate.quota; // Bytes available
    const percentUsed = (usage / quota * 100).toFixed(2);
    
    console.log(`Storage: ${formatBytes(usage)} / ${formatBytes(quota)}`);
    console.log(`${percentUsed}% used`);
    
    if (estimate.usageDetails) {
      console.log("Breakdown:", estimate.usageDetails);
      // { indexedDB: 1234, caches: 5678, ... }
    }
    
    return { usage, quota, percentUsed };
  }
}

function formatBytes(bytes) {
  if (bytes === 0) return "0 Bytes";
  const k = 1024;
  const sizes = ["Bytes", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
}

// Request persistent storage
async function requestPersistentStorage() {
  if (navigator.storage && navigator.storage.persist) {
    const isPersisted = await navigator.storage.persist();
    console.log(`Persistent storage: ${isPersisted}`);
    return isPersisted;
  }
  return false;
}

// Check if storage is persistent
async function checkPersistence() {
  if (navigator.storage && navigator.storage.persisted) {
    const isPersisted = await navigator.storage.persisted();
    console.log(`Storage persisted: ${isPersisted}`);
    return isPersisted;
  }
  return false;
}

// Monitor storage before operations
async function monitorStorage() {
  const before = await navigator.storage.estimate();
  
  // Perform storage operations
  await performHeavyStorageOperation();
  
  const after = await navigator.storage.estimate();
  const increased = after.usage - before.usage;
  console.log(`Storage increased by: ${formatBytes(increased)}`);
}
Note: Quota varies by browser and available disk space (typically 10-50% of available disk). persist() requires user interaction or installed PWA. Persistent storage won't be cleared under storage pressure. Usage includes IndexedDB, Cache API, and File System API data.

Storage API Best Practices

  • Use localStorage for small, simple data (settings, preferences) - synchronous and limited to 5-10MB
  • Use IndexedDB for large structured data (offline databases, cached content) - asynchronous and scalable
  • Use Cache API with Service Workers for offline-first applications and resource caching
  • Always wrap storage operations in try/catch - quota exceeded and private mode can cause errors
  • Use JSON.stringify/parse for complex objects in localStorage
  • Set appropriate cookie attributes: Secure, HttpOnly, SameSite
  • Request persistent storage for critical data with navigator.storage.persist()
  • Monitor quota usage with navigator.storage.estimate() before large operations
  • Never store sensitive data (passwords, tokens) in client-side storage - vulnerable to XSS
  • Consider data expiration strategies for cached content to prevent stale data

5. Media and Graphics APIs

5.1 HTMLMediaElement (Audio/Video) Control API

Property Type Description Read-Only
src string URL of media resource. Can be set to load new media. No
currentTime number Current playback position in seconds. Set to seek. No
duration number Total duration in seconds. NaN if unknown. Yes
paused boolean true if media is paused. Use play()/pause() to change. Yes
ended boolean true if media has finished playing. Yes
volume number Volume level 0.0 to 1.0. Throws error if out of range. No
muted boolean Whether audio is muted. Doesn't affect volume property. No
playbackRate number Playback speed. 1.0 is normal, 2.0 is 2x, 0.5 is half speed. No
readyState number 0=HAVE_NOTHING, 1=HAVE_METADATA, 2=HAVE_CURRENT_DATA, 3=HAVE_FUTURE_DATA, 4=HAVE_ENOUGH_DATA Yes
networkState number 0=NETWORK_EMPTY, 1=NETWORK_IDLE, 2=NETWORK_LOADING, 3=NETWORK_NO_SOURCE Yes
buffered TimeRanges Time ranges of buffered media data. Yes
seekable TimeRanges Time ranges user can seek to. Yes
loop boolean Whether to restart playback when ended. No
autoplay boolean Whether to start playing automatically. Restricted by browser policies. No
Method Returns Description
play() Promise Starts playback. Returns Promise that resolves when playback starts. May reject if autoplay blocked.
pause() void Pauses playback. Sets paused to true.
load() void Resets media element and reloads source. Aborts ongoing loading.
canPlayType(type) string Returns "probably", "maybe", or "" for MIME type support detection.
Event When Fired Use Case
loadedmetadata Metadata loaded (duration, dimensions available) Get video dimensions, duration
loadeddata First frame loaded Display thumbnail, enable controls
canplay Enough data to play (but may still buffer) Enable play button
canplaythrough Can play to end without buffering Start autoplay
play Playback started/resumed Update UI state
pause Playback paused Update UI state
ended Playback reached end Show replay button, load next
timeupdate currentTime changed (fires ~4 times/second) Update progress bar, time display
seeking Seek operation started Show loading indicator
seeked Seek operation completed Hide loading indicator
volumechange volume or muted changed Update volume UI
error Error occurred loading or playing Display error message
waiting Playback stopped due to buffering Show buffering spinner
playing Playback resumed after buffering Hide buffering spinner

Example: Basic video controls

const video = document.querySelector("video");

// Play video with error handling
async function playVideo() {
  try {
    await video.play();
    console.log("Playing");
  } catch (error) {
    console.error("Autoplay blocked:", error);
  }
}

// Pause video
function pauseVideo() {
  video.pause();
}

// Seek to specific time
function seekTo(seconds) {
  video.currentTime = seconds;
}

// Skip forward/backward
function skip(seconds) {
  video.currentTime += seconds;
}

// Set volume (0.0 to 1.0)
function setVolume(level) {
  video.volume = Math.max(0, Math.min(1, level));
}

// Toggle mute
function toggleMute() {
  video.muted = !video.muted;
}

// Set playback speed
function setSpeed(rate) {
  video.playbackRate = rate; // 0.5, 1.0, 1.5, 2.0
}

// Toggle fullscreen
async function toggleFullscreen() {
  if (document.fullscreenElement) {
    await document.exitFullscreen();
  } else {
    await video.requestFullscreen();
  }
}

Example: Event handling and progress tracking

const video = document.querySelector("video");
const progressBar = document.querySelector(".progress");
const timeDisplay = document.querySelector(".time");

// Get duration when metadata loads
video.addEventListener("loadedmetadata", () => {
  console.log(`Duration: ${video.duration} seconds`);
  console.log(`Dimensions: ${video.videoWidth}x${video.videoHeight}`);
});

// Update progress bar
video.addEventListener("timeupdate", () => {
  const percent = (video.currentTime / video.duration) * 100;
  progressBar.style.width = `${percent}%`;
  
  // Format time display
  const current = formatTime(video.currentTime);
  const total = formatTime(video.duration);
  timeDisplay.textContent = `${current} / ${total}`;
});

function formatTime(seconds) {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${secs.toString().padStart(2, "0")}`;
}

// Handle playback state
video.addEventListener("play", () => {
  console.log("Video playing");
});

video.addEventListener("pause", () => {
  console.log("Video paused");
});

video.addEventListener("ended", () => {
  console.log("Video ended");
});

// Handle buffering
video.addEventListener("waiting", () => {
  console.log("Buffering...");
});

video.addEventListener("playing", () => {
  console.log("Playback resumed");
});

// Handle errors
video.addEventListener("error", () => {
  const error = video.error;
  console.error("Media error:", error.code, error.message);
});
Note: play() returns a Promise - use async/await or .then/.catch for error handling. Autoplay is restricted by browsers - user interaction often required. Use canPlayType() to check format support before loading. timeupdate fires frequently - throttle expensive operations.

5.2 MediaStream and getUserMedia for Camera/Microphone

Method Syntax Description Browser Support
getUserMedia navigator.mediaDevices.getUserMedia(constraints) Requests camera/microphone access. Returns Promise<MediaStream>. Requires HTTPS or localhost. All Browsers
getDisplayMedia navigator.mediaDevices.getDisplayMedia(constraints) Captures screen/window/tab. Returns Promise<MediaStream>. Requires user gesture. Modern Browsers
enumerateDevices navigator.mediaDevices.enumerateDevices() Lists available media input/output devices. Returns Promise<MediaDeviceInfo[]>. All Browsers
Constraint Type Description Values
video boolean | object Request video track. true for default, object for constraints. true, false, { width, height, facingMode }
audio boolean | object Request audio track. true for default, object for constraints. true, false, { sampleRate, echoCancellation }
width number | object Video width. Can specify ideal, min, max, exact. 1280, { ideal: 1920, min: 1280 }
height number | object Video height. Can specify ideal, min, max, exact. 720, { ideal: 1080, min: 720 }
facingMode string Camera direction on mobile devices. "user" (front), "environment" (back)
frameRate number | object Frames per second. Can specify ideal, min, max, exact. 30, { ideal: 60, min: 30 }
echoCancellation boolean Audio echo cancellation. Useful for video calls. true, false
noiseSuppression boolean Audio noise reduction. true, false
MediaStream Method Description Returns
getTracks() Gets all tracks (audio and video) in stream. MediaStreamTrack[]
getVideoTracks() Gets only video tracks. MediaStreamTrack[]
getAudioTracks() Gets only audio tracks. MediaStreamTrack[]
addTrack(track) Adds track to stream. void
removeTrack(track) Removes track from stream. void
MediaStreamTrack Method Description
stop() Stops track permanently. Releases camera/microphone. Cannot be restarted.
enabled Boolean property. Set to false to mute (temporary). Set to true to unmute.
getSettings() Returns actual settings (width, height, frameRate, etc.).
getCapabilities() Returns supported capabilities (min/max width, height, frameRate, etc.).
applyConstraints(constraints) Updates constraints dynamically. Returns Promise.

Example: Getting camera and microphone

// Basic usage
async function startCamera() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      "video": true,
      "audio": true
    });
    
    // Display in video element
    const video = document.querySelector("video");
    video.srcObject = stream;
    await video.play();
    
    console.log("Camera started");
  } catch (error) {
    console.error("Camera access denied:", error);
  }
}

// With constraints
async function startHDCamera() {
  const stream = await navigator.mediaDevices.getUserMedia({
    "video": {
      "width": { "ideal": 1920, "min": 1280 },
      "height": { "ideal": 1080, "min": 720 },
      "frameRate": { "ideal": 60, "min": 30 },
      "facingMode": "user"
    },
    "audio": {
      "echoCancellation": true,
      "noiseSuppression": true,
      "sampleRate": 48000
    }
  });
  
  const video = document.querySelector("video");
  video.srcObject = stream;
  return stream;
}

// Screen capture
async function captureScreen() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      "video": {
        "width": { "ideal": 1920 },
        "height": { "ideal": 1080 }
      },
      "audio": false
    });
    
    const video = document.querySelector("video");
    video.srcObject = stream;
    return stream;
  } catch (error) {
    console.error("Screen capture cancelled:", error);
  }
}

Example: Device enumeration and track control

// List available devices
async function listDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  
  const videoInputs = devices.filter((d) => d.kind === "videoinput");
  const audioInputs = devices.filter((d) => d.kind === "audioinput");
  const audioOutputs = devices.filter((d) => d.kind === "audiooutput");
  
  console.log("Cameras:", videoInputs);
  console.log("Microphones:", audioInputs);
  console.log("Speakers:", audioOutputs);
  
  return { videoInputs, audioInputs, audioOutputs };
}

// Select specific device
async function useSpecificCamera(deviceId) {
  const stream = await navigator.mediaDevices.getUserMedia({
    "video": {
      "deviceId": { "exact": deviceId }
    }
  });
  return stream;
}

// Stop all tracks
function stopStream(stream) {
  stream.getTracks().forEach((track) => {
    track.stop();
    console.log(`Stopped ${track.kind} track`);
  });
}

// Mute/unmute video
function toggleVideo(stream, enabled) {
  stream.getVideoTracks().forEach((track) => {
    track.enabled = enabled;
  });
}

// Mute/unmute audio
function toggleAudio(stream, enabled) {
  stream.getAudioTracks().forEach((track) => {
    track.enabled = enabled;
  });
}

// Get track settings
function getTrackInfo(stream) {
  const videoTrack = stream.getVideoTracks()[0];
  if (videoTrack) {
    const settings = videoTrack.getSettings();
    console.log("Video settings:", settings);
    // { width: 1920, height: 1080, frameRate: 30, ... }
    
    const capabilities = videoTrack.getCapabilities();
    console.log("Video capabilities:", capabilities);
    // { width: { min: 640, max: 1920 }, ... }
  }
}

// Change constraints dynamically
async function changeResolution(stream, width, height) {
  const videoTrack = stream.getVideoTracks()[0];
  await videoTrack.applyConstraints({
    "width": { "ideal": width },
    "height": { "ideal": height }
  });
}
Note: getUserMedia requires HTTPS (or localhost for development). User must grant permission - handle denial gracefully. Always stop() tracks when done to release camera/microphone. Setting enabled = false mutes temporarily; stop() releases permanently.
Warning: Screen capture (getDisplayMedia) requires user gesture - can't be called on page load. Don't request more permissions than needed - ask for audio OR video separately if possible. Some constraints are mandatory (exact) and will fail if not supported - use ideal for preferences.

5.3 MediaRecorder API for Recording Media

Constructor Description Browser Support
MediaRecorder new MediaRecorder(stream, options) - Creates recorder for MediaStream. Options include mimeType, audioBitsPerSecond, videoBitsPerSecond. All Browsers
Method Description When to Use
start(timeslice) Starts recording. Optional timeslice in ms for data chunks. Begin recording
stop() Stops recording. Fires dataavailable and stop events. End recording
pause() Pauses recording without ending. Can resume(). Temporary pause
resume() Resumes paused recording. Continue after pause
requestData() Forces dataavailable event with current buffer. Get data during recording
Property Type Description
state string "inactive", "recording", or "paused". Read-only.
mimeType string MIME type being used (e.g., "video/webm"). Read-only.
videoBitsPerSecond number Video encoding bitrate. Read-only.
audioBitsPerSecond number Audio encoding bitrate. Read-only.
Event When Fired Event Data
dataavailable Data chunk ready (at timeslice or stop) event.data - Blob with recorded data
start Recording started -
stop Recording stopped -
pause Recording paused -
resume Recording resumed -
error Recording error occurred event.error - DOMException

Example: Recording camera/microphone

let mediaRecorder;
let recordedChunks = [];

async function startRecording() {
  // Get camera and microphone
  const stream = await navigator.mediaDevices.getUserMedia({
    "video": true,
    "audio": true
  });
  
  // Create recorder
  mediaRecorder = new MediaRecorder(stream, {
    "mimeType": "video/webm;codecs=vp9",
    "videoBitsPerSecond": 2500000
  });
  
  // Collect data chunks
  mediaRecorder.ondataavailable = (event) => {
    if (event.data.size > 0) {
      recordedChunks.push(event.data);
    }
  };
  
  // Handle recording complete
  mediaRecorder.onstop = () => {
    const blob = new Blob(recordedChunks, {
      "type": "video/webm"
    });
    
    // Download file
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "recording.webm";
    a.click();
    
    // Cleanup
    URL.revokeObjectURL(url);
    recordedChunks = [];
  };
  
  // Start recording
  mediaRecorder.start();
  console.log("Recording started");
}

function stopRecording() {
  if (mediaRecorder && mediaRecorder.state !== "inactive") {
    mediaRecorder.stop();
    
    // Stop camera/microphone
    mediaRecorder.stream.getTracks().forEach((track) => track.stop());
    
    console.log("Recording stopped");
  }
}

function pauseRecording() {
  if (mediaRecorder && mediaRecorder.state === "recording") {
    mediaRecorder.pause();
  }
}

function resumeRecording() {
  if (mediaRecorder && mediaRecorder.state === "paused") {
    mediaRecorder.resume();
  }
}

Example: Recording screen with chunked data

async function recordScreen() {
  const stream = await navigator.mediaDevices.getDisplayMedia({
    "video": { "width": 1920, "height": 1080 },
    "audio": true
  });
  
  // Check supported MIME types
  const options = [
    "video/webm;codecs=vp9",
    "video/webm;codecs=vp8",
    "video/webm"
  ];
  
  const mimeType = options.find(MediaRecorder.isTypeSupported);
  console.log("Using MIME type:", mimeType);
  
  const recorder = new MediaRecorder(stream, {
    "mimeType": mimeType,
    "videoBitsPerSecond": 5000000
  });
  
  const chunks = [];
  
  // Capture data every second
  recorder.ondataavailable = (event) => {
    chunks.push(event.data);
    console.log(`Chunk ${chunks.length}: ${event.data.size} bytes`);
  };
  
  recorder.onstop = async () => {
    const blob = new Blob(chunks, { "type": mimeType });
    
    // Preview recorded video
    const videoUrl = URL.createObjectURL(blob);
    const video = document.createElement("video");
    video.src = videoUrl;
    video.controls = true;
    document.body.appendChild(video);
    
    // Or upload to server
    const formData = new FormData();
    formData.append("video", blob, "screen-recording.webm");
    
    await fetch("/upload", {
      "method": "POST",
      "body": formData
    });
  };
  
  // Start with 1 second chunks
  recorder.start(1000);
  
  return recorder;
}

// Check format support
function checkMediaRecorderSupport() {
  const types = [
    "video/webm",
    "video/webm;codecs=vp9",
    "video/webm;codecs=vp8",
    "video/mp4",
    "audio/webm",
    "audio/webm;codecs=opus"
  ];
  
  types.forEach((type) => {
    const supported = MediaRecorder.isTypeSupported(type);
    console.log(`${type}: ${supported}`);
  });
}
Note: MediaRecorder support varies by browser - use MediaRecorder.isTypeSupported(mimeType) to check. Common formats: video/webm (Chrome/Firefox), video/mp4 (Safari). Use timeslice parameter in start() to get data in chunks for streaming or progress tracking.
Warning: Recording consumes memory - collect chunks in dataavailable events. Large recordings can cause memory issues - consider chunked uploads. Always stop tracks when done to release camera/microphone. Some browsers limit recording duration or file size.

5.4 Web Audio API for Audio Processing

Node Type Purpose Common Use
AudioContext Main audio processing graph. All nodes connect through context. Initialize audio system
AudioBufferSourceNode Plays audio from buffer. One-shot playback. Sound effects, samples
MediaElementSourceNode Audio source from <audio> or <video> element. Process music/video audio
MediaStreamSourceNode Audio source from MediaStream (microphone). Process live audio input
OscillatorNode Generates waveform (sine, square, sawtooth, triangle). Synthesizers, beeps, tones
GainNode Controls volume (gain). Multiply by 0.0 to 1.0+. Volume control, fading
BiquadFilterNode Frequency filter (lowpass, highpass, bandpass, etc.). EQ, bass boost, noise filter
ConvolverNode Reverb and spatial effects using impulse response. Room reverb, 3D audio
DelayNode Delays audio signal by time in seconds. Echo, chorus effects
AnalyserNode Provides frequency/time domain data. Doesn't modify audio. Visualizations, meters
StereoPannerNode Pan audio left/right (-1 to 1). Stereo positioning
ChannelMergerNode Combines multiple mono into multichannel. Mixing channels
ChannelSplitterNode Splits multichannel into separate mono. Process channels separately

Example: Basic audio playback and synthesis

// Create audio context
const audioContext = new (window.AudioContext || window.webkitAudioContext)();

// Play audio file
async function playAudioFile(url) {
  // Fetch audio data
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  
  // Decode audio data
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
  
  // Create source
  const source = audioContext.createBufferSource();
  source.buffer = audioBuffer;
  
  // Connect to destination (speakers)
  source.connect(audioContext.destination);
  
  // Play
  source.start();
  
  return source;
}

// Generate tone
function playTone(frequency, duration) {
  const oscillator = audioContext.createOscillator();
  const gainNode = audioContext.createGain();
  
  oscillator.type = "sine"; // sine, square, sawtooth, triangle
  oscillator.frequency.value = frequency; // Hz
  
  // Fade in and out
  gainNode.gain.setValueAtTime(0, audioContext.currentTime);
  gainNode.gain.linearRampToValueAtTime(0.5, audioContext.currentTime + 0.01);
  gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration);
  
  // Connect: oscillator -> gain -> destination
  oscillator.connect(gainNode);
  gainNode.connect(audioContext.destination);
  
  // Play
  oscillator.start(audioContext.currentTime);
  oscillator.stop(audioContext.currentTime + duration);
}

// Play 440Hz (A note) for 1 second
playTone(440, 1);

// Play from <audio> element with processing
function processAudioElement(audioElement) {
  const source = audioContext.createMediaElementSource(audioElement);
  const gainNode = audioContext.createGain();
  
  // Connect: source -> gain -> destination
  source.connect(gainNode);
  gainNode.connect(audioContext.destination);
  
  // Control volume
  gainNode.gain.value = 0.7;
  
  return { source, gainNode };
}

Example: Audio effects and filtering

// Create audio graph with effects
async function createAudioChain(url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
  
  // Create nodes
  const source = audioContext.createBufferSource();
  const filter = audioContext.createBiquadFilter();
  const gainNode = audioContext.createGain();
  const panner = audioContext.createStereoPanner();
  
  source.buffer = audioBuffer;
  
  // Configure filter (lowpass at 1000Hz)
  filter.type = "lowpass"; // lowpass, highpass, bandpass, notch, etc.
  filter.frequency.value = 1000;
  filter.Q.value = 1;
  
  // Configure gain (volume)
  gainNode.gain.value = 0.8;
  
  // Configure panning (center)
  panner.pan.value = 0; // -1 (left) to 1 (right)
  
  // Connect: source -> filter -> gain -> panner -> destination
  source.connect(filter);
  filter.connect(gainNode);
  gainNode.connect(panner);
  panner.connect(audioContext.destination);
  
  return { source, filter, gainNode, panner };
}

// Dynamic filter sweep
function filterSweep(filterNode, startFreq, endFreq, duration) {
  const now = audioContext.currentTime;
  filterNode.frequency.setValueAtTime(startFreq, now);
  filterNode.frequency.exponentialRampToValueAtTime(endFreq, now + duration);
}

// Fade in/out
function fadeIn(gainNode, duration) {
  const now = audioContext.currentTime;
  gainNode.gain.setValueAtTime(0, now);
  gainNode.gain.linearRampToValueAtTime(1, now + duration);
}

function fadeOut(gainNode, duration) {
  const now = audioContext.currentTime;
  gainNode.gain.setValueAtTime(gainNode.gain.value, now);
  gainNode.gain.linearRampToValueAtTime(0, now + duration);
}

// Create delay/echo effect
function createEcho(delayTime, feedback) {
  const delay = audioContext.createDelay();
  const feedbackGain = audioContext.createGain();
  const wetGain = audioContext.createGain();
  
  delay.delayTime.value = delayTime; // seconds
  feedbackGain.gain.value = feedback; // 0.0 to 1.0
  wetGain.gain.value = 0.5;
  
  // Create feedback loop
  delay.connect(feedbackGain);
  feedbackGain.connect(delay);
  delay.connect(wetGain);
  
  return { input: delay, output: wetGain };
}

Example: Audio visualization with AnalyserNode

// Create analyzer for visualization
function createVisualizer(sourceNode) {
  const analyser = audioContext.createAnalyser();
  analyser.fftSize = 2048; // 256, 512, 1024, 2048, 4096, etc.
  
  sourceNode.connect(analyser);
  analyser.connect(audioContext.destination);
  
  const bufferLength = analyser.frequencyBinCount;
  const dataArray = new Uint8Array(bufferLength);
  
  return { analyser, dataArray };
}

// Draw waveform
function drawWaveform(analyser, dataArray, canvas) {
  const ctx = canvas.getContext("2d");
  const width = canvas.width;
  const height = canvas.height;
  
  function draw() {
    requestAnimationFrame(draw);
    
    // Get time domain data
    analyser.getByteTimeDomainData(dataArray);
    
    // Clear canvas
    ctx.fillStyle = "rgb(0, 0, 0)";
    ctx.fillRect(0, 0, width, height);
    
    // Draw waveform
    ctx.lineWidth = 2;
    ctx.strokeStyle = "rgb(0, 255, 0)";
    ctx.beginPath();
    
    const sliceWidth = width / dataArray.length;
    let x = 0;
    
    for (let i = 0; i < dataArray.length; i++) {
      const v = dataArray[i] / 128.0;
      const y = v * height / 2;
      
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
      
      x += sliceWidth;
    }
    
    ctx.lineTo(width, height / 2);
    ctx.stroke();
  }
  
  draw();
}

// Draw frequency bars
function drawFrequencyBars(analyser, dataArray, canvas) {
  const ctx = canvas.getContext("2d");
  const width = canvas.width;
  const height = canvas.height;
  
  function draw() {
    requestAnimationFrame(draw);
    
    // Get frequency data
    analyser.getByteFrequencyData(dataArray);
    
    ctx.fillStyle = "rgb(0, 0, 0)";
    ctx.fillRect(0, 0, width, height);
    
    const barWidth = (width / dataArray.length) * 2.5;
    let x = 0;
    
    for (let i = 0; i < dataArray.length; i++) {
      const barHeight = (dataArray[i] / 255) * height;
      
      ctx.fillStyle = `rgb(${dataArray[i] + 100}, 50, 50)`;
      ctx.fillRect(x, height - barHeight, barWidth, barHeight);
      
      x += barWidth + 1;
    }
  }
  
  draw();
}
Note: AudioContext may be suspended on page load - resume with user gesture: audioContext.resume(). All timing uses audioContext.currentTime (high precision). Audio nodes are one-time use - create new nodes for each playback. Use AudioParam methods (setValueAtTime, linearRampToValueAtTime) for smooth parameter changes.

5.5 Canvas API for 2D Graphics and Drawing

Method Category Methods Purpose
Rectangles fillRect, strokeRect, clearRect Draw filled/outlined rectangles, clear area
Paths beginPath, moveTo, lineTo, arc, arcTo, quadraticCurveTo, bezierCurveTo, closePath Create complex shapes with lines and curves
Path Drawing fill, stroke, clip Render paths as filled, outlined, or clipping region
Text fillText, strokeText, measureText Draw and measure text
Images drawImage Draw images, video frames, or other canvases
Transformations translate, rotate, scale, transform, setTransform, resetTransform Move, rotate, scale drawing operations
State save, restore Save/restore drawing state (styles, transforms)
Pixel Data getImageData, putImageData, createImageData Direct pixel manipulation
Style Property Type Description Example
fillStyle string | CanvasGradient | CanvasPattern Fill color/gradient/pattern for shapes and text "#ff0000", "rgb(255,0,0)"
strokeStyle string | CanvasGradient | CanvasPattern Stroke color/gradient/pattern for outlines "blue", "rgba(0,0,255,0.5)"
lineWidth number Line thickness in pixels 2, 5.5
lineCap string Line end style: "butt", "round", "square" "round"
lineJoin string Line corner style: "miter", "round", "bevel" "round"
font string Text font (CSS syntax) "16px Arial", "bold 20px serif"
textAlign string "left", "right", "center", "start", "end" "center"
textBaseline string "top", "middle", "bottom", "alphabetic", "hanging" "middle"
globalAlpha number Global transparency 0.0 to 1.0 0.5
globalCompositeOperation string Blending mode: "source-over", "multiply", "screen", etc. "multiply"
shadowColor string Shadow color "rgba(0,0,0,0.5)"
shadowBlur number Shadow blur radius in pixels 10
shadowOffsetX number Horizontal shadow offset 5
shadowOffsetY number Vertical shadow offset 5

Example: Basic shapes and text

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

// Rectangle
ctx.fillStyle = "#ff0000";
ctx.fillRect(10, 10, 100, 50);

// Outlined rectangle
ctx.strokeStyle = "#00ff00";
ctx.lineWidth = 3;
ctx.strokeRect(120, 10, 100, 50);

// Circle
ctx.fillStyle = "#0000ff";
ctx.beginPath();
ctx.arc(75, 150, 40, 0, Math.PI * 2);
ctx.fill();

// Triangle
ctx.fillStyle = "#ffff00";
ctx.beginPath();
ctx.moveTo(175, 110);
ctx.lineTo(225, 190);
ctx.lineTo(125, 190);
ctx.closePath();
ctx.fill();

// Text
ctx.fillStyle = "#000000";
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Hello Canvas!", 150, 250);

// Outlined text
ctx.strokeStyle = "#ff00ff";
ctx.lineWidth = 2;
ctx.strokeText("Outlined", 150, 280);

// Clear area
ctx.clearRect(50, 50, 50, 50);

Example: Paths, gradients, and patterns

// Complex path
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.quadraticCurveTo(200, 75, 150, 100);
ctx.bezierCurveTo(150, 120, 100, 140, 50, 100);
ctx.closePath();
ctx.fillStyle = "#ff6600";
ctx.fill();
ctx.strokeStyle = "#000000";
ctx.lineWidth = 2;
ctx.stroke();

// Linear gradient
const linearGrad = ctx.createLinearGradient(0, 0, 200, 0);
linearGrad.addColorStop(0, "red");
linearGrad.addColorStop(0.5, "yellow");
linearGrad.addColorStop(1, "blue");
ctx.fillStyle = linearGrad;
ctx.fillRect(10, 200, 200, 50);

// Radial gradient
const radialGrad = ctx.createRadialGradient(150, 350, 10, 150, 350, 50);
radialGrad.addColorStop(0, "white");
radialGrad.addColorStop(1, "black");
ctx.fillStyle = radialGrad;
ctx.beginPath();
ctx.arc(150, 350, 50, 0, Math.PI * 2);
ctx.fill();

// Pattern from image
const img = new Image();
img.onload = () => {
  const pattern = ctx.createPattern(img, "repeat"); // repeat, repeat-x, repeat-y, no-repeat
  ctx.fillStyle = pattern;
  ctx.fillRect(250, 200, 150, 150);
};
img.src = "pattern.png";

// Shadows
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = "#00ff00";
ctx.fillRect(300, 50, 100, 100);

// Reset shadows
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;

Example: Transformations and animations

// Save/restore state
ctx.save();

// Translate (move origin)
ctx.translate(100, 100);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 50, 50); // Drawn at (100, 100)

// Rotate (radians)
ctx.rotate(Math.PI / 4); // 45 degrees
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 50, 50);

// Restore original state
ctx.restore();

// Scale
ctx.save();
ctx.scale(2, 2); // 2x size
ctx.fillRect(10, 10, 50, 50); // Drawn 2x larger
ctx.restore();

// Animation loop
let angle = 0;

function animate() {
  // Clear canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // Save state
  ctx.save();
  
  // Move to center
  ctx.translate(canvas.width / 2, canvas.height / 2);
  
  // Rotate
  ctx.rotate(angle);
  
  // Draw rotating rectangle
  ctx.fillStyle = "purple";
  ctx.fillRect(-25, -25, 50, 50);
  
  // Restore state
  ctx.restore();
  
  // Update angle
  angle += 0.02;
  
  // Continue animation
  requestAnimationFrame(animate);
}

animate();

// Draw image
const image = new Image();
image.onload = () => {
  // Draw whole image
  ctx.drawImage(image, 0, 0);
  
  // Draw scaled
  ctx.drawImage(image, 0, 0, 100, 100);
  
  // Draw cropped and positioned
  // drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
  ctx.drawImage(image, 50, 50, 100, 100, 200, 200, 150, 150);
};
image.src = "photo.jpg";

Example: Pixel manipulation

// Get pixel data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data; // Uint8ClampedArray [r,g,b,a, r,g,b,a, ...]

// Grayscale filter
for (let i = 0; i < pixels.length; i += 4) {
  const r = pixels[i];
  const g = pixels[i + 1];
  const b = pixels[i + 2];
  const gray = 0.299 * r + 0.587 * g + 0.114 * b;
  
  pixels[i] = gray;     // Red
  pixels[i + 1] = gray; // Green
  pixels[i + 2] = gray; // Blue
  // pixels[i + 3] is alpha (unchanged)
}

// Put modified pixels back
ctx.putImageData(imageData, 0, 0);

// Invert colors
for (let i = 0; i < pixels.length; i += 4) {
  pixels[i] = 255 - pixels[i];         // Red
  pixels[i + 1] = 255 - pixels[i + 1]; // Green
  pixels[i + 2] = 255 - pixels[i + 2]; // Blue
}
ctx.putImageData(imageData, 0, 0);

// Increase brightness
const brightness = 50;
for (let i = 0; i < pixels.length; i += 4) {
  pixels[i] += brightness;     // Red
  pixels[i + 1] += brightness; // Green
  pixels[i + 2] += brightness; // Blue
}
ctx.putImageData(imageData, 0, 0);
Note: Canvas operations are immediate mode - no scene graph. Use requestAnimationFrame for smooth animations. save() and restore() manage state stack (styles, transforms). Canvas is raster-based - scales poorly; use CSS for sizing and set canvas.width/height for resolution.

5.6 WebGL API for 3D Graphics Rendering

WebGL Concept Description Purpose
Context canvas.getContext("webgl") or "webgl2" Initialize WebGL rendering context
Shader GLSL programs (vertex and fragment shaders) GPU-side rendering logic
Program Linked vertex + fragment shader pair Complete rendering pipeline
Buffer GPU memory for vertex data (positions, colors, UVs) Store geometry data
Attribute Per-vertex data (position, normal, UV) Vertex shader inputs
Uniform Global shader variables (MVP matrix, time, color) Shader parameters
Texture Image data on GPU for mapping onto geometry Surface details, materials

Example: Basic WebGL setup and triangle

const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");

if (!gl) {
  console.error("WebGL not supported");
}

// Vertex shader (GLSL)
const vertexShaderSource = `
  attribute vec2 a_position;
  void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
  }
`;

// Fragment shader (GLSL)
const fragmentShaderSource = `
  precision mediump float;
  uniform vec4 u_color;
  void main() {
    gl_FragColor = u_color;
  }
`;

// Create shader
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error("Shader compile error:", gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  
  return shader;
}

// Create program
function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error("Program link error:", gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }
  
  return program;
}

// Compile shaders
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

// Create program
const program = createProgram(gl, vertexShader, fragmentShader);

// Get attribute/uniform locations
const positionLocation = gl.getAttribLocation(program, "a_position");
const colorLocation = gl.getUniformLocation(program, "u_color");

// Triangle vertices
const positions = new Float32Array([
  0.0,  0.5,  // Top
  -0.5, -0.5, // Bottom left
  0.5, -0.5   // Bottom right
]);

// Create buffer
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

// Clear canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// Use program
gl.useProgram(program);

// Enable attribute
gl.enableVertexAttribArray(positionLocation);

// Bind buffer
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

// Tell attribute how to get data
gl.vertexAttribPointer(
  positionLocation,
  2,          // 2 components per iteration
  gl.FLOAT,   // data type
  false,      // normalize
  0,          // stride
  0           // offset
);

// Set color uniform
gl.uniform4f(colorLocation, 1.0, 0.0, 0.0, 1.0); // Red

// Draw
gl.drawArrays(gl.TRIANGLES, 0, 3);

Example: Textured quad with WebGL

// Vertex shader with UVs
const vertexShaderSource = `
  attribute vec2 a_position;
  attribute vec2 a_texCoord;
  varying vec2 v_texCoord;
  
  void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
    v_texCoord = a_texCoord;
  }
`;

// Fragment shader with texture
const fragmentShaderSource = `
  precision mediump float;
  varying vec2 v_texCoord;
  uniform sampler2D u_texture;
  
  void main() {
    gl_FragColor = texture2D(u_texture, v_texCoord);
  }
`;

// Quad vertices (2 triangles)
const positions = new Float32Array([
  -0.5,  0.5,  // Top left
  -0.5, -0.5,  // Bottom left
  0.5,  0.5,   // Top right
  0.5, -0.5    // Bottom right
]);

// Texture coordinates
const texCoords = new Float32Array([
  0.0, 0.0,  // Top left
  0.0, 1.0,  // Bottom left
  1.0, 0.0,  // Top right
  1.0, 1.0   // Bottom right
]);

// Create texture
function loadTexture(gl, url) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  
  // Temporary 1x1 blue pixel
  gl.texImage2D(
    gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
    gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([0, 0, 255, 255])
  );
  
  // Load image
  const image = new Image();
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    
    // Set filtering
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  };
  image.src = url;
  
  return texture;
}

// Draw textured quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
Note: WebGL is low-level - consider libraries like Three.js, Babylon.js for easier 3D. WebGL 2 has better features but less browser support than WebGL 1. Always check for WebGL support: !!canvas.getContext("webgl"). Shaders are written in GLSL (OpenGL Shading Language).
Warning: WebGL can fail on old hardware or when GPU is unavailable. Always check context creation. Shader compilation can fail - always check gl.getShaderParameter and gl.getProgramParameter. WebGL state is global - careful with state management in complex apps.

Media and Graphics API Best Practices

  • Use play().catch() to handle autoplay failures gracefully
  • Always stop() media tracks when done to release camera/microphone hardware
  • Request minimal permissions - audio OR video separately if possible
  • Use MediaRecorder.isTypeSupported() to check format support before recording
  • For Web Audio, resume AudioContext with user gesture: audioContext.resume()
  • Connect audio nodes before starting playback - nodes can't be reconnected after start
  • Use requestAnimationFrame for smooth Canvas and WebGL animations
  • Save/restore Canvas state with ctx.save() and ctx.restore() for clean transforms
  • For Canvas, set canvas.width and canvas.height for resolution, use CSS for display size
  • Consider Three.js or Babylon.js instead of raw WebGL for complex 3D scenes
  • Always check WebGL shader compilation and program linking for errors
  • Use getUserMedia constraints wisely - ideal values are preferred, not mandatory

6. Device and Sensor Integration APIs

6.1 Geolocation API for Location Services

Method Syntax Description Browser Support
getCurrentPosition navigator.geolocation.getCurrentPosition(success, error, options) Gets current location once. Requires user permission. HTTPS required. All Browsers
watchPosition navigator.geolocation.watchPosition(success, error, options) Watches location changes continuously. Returns watch ID. Requires permission. All Browsers
clearWatch navigator.geolocation.clearWatch(id) Stops watching location. Use watch ID from watchPosition. All Browsers
Position Property Type Description Always Available
coords.latitude number Latitude in decimal degrees (-90 to 90). Yes
coords.longitude number Longitude in decimal degrees (-180 to 180). Yes
coords.accuracy number Accuracy in meters. Lower is better. Yes
coords.altitude number | null Altitude in meters above sea level. May be null. No
coords.altitudeAccuracy number | null Altitude accuracy in meters. May be null. No
coords.heading number | null Direction of travel in degrees (0-360). 0 is north. May be null. No
coords.speed number | null Speed in meters per second. May be null. No
timestamp number DOMTimeStamp when position was acquired. Yes
Option Type Description Default
enableHighAccuracy boolean Request GPS/high accuracy. Slower, more battery. Desktop may ignore. false
timeout number Max time in milliseconds to wait. 0 means no timeout. Infinity
maximumAge number Max age in milliseconds of cached position. 0 means no cache. 0
Error Code Value Meaning
PERMISSION_DENIED 1 User denied permission or location disabled.
POSITION_UNAVAILABLE 2 Location information unavailable (no GPS signal, network error).
TIMEOUT 3 Request timed out before getting location.

Example: Get current location

// Check if geolocation is supported
if ("geolocation" in navigator) {
  console.log("Geolocation available");
} else {
  console.log("Geolocation not supported");
}

// Get current position
navigator.geolocation.getCurrentPosition(
  (position) => {
    const { latitude, longitude, accuracy } = position.coords;
    console.log(`Location: ${latitude}, ${longitude}`);
    console.log(`Accuracy: ${accuracy} meters`);
    console.log(`Timestamp: ${new Date(position.timestamp)}`);
    
    // Optional fields
    if (position.coords.altitude !== null) {
      console.log(`Altitude: ${position.coords.altitude} meters`);
    }
    if (position.coords.speed !== null) {
      console.log(`Speed: ${position.coords.speed} m/s`);
    }
    if (position.coords.heading !== null) {
      console.log(`Heading: ${position.coords.heading} degrees`);
    }
  },
  (error) => {
    switch (error.code) {
      case error.PERMISSION_DENIED:
        console.error("User denied geolocation permission");
        break;
      case error.POSITION_UNAVAILABLE:
        console.error("Location unavailable");
        break;
      case error.TIMEOUT:
        console.error("Geolocation timeout");
        break;
      default:
        console.error("Unknown geolocation error");
    }
  },
  {
    "enableHighAccuracy": true,
    "timeout": 10000,
    "maximumAge": 0
  }
);

Example: Watch position and distance calculation

let watchId;

// Start watching position
function startWatching() {
  watchId = navigator.geolocation.watchPosition(
    (position) => {
      const { latitude, longitude, accuracy } = position.coords;
      console.log(`Updated location: ${latitude}, ${longitude}`);
      console.log(`Accuracy: ${accuracy}m`);
      
      // Update map or UI
      updateMap(latitude, longitude);
    },
    (error) => {
      console.error("Watch position error:", error.message);
    },
    {
      "enableHighAccuracy": true,
      "timeout": 5000,
      "maximumAge": 2000
    }
  );
  
  console.log("Watching position, ID:", watchId);
}

// Stop watching
function stopWatching() {
  if (watchId) {
    navigator.geolocation.clearWatch(watchId);
    console.log("Stopped watching position");
  }
}

// Calculate distance between two points (Haversine formula)
function calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 6371e3; // Earth radius in meters
  const φ1 = lat1 * Math.PI / 180;
  const φ2 = lat2 * Math.PI / 180;
  const Δφ = (lat2 - lat1) * Math.PI / 180;
  const Δλ = (lon2 - lon1) * Math.PI / 180;
  
  const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
            Math.cos(φ1) * Math.cos(φ2) *
            Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  
  return R * c; // Distance in meters
}

// Usage
const distance = calculateDistance(51.5074, -0.1278, 48.8566, 2.3522);
console.log(`Distance: ${(distance / 1000).toFixed(2)} km`);
Note: Geolocation requires HTTPS (or localhost for development). User must grant permission - request shows browser prompt. Accuracy varies by device (GPS, WiFi, IP). enableHighAccuracy uses GPS on mobile (slower, more battery). Desktop typically uses IP/WiFi regardless.
Warning: watchPosition can drain battery - always clearWatch() when done. Don't rely on optional fields (altitude, speed, heading) - may be null. Location can be spoofed - don't trust for security. Privacy concern - explain why you need location to users.

6.2 Device Orientation and Motion APIs

Event Description Browser Support
deviceorientation Fires when device orientation changes. Provides alpha, beta, gamma rotation angles. Mobile Browsers
deviceorientationabsolute Like deviceorientation but relative to Earth's coordinate frame (compass). Modern Mobile
devicemotion Fires when device acceleration changes. Provides acceleration and rotation rate data. Mobile Browsers
DeviceOrientation Property Type Description Range
alpha number Rotation around Z axis (compass direction). 0-360 degrees. 0-360
beta number Rotation around X axis (front-to-back tilt). -180 to 180 degrees. -180 to 180
gamma number Rotation around Y axis (left-to-right tilt). -90 to 90 degrees. -90 to 90
absolute boolean true if orientation is absolute (compass-based), false if arbitrary. -
DeviceMotion Property Type Description Units
acceleration object Acceleration excluding gravity. Properties: x, y, z. m/s²
accelerationIncludingGravity object Acceleration including gravity. Properties: x, y, z. m/s²
rotationRate object Rotation speed. Properties: alpha, beta, gamma. deg/s
interval number Interval in milliseconds between events. ms

Example: Device orientation

// Request permission (iOS 13+)
async function requestOrientationPermission() {
  if (typeof DeviceOrientationEvent !== "undefined" &&
      typeof DeviceOrientationEvent.requestPermission === "function") {
    try {
      const permission = await DeviceOrientationEvent.requestPermission();
      if (permission === "granted") {
        console.log("Orientation permission granted");
        return true;
      }
    } catch (error) {
      console.error("Orientation permission error:", error);
    }
    return false;
  }
  return true; // No permission needed (Android, older iOS)
}

// Listen for orientation changes
window.addEventListener("deviceorientation", (event) => {
  const alpha = event.alpha; // Compass direction (0-360)
  const beta = event.beta;   // Front-to-back tilt (-180 to 180)
  const gamma = event.gamma; // Left-to-right tilt (-90 to 90)
  const absolute = event.absolute;
  
  console.log(`Alpha (Z): ${alpha?.toFixed(2)}°`);
  console.log(`Beta (X): ${beta?.toFixed(2)}°`);
  console.log(`Gamma (Y): ${gamma?.toFixed(2)}°`);
  console.log(`Absolute: ${absolute}`);
  
  // Rotate element based on device orientation
  const element = document.querySelector(".compass");
  if (alpha !== null) {
    element.style.transform = `rotate(${-alpha}deg)`;
  }
});

// Compass heading (absolute orientation)
window.addEventListener("deviceorientationabsolute", (event) => {
  const heading = event.alpha; // True compass heading
  console.log(`Compass heading: ${heading?.toFixed(2)}°`);
});

Example: Device motion and shake detection

// Request permission (iOS 13+)
async function requestMotionPermission() {
  if (typeof DeviceMotionEvent !== "undefined" &&
      typeof DeviceMotionEvent.requestPermission === "function") {
    try {
      const permission = await DeviceMotionEvent.requestPermission();
      return permission === "granted";
    } catch (error) {
      console.error("Motion permission error:", error);
      return false;
    }
  }
  return true;
}

// Listen for motion events
window.addEventListener("devicemotion", (event) => {
  // Acceleration (without gravity)
  const acc = event.acceleration;
  if (acc) {
    console.log(`Acceleration: x=${acc.x}, y=${acc.y}, z=${acc.z} m/s²`);
  }
  
  // Acceleration (with gravity)
  const accGrav = event.accelerationIncludingGravity;
  if (accGrav) {
    console.log(`With gravity: x=${accGrav.x}, y=${accGrav.y}, z=${accGrav.z}`);
  }
  
  // Rotation rate
  const rotation = event.rotationRate;
  if (rotation) {
    console.log(`Rotation: α=${rotation.alpha}, β=${rotation.beta}, γ=${rotation.gamma} deg/s`);
  }
  
  console.log(`Interval: ${event.interval}ms`);
});

// Shake detection
let lastTime = 0;
let lastX = 0, lastY = 0, lastZ = 0;
const shakeThreshold = 15; // Adjust sensitivity

window.addEventListener("devicemotion", (event) => {
  const acc = event.accelerationIncludingGravity;
  if (!acc) return;
  
  const currentTime = Date.now();
  const timeDiff = currentTime - lastTime;
  
  if (timeDiff > 100) {
    const deltaX = Math.abs(acc.x - lastX);
    const deltaY = Math.abs(acc.y - lastY);
    const deltaZ = Math.abs(acc.z - lastZ);
    
    const speed = (deltaX + deltaY + deltaZ) / timeDiff * 10000;
    
    if (speed > shakeThreshold) {
      console.log("Device shaken!");
      onShake();
    }
    
    lastTime = currentTime;
    lastX = acc.x;
    lastY = acc.y;
    lastZ = acc.z;
  }
});

function onShake() {
  console.log("Shake detected!");
  // Trigger shake action
}
Note: iOS 13+ requires user permission and user gesture to access orientation/motion. Request permission with DeviceOrientationEvent.requestPermission(). Events fire frequently (30-60 times/second) - throttle expensive operations. Desktop browsers may not support these events.
Warning: Sensor data can be noisy - apply smoothing/filtering for better results. Battery drain with continuous monitoring - remove listeners when not needed. Privacy concern - can be used for fingerprinting. Not all devices have all sensors (gyroscope, magnetometer).

6.3 Battery Status API (deprecated but legacy)

Property Type Description Status
navigator.getBattery() Promise<BatteryManager> Returns Promise with battery status object. DEPRECATED
level number Battery level 0.0 to 1.0 (0% to 100%). Read-only
charging boolean true if device is charging. Read-only
chargingTime number Seconds until fully charged. Infinity if not charging or unknown. Read-only
dischargingTime number Seconds until battery empty. Infinity if charging or unknown. Read-only
Event When Fired
levelchange Battery level changed
chargingchange Charging status changed (plugged/unplugged)
chargingtimechange Charging time estimate changed
dischargingtimechange Discharging time estimate changed

Example: Battery status monitoring

// Check if API is available
if ("getBattery" in navigator) {
  navigator.getBattery().then((battery) => {
    // Initial state
    console.log(`Battery level: ${battery.level * 100}%`);
    console.log(`Charging: ${battery.charging}`);
    console.log(`Charging time: ${battery.chargingTime} seconds`);
    console.log(`Discharging time: ${battery.dischargingTime} seconds`);
    
    // Listen for level changes
    battery.addEventListener("levelchange", () => {
      console.log(`Battery level: ${battery.level * 100}%`);
      
      // Warn if low battery
      if (battery.level < 0.2 && !battery.charging) {
        console.warn("Low battery!");
      }
    });
    
    // Listen for charging state changes
    battery.addEventListener("chargingchange", () => {
      if (battery.charging) {
        console.log("Device is charging");
      } else {
        console.log("Device is not charging");
      }
    });
    
    // Charging time updates
    battery.addEventListener("chargingtimechange", () => {
      if (battery.chargingTime !== Infinity) {
        const minutes = Math.floor(battery.chargingTime / 60);
        console.log(`Fully charged in ${minutes} minutes`);
      }
    });
    
    // Discharging time updates
    battery.addEventListener("dischargingtimechange", () => {
      if (battery.dischargingTime !== Infinity) {
        const minutes = Math.floor(battery.dischargingTime / 60);
        console.log(`Battery empty in ${minutes} minutes`);
      }
    });
  });
} else {
  console.log("Battery Status API not supported");
}
Warning: Battery Status API is DEPRECATED due to privacy concerns (fingerprinting). Removed from most browsers. Firefox disabled by default. Chrome plans removal. Don't rely on this API for new projects. Use feature detection before accessing.

6.4 Vibration API for Haptic Feedback

Method Syntax Description Browser Support
vibrate navigator.vibrate(pattern) Vibrates device with pattern. Pattern is number (ms) or array [vibrate, pause, ...]. Mobile Browsers
vibrate(0) navigator.vibrate(0) Stops ongoing vibration. Mobile Browsers

Example: Vibration patterns

// Check support
if ("vibrate" in navigator) {
  console.log("Vibration API supported");
} else {
  console.log("Vibration not supported");
}

// Simple vibration (200ms)
navigator.vibrate(200);

// Vibration pattern [vibrate, pause, vibrate, pause, ...]
// Vibrate 200ms, pause 100ms, vibrate 200ms
navigator.vibrate([200, 100, 200]);

// SOS pattern (... --- ...)
navigator.vibrate([
  100, 30, 100, 30, 100,  // S (...)
  200,                     // Pause
  200, 30, 200, 30, 200,  // O (---)
  200,                     // Pause
  100, 30, 100, 30, 100   // S (...)
]);

// Stop vibration
navigator.vibrate(0);
// or
navigator.vibrate([]);

// Pulse pattern (heartbeat)
function vibratePulse() {
  navigator.vibrate([50, 50, 50, 50, 200]);
}

// Success feedback
function vibrateSuccess() {
  navigator.vibrate([100, 50, 100]);
}

// Error feedback
function vibrateError() {
  navigator.vibrate([200, 100, 200, 100, 200]);
}

// Click feedback
function vibrateClick() {
  navigator.vibrate(10); // Short tap
}

// Button with haptic feedback
document.querySelector("button").addEventListener("click", () => {
  navigator.vibrate(50);
  // Handle button action
});
Note: Vibration only works on mobile devices with vibration hardware. Desktop browsers ignore vibrate() calls. Maximum vibration time limited by browsers (few seconds). User can disable vibration in system settings. Requires user gesture on some browsers.
Warning: Don't overuse vibration - annoying and drains battery. Respect user preferences - provide option to disable. Some users rely on vibration for accessibility - use meaningfully. Vibration patterns limited to prevent abuse (max length, max duration).

6.5 Screen Orientation API and Lock

Property/Method Description Browser Support
screen.orientation.type Current orientation: "portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary". Modern Browsers
screen.orientation.angle Orientation angle: 0, 90, 180, or 270 degrees. Modern Browsers
screen.orientation.lock(orientation) Locks to specific orientation. Returns Promise. Requires fullscreen on most browsers. Modern Mobile
screen.orientation.unlock() Unlocks orientation, allowing device rotation. Modern Mobile
orientationchange event Fires when screen orientation changes. Modern Browsers
Orientation Value Description
portrait-primary Normal portrait (0° or 180°)
portrait-secondary Upside-down portrait (180° or 0°)
landscape-primary Normal landscape (90° or 270°)
landscape-secondary Reverse landscape (270° or 90°)
portrait Any portrait orientation
landscape Any landscape orientation
any Allow all orientations (default)

Example: Detect and lock orientation

// Check current orientation
if (screen.orientation) {
  console.log(`Type: ${screen.orientation.type}`);
  console.log(`Angle: ${screen.orientation.angle}°`);
} else {
  console.log("Screen Orientation API not supported");
}

// Listen for orientation changes
screen.orientation?.addEventListener("change", () => {
  console.log(`Orientation changed to: ${screen.orientation.type}`);
  console.log(`Angle: ${screen.orientation.angle}°`);
  
  // Adjust layout based on orientation
  if (screen.orientation.type.startsWith("landscape")) {
    document.body.classList.add("landscape");
    document.body.classList.remove("portrait");
  } else {
    document.body.classList.add("portrait");
    document.body.classList.remove("landscape");
  }
});

// Lock orientation (requires fullscreen)
async function lockLandscape() {
  try {
    // Enter fullscreen first
    await document.documentElement.requestFullscreen();
    
    // Lock to landscape
    await screen.orientation.lock("landscape");
    console.log("Locked to landscape");
  } catch (error) {
    console.error("Failed to lock orientation:", error);
  }
}

// Lock to specific orientation
async function lockOrientation(orientation) {
  try {
    await screen.orientation.lock(orientation);
    console.log(`Locked to ${orientation}`);
  } catch (error) {
    if (error.name === "NotSupportedError") {
      console.error("Orientation lock not supported");
    } else if (error.name === "SecurityError") {
      console.error("Must be in fullscreen to lock orientation");
    } else {
      console.error("Lock failed:", error);
    }
  }
}

// Unlock orientation
function unlockOrientation() {
  screen.orientation?.unlock();
  console.log("Orientation unlocked");
}

// Game example: lock to landscape when playing
async function startGame() {
  await document.documentElement.requestFullscreen();
  await screen.orientation.lock("landscape");
  // Start game
}

function exitGame() {
  screen.orientation.unlock();
  document.exitFullscreen();
}
Note: Orientation lock typically requires fullscreen mode. Desktop browsers may not support orientation lock. Use CSS media queries for responsive design alongside API. Some browsers use window.orientation (deprecated) - use screen.orientation instead.
Warning: Don't lock orientation unless necessary (games, video). Respect user preference - they rotated device intentionally. Unlock orientation when leaving fullscreen. May not work in iframes or certain contexts.

6.6 Generic Sensor API and Accelerometer

Sensor Description Browser Support
Accelerometer Measures acceleration in m/s² along X, Y, Z axes. Limited (Chrome)
LinearAccelerationSensor Acceleration excluding gravity. Limited (Chrome)
GravitySensor Gravity component of acceleration. Limited (Chrome)
Gyroscope Measures angular velocity (rotation rate) in rad/s. Limited (Chrome)
Magnetometer Measures magnetic field in µT (microtesla). Limited (Chrome)
AbsoluteOrientationSensor Device orientation relative to Earth's coordinate system. Limited (Chrome)
RelativeOrientationSensor Device orientation relative to arbitrary reference. Limited (Chrome)
AmbientLightSensor Measures ambient light in lux. Limited (Chrome)
Sensor Method/Property Description
start() Starts sensor. Fires reading event when data available.
stop() Stops sensor and data collection.
x, y, z Sensor readings for respective axes.
timestamp DOMHighResTimeStamp when reading was taken.
activated true if sensor is active and providing readings.
reading event Fires when new sensor reading is available.
error event Fires when sensor error occurs.

Example: Accelerometer usage

// Request permission (if needed)
async function requestSensorPermission() {
  try {
    const result = await navigator.permissions.query({ "name": "accelerometer" });
    if (result.state === "denied") {
      console.error("Accelerometer permission denied");
      return false;
    }
    return true;
  } catch (error) {
    console.error("Permission query failed:", error);
    return false;
  }
}

// Create accelerometer
try {
  const accelerometer = new Accelerometer({
    "frequency": 60 // 60 Hz (readings per second)
  });
  
  // Listen for readings
  accelerometer.addEventListener("reading", () => {
    console.log(`Acceleration: x=${accelerometer.x}, y=${accelerometer.y}, z=${accelerometer.z} m/s²`);
    console.log(`Timestamp: ${accelerometer.timestamp}`);
    
    // Detect device tilt
    const tiltX = Math.atan2(accelerometer.y, accelerometer.z) * 180 / Math.PI;
    const tiltY = Math.atan2(accelerometer.x, accelerometer.z) * 180 / Math.PI;
    console.log(`Tilt: X=${tiltX.toFixed(2)}°, Y=${tiltY.toFixed(2)}°`);
  });
  
  // Handle errors
  accelerometer.addEventListener("error", (event) => {
    console.error("Accelerometer error:", event.error.name, event.error.message);
  });
  
  // Start sensor
  accelerometer.start();
  console.log("Accelerometer started");
  
  // Stop after 10 seconds
  setTimeout(() => {
    accelerometer.stop();
    console.log("Accelerometer stopped");
  }, 10000);
  
} catch (error) {
  console.error("Accelerometer not supported:", error);
}

// Linear acceleration (without gravity)
try {
  const linearAccel = new LinearAccelerationSensor({ "frequency": 60 });
  
  linearAccel.addEventListener("reading", () => {
    console.log(`Linear acceleration: x=${linearAccel.x}, y=${linearAccel.y}, z=${linearAccel.z}`);
  });
  
  linearAccel.start();
} catch (error) {
  console.error("LinearAccelerationSensor not supported:", error);
}

Example: Gyroscope and orientation sensors

// Gyroscope (rotation rate)
try {
  const gyroscope = new Gyroscope({ "frequency": 60 });
  
  gyroscope.addEventListener("reading", () => {
    console.log(`Rotation rate: x=${gyroscope.x}, y=${gyroscope.y}, z=${gyroscope.z} rad/s`);
    
    // Convert to degrees per second
    const degPerSec = {
      "x": gyroscope.x * 180 / Math.PI,
      "y": gyroscope.y * 180 / Math.PI,
      "z": gyroscope.z * 180 / Math.PI
    };
    console.log(`Rotation (deg/s): x=${degPerSec.x.toFixed(2)}, y=${degPerSec.y.toFixed(2)}, z=${degPerSec.z.toFixed(2)}`);
  });
  
  gyroscope.start();
} catch (error) {
  console.error("Gyroscope not supported:", error);
}

// Absolute orientation (quaternion)
try {
  const orientation = new AbsoluteOrientationSensor({ "frequency": 60 });
  
  orientation.addEventListener("reading", () => {
    // Quaternion representation
    const [x, y, z, w] = orientation.quaternion;
    console.log(`Quaternion: [${x}, ${y}, ${z}, ${w}]`);
    
    // Convert to Euler angles
    const angles = quaternionToEuler(x, y, z, w);
    console.log(`Euler angles: ${JSON.stringify(angles)}`);
  });
  
  orientation.start();
} catch (error) {
  console.error("AbsoluteOrientationSensor not supported:", error);
}

// Helper: Convert quaternion to Euler angles
function quaternionToEuler(x, y, z, w) {
  const roll = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
  const pitch = Math.asin(2 * (w * y - z * x));
  const yaw = Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z));
  
  return {
    "roll": roll * 180 / Math.PI,
    "pitch": pitch * 180 / Math.PI,
    "yaw": yaw * 180 / Math.PI
  };
}

// Ambient light sensor
try {
  const lightSensor = new AmbientLightSensor({ "frequency": 1 });
  
  lightSensor.addEventListener("reading", () => {
    console.log(`Illuminance: ${lightSensor.illuminance} lux`);
    
    // Adjust UI based on ambient light
    if (lightSensor.illuminance < 50) {
      document.body.classList.add("dark-mode");
    } else {
      document.body.classList.remove("dark-mode");
    }
  });
  
  lightSensor.start();
} catch (error) {
  console.error("AmbientLightSensor not supported:", error);
}
Note: Generic Sensor API has limited browser support (mainly Chrome). Requires HTTPS and user permission. Sensors may not be available on all devices. Use frequency option to control sampling rate (affects battery and CPU). Always wrap in try/catch for feature detection.
Warning: Sensor APIs can drain battery with high frequency - use lowest frequency needed. Always stop() sensors when not in use. Privacy concern - sensor data can be used for fingerprinting or side-channel attacks. Check browser compatibility before using - very limited support.

Device and Sensor API Best Practices

  • Always check feature availability with if ("geolocation" in navigator) before using APIs
  • Request location/sensor permissions with clear explanation why you need them
  • Use enableHighAccuracy: false for geolocation unless GPS precision is critical
  • Always clearWatch() geolocation watches when done to save battery
  • Throttle or debounce orientation/motion event handlers - they fire very frequently
  • Request DeviceOrientation/Motion permissions on iOS 13+ with user gesture
  • Don't rely on Battery Status API - deprecated and removed from most browsers
  • Use vibration sparingly - annoying when overused and drains battery
  • Orientation lock requires fullscreen mode - unlock when exiting fullscreen
  • Generic Sensor API has limited support - use DeviceOrientation/Motion for broader compatibility
  • Always stop sensors when not needed to conserve battery and CPU
  • Handle permission denials gracefully - provide fallback UX

7. File and Clipboard APIs

7.1 File API for File Object Manipulation

Property Type Description Read-Only
name string File name with extension (e.g., "photo.jpg"). Does not include path. Yes
size number File size in bytes. Yes
type string MIME type (e.g., "image/jpeg", "text/plain"). Empty string if unknown. Yes
lastModified number Last modified timestamp in milliseconds since epoch. Yes
lastModifiedDate Date Last modified date as Date object. Deprecated - use lastModified instead. Yes
Method Returns Description
slice(start, end, contentType) Blob Returns portion of file as new Blob. Parameters optional. Useful for chunked uploads.
text() Promise<string> Reads file content as text. Modern alternative to FileReader.
arrayBuffer() Promise<ArrayBuffer> Reads file content as ArrayBuffer. Modern alternative to FileReader.
stream() ReadableStream Returns ReadableStream for streaming file content.

Example: File input and validation

// HTML: <input type="file" id="fileInput" multiple accept="image/*">

const fileInput = document.getElementById("fileInput");

fileInput.addEventListener("change", (event) => {
  const files = event.target.files; // FileList object
  
  console.log(`${files.length} file(s) selected`);
  
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    
    console.log("File info:");
    console.log(`  Name: ${file.name}`);
    console.log(`  Size: ${formatBytes(file.size)}`);
    console.log(`  Type: ${file.type}`);
    console.log(`  Last modified: ${new Date(file.lastModified)}`);
    
    // Validate file
    if (!validateFile(file)) {
      console.error("Invalid file:", file.name);
      continue;
    }
    
    // Process file
    processFile(file);
  }
});

// Validate file
function validateFile(file) {
  // Check file type
  const validTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
  if (!validTypes.includes(file.type)) {
    alert(`Invalid file type: ${file.type}`);
    return false;
  }
  
  // Check file size (max 5MB)
  const maxSize = 5 * 1024 * 1024;
  if (file.size > maxSize) {
    alert(`File too large: ${formatBytes(file.size)} (max ${formatBytes(maxSize)})`);
    return false;
  }
  
  // Check file name
  if (file.name.length > 255) {
    alert("File name too long");
    return false;
  }
  
  return true;
}

// Format bytes
function formatBytes(bytes) {
  if (bytes === 0) return "0 Bytes";
  const k = 1024;
  const sizes = ["Bytes", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
}

Example: Modern file reading with promises

// Read file as text
async function readFileAsText(file) {
  try {
    const text = await file.text();
    console.log("File content:", text);
    return text;
  } catch (error) {
    console.error("Error reading file:", error);
  }
}

// Read file as ArrayBuffer
async function readFileAsArrayBuffer(file) {
  try {
    const buffer = await file.arrayBuffer();
    console.log("Buffer size:", buffer.byteLength);
    return buffer;
  } catch (error) {
    console.error("Error reading file:", error);
  }
}

// Read file as data URL for preview
async function readFileAsDataURL(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(file);
  });
}

// Usage
fileInput.addEventListener("change", async (event) => {
  const file = event.target.files[0];
  
  if (file.type.startsWith("text/")) {
    const text = await file.text();
    console.log(text);
  } else if (file.type.startsWith("image/")) {
    const dataURL = await readFileAsDataURL(file);
    const img = document.createElement("img");
    img.src = dataURL;
    document.body.appendChild(img);
  }
});

// Slice file for chunked upload
function uploadFileInChunks(file, chunkSize = 1024 * 1024) {
  const chunks = Math.ceil(file.size / chunkSize);
  
  for (let i = 0; i < chunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    console.log(`Chunk ${i + 1}/${chunks}: ${chunk.size} bytes`);
    uploadChunk(chunk, i);
  }
}

async function uploadChunk(chunk, index) {
  const formData = new FormData();
  formData.append("chunk", chunk);
  formData.append("index", index);
  
  await fetch("/upload-chunk", {
    "method": "POST",
    "body": formData
  });
}
Note: File objects are Blob subclasses with additional name and lastModified properties. Files from <input type="file"> are read-only. Use accept attribute to filter file types in picker. Modern methods (text(), arrayBuffer()) are Promise-based and simpler than FileReader.

7.2 FileReader API for File Content Reading

Method Description Result Format
readAsText(blob, encoding) Reads file as text string. Optional encoding (default UTF-8). string
readAsDataURL(blob) Reads file as data URL (base64-encoded). Good for images. data:image/jpeg;base64,...
readAsArrayBuffer(blob) Reads file as ArrayBuffer. Good for binary data. ArrayBuffer
readAsBinaryString(blob) Reads file as binary string. Deprecated - use readAsArrayBuffer instead. string
abort() Aborts ongoing read operation. Triggers abort event. -
Property Type Description
result string | ArrayBuffer File content after successful read. null before load event.
error DOMException Error object if read failed. null otherwise.
readyState number 0=EMPTY, 1=LOADING, 2=DONE
Event When Fired
loadstart Read operation started
progress During read (for progress tracking). event.loaded and event.total available.
load Read completed successfully. Result available in reader.result.
loadend Read completed (success or failure)
error Read failed. Error available in reader.error.
abort Read aborted with abort() method

Example: FileReader with event handlers

function readFile(file) {
  const reader = new FileReader();
  
  // Progress tracking
  reader.addEventListener("loadstart", () => {
    console.log("Reading started");
  });
  
  reader.addEventListener("progress", (event) => {
    if (event.lengthComputable) {
      const percent = (event.loaded / event.total) * 100;
      console.log(`Progress: ${percent.toFixed(2)}%`);
      updateProgressBar(percent);
    }
  });
  
  reader.addEventListener("load", () => {
    console.log("Reading completed");
    console.log("Result:", reader.result);
    
    // Display image
    if (file.type.startsWith("image/")) {
      const img = document.createElement("img");
      img.src = reader.result;
      document.body.appendChild(img);
    }
  });
  
  reader.addEventListener("error", () => {
    console.error("Read error:", reader.error);
  });
  
  reader.addEventListener("abort", () => {
    console.log("Read aborted");
  });
  
  reader.addEventListener("loadend", () => {
    console.log("Read ended");
  });
  
  // Start reading
  if (file.type.startsWith("image/")) {
    reader.readAsDataURL(file);
  } else if (file.type.startsWith("text/")) {
    reader.readAsText(file);
  } else {
    reader.readAsArrayBuffer(file);
  }
}

// Image preview
function previewImage(file) {
  const reader = new FileReader();
  
  reader.onload = (event) => {
    const img = new Image();
    img.src = event.target.result;
    img.onload = () => {
      console.log(`Image dimensions: ${img.width}x${img.height}`);
    };
    document.getElementById("preview").appendChild(img);
  };
  
  reader.readAsDataURL(file);
}

// Read CSV file
function readCSV(file) {
  const reader = new FileReader();
  
  reader.onload = (event) => {
    const text = event.target.result;
    const rows = text.split("\n").map((row) => row.split(","));
    console.log("CSV data:", rows);
  };
  
  reader.readAsText(file);
}

// Read binary file
function readBinary(file) {
  const reader = new FileReader();
  
  reader.onload = (event) => {
    const buffer = event.target.result;
    const view = new Uint8Array(buffer);
    console.log("First 10 bytes:", Array.from(view.slice(0, 10)));
  };
  
  reader.readAsArrayBuffer(file);
}
Note: FileReader is event-based and asynchronous. For modern code, prefer file.text() or file.arrayBuffer() which return Promises. readAsDataURL creates large base64 strings - consider object URLs for better performance. Each FileReader instance can only read one file at a time.

7.3 Clipboard API for Copy/Paste Operations

Method Description Browser Support
navigator.clipboard.writeText(text) Writes text to clipboard. Returns Promise. Requires user gesture or permission. Modern Browsers
navigator.clipboard.readText() Reads text from clipboard. Returns Promise<string>. Requires permission. Modern Browsers
navigator.clipboard.write(items) Writes ClipboardItem array to clipboard. Supports images, rich content. Modern Browsers
navigator.clipboard.read() Reads ClipboardItem array from clipboard. Returns Promise<ClipboardItem[]>. Modern Browsers
Legacy Method Description Status
document.execCommand("copy") Copies selected text to clipboard. Deprecated but widely supported. DEPRECATED
document.execCommand("cut") Cuts selected text to clipboard. Deprecated but widely supported. DEPRECATED
document.execCommand("paste") Pastes from clipboard. Deprecated and security restricted. DEPRECATED

Example: Modern clipboard API

// Copy text to clipboard
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    console.log("Text copied to clipboard");
    return true;
  } catch (error) {
    console.error("Failed to copy:", error);
    return false;
  }
}

// Read text from clipboard
async function readFromClipboard() {
  try {
    const text = await navigator.clipboard.readText();
    console.log("Clipboard content:", text);
    return text;
  } catch (error) {
    console.error("Failed to read clipboard:", error);
    return null;
  }
}

// Copy button handler
document.querySelector(".copy-btn").addEventListener("click", async () => {
  const text = document.querySelector(".code-block").textContent;
  const success = await copyToClipboard(text);
  
  if (success) {
    // Show feedback
    showToast("Copied to clipboard!");
  }
});

// Paste handler
document.querySelector(".paste-btn").addEventListener("click", async () => {
  const text = await readFromClipboard();
  if (text) {
    document.querySelector("textarea").value = text;
  }
});

// Copy rich content (HTML, images)
async function copyRichContent() {
  const blob = new Blob(
    ["<h1>Hello</h1><p>Rich content</p>"],
    { "type": "text/html" }
  );
  
  const item = new ClipboardItem({
    "text/html": blob
  });
  
  try {
    await navigator.clipboard.write([item]);
    console.log("Rich content copied");
  } catch (error) {
    console.error("Failed to copy rich content:", error);
  }
}

// Copy image to clipboard
async function copyImageToClipboard(imageUrl) {
  try {
    const response = await fetch(imageUrl);
    const blob = await response.blob();
    
    const item = new ClipboardItem({
      [blob.type]: blob
    });
    
    await navigator.clipboard.write([item]);
    console.log("Image copied to clipboard");
  } catch (error) {
    console.error("Failed to copy image:", error);
  }
}

// Read clipboard with multiple formats
async function readClipboardData() {
  try {
    const items = await navigator.clipboard.read();
    
    for (const item of items) {
      console.log("Clipboard item types:", item.types);
      
      for (const type of item.types) {
        const blob = await item.getType(type);
        
        if (type.startsWith("text/")) {
          const text = await blob.text();
          console.log(`${type}:`, text);
        } else if (type.startsWith("image/")) {
          const url = URL.createObjectURL(blob);
          console.log(`${type}:`, url);
          
          const img = document.createElement("img");
          img.src = url;
          document.body.appendChild(img);
        }
      }
    }
  } catch (error) {
    console.error("Failed to read clipboard:", error);
  }
}

Example: Legacy execCommand fallback

// Copy with fallback
async function copyText(text) {
  // Try modern API
  if (navigator.clipboard && navigator.clipboard.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (error) {
      console.warn("Clipboard API failed, trying fallback");
    }
  }
  
  // Fallback to execCommand
  const textarea = document.createElement("textarea");
  textarea.value = text;
  textarea.style.position = "fixed";
  textarea.style.opacity = "0";
  document.body.appendChild(textarea);
  textarea.select();
  
  let success = false;
  try {
    success = document.execCommand("copy");
  } catch (error) {
    console.error("execCommand failed:", error);
  }
  
  document.body.removeChild(textarea);
  return success;
}

// Copy from element
function copyElementText(element) {
  const text = element.textContent || element.innerText;
  return copyText(text);
}

// Select and copy
function selectAndCopy(element) {
  const range = document.createRange();
  range.selectNodeContents(element);
  
  const selection = window.getSelection();
  selection.removeAllRanges();
  selection.addRange(range);
  
  const success = document.execCommand("copy");
  selection.removeAllRanges();
  
  return success;
}
Note: Clipboard API requires HTTPS (or localhost). writeText() requires user gesture on some browsers. readText() requires clipboard permission. ClipboardItem supports multiple MIME types. Always provide fallback for older browsers.
Warning: Reading clipboard requires user permission - prompt may appear. Don't spam clipboard writes - annoying UX. execCommand is deprecated but still needed for fallback. Never auto-read clipboard on page load - privacy violation.

7.4 Drag and Drop API with DataTransfer

Event Target When Fired Cancelable
dragstart Source element User starts dragging element Yes
drag Source element While dragging (fires continuously) Yes
dragend Source element Drag operation ends (drop or cancel) No
dragenter Drop target Dragged element enters drop target Yes
dragover Drop target Dragged element over drop target (fires continuously) Yes
dragleave Drop target Dragged element leaves drop target No
drop Drop target Element dropped on target. Must preventDefault() on dragover. Yes
DataTransfer Property Type Description
dropEffect string Visual feedback: "none", "copy", "move", "link". Set in dragover.
effectAllowed string Allowed operations: "none", "copy", "move", "link", "copyMove", "all", etc. Set in dragstart.
files FileList Files being dragged (from file system). Available in drop event.
items DataTransferItemList List of drag data items. More powerful than files.
types string[] Array of data format types available.
DataTransfer Method Description
setData(format, data) Sets drag data. Common formats: "text/plain", "text/html", "text/uri-list".
getData(format) Gets drag data. Only accessible in drop event.
clearData(format) Clears drag data. Optional format parameter.
setDragImage(element, x, y) Sets custom drag preview image. x, y are hotspot offsets.

Example: Basic drag and drop

// Make element draggable
const draggable = document.querySelector(".draggable");
draggable.draggable = true;

// Drag start
draggable.addEventListener("dragstart", (event) => {
  console.log("Drag started");
  
  // Set data
  event.dataTransfer.setData("text/plain", event.target.id);
  event.dataTransfer.setData("text/html", event.target.outerHTML);
  
  // Set effect
  event.dataTransfer.effectAllowed = "move";
  
  // Visual feedback
  event.target.style.opacity = "0.5";
});

// Drag end
draggable.addEventListener("dragend", (event) => {
  console.log("Drag ended");
  event.target.style.opacity = "1";
});

// Drop target
const dropZone = document.querySelector(".drop-zone");

// Prevent default to allow drop
dropZone.addEventListener("dragover", (event) => {
  event.preventDefault();
  event.dataTransfer.dropEffect = "move";
});

// Visual feedback
dropZone.addEventListener("dragenter", (event) => {
  event.preventDefault();
  dropZone.classList.add("drag-over");
});

dropZone.addEventListener("dragleave", () => {
  dropZone.classList.remove("drag-over");
});

// Handle drop
dropZone.addEventListener("drop", (event) => {
  event.preventDefault();
  dropZone.classList.remove("drag-over");
  
  // Get data
  const id = event.dataTransfer.getData("text/plain");
  console.log("Dropped element ID:", id);
  
  // Move element
  const element = document.getElementById(id);
  dropZone.appendChild(element);
});

Example: File drag and drop

const dropZone = document.getElementById("file-drop-zone");

// Prevent default browser behavior
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
  dropZone.addEventListener(eventName, (e) => {
    e.preventDefault();
    e.stopPropagation();
  });
});

// Visual feedback
["dragenter", "dragover"].forEach((eventName) => {
  dropZone.addEventListener(eventName, () => {
    dropZone.classList.add("highlight");
  });
});

["dragleave", "drop"].forEach((eventName) => {
  dropZone.addEventListener(eventName, () => {
    dropZone.classList.remove("highlight");
  });
});

// Handle dropped files
dropZone.addEventListener("drop", (event) => {
  const files = event.dataTransfer.files;
  console.log(`${files.length} file(s) dropped`);
  
  // Process files
  Array.from(files).forEach((file) => {
    console.log("File:", file.name, file.type, file.size);
    handleFile(file);
  });
  
  // Or use DataTransferItemList for more control
  const items = event.dataTransfer.items;
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    
    if (item.kind === "file") {
      const file = item.getAsFile();
      console.log("File from item:", file.name);
    }
  }
});

// Validate dropped files
function validateDrop(event) {
  const items = event.dataTransfer.items;
  
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    
    // Check if file
    if (item.kind !== "file") {
      return false;
    }
    
    // Check file type
    if (!item.type.startsWith("image/")) {
      alert("Only images allowed");
      return false;
    }
  }
  
  return true;
}

// Preview dropped images
async function handleFile(file) {
  if (!file.type.startsWith("image/")) return;
  
  const img = document.createElement("img");
  img.file = file;
  
  const reader = new FileReader();
  reader.onload = (e) => {
    img.src = e.target.result;
    img.style.maxWidth = "200px";
    dropZone.appendChild(img);
  };
  reader.readAsDataURL(file);
}
Note: Must call preventDefault() on dragover event to allow drop. getData() only works in drop event (security). Set draggable="true" attribute on elements to make draggable. Use event.dataTransfer.files for dropped files.
Warning: dragover fires very frequently - throttle expensive operations. Some browsers restrict file access from drag/drop for security. Always validate dropped files before processing. Mobile browsers have limited drag/drop support.

7.5 File System Access API for Local Files

Method Description Browser Support
window.showOpenFilePicker(options) Shows file picker. Returns Promise<FileSystemFileHandle[]>. Requires user gesture. Modern Browsers
window.showSaveFilePicker(options) Shows save dialog. Returns Promise<FileSystemFileHandle>. Requires user gesture. Modern Browsers
window.showDirectoryPicker(options) Shows directory picker. Returns Promise<FileSystemDirectoryHandle>. Requires user gesture. Modern Browsers
FileSystemFileHandle Method Description
getFile() Returns Promise<File> with file contents.
createWritable() Returns Promise<FileSystemWritableFileStream> for writing.
queryPermission(descriptor) Checks permission status. Returns "granted", "denied", or "prompt".
requestPermission(descriptor) Requests permission. Returns "granted" or "denied".

Example: Open and read file

// Open file picker
async function openFile() {
  try {
    const [fileHandle] = await window.showOpenFilePicker({
      "types": [
        {
          "description": "Text Files",
          "accept": {
            "text/plain": [".txt", ".md"],
            "text/html": [".html", ".htm"]
          }
        }
      ],
      "multiple": false
    });
    
    // Get file
    const file = await fileHandle.getFile();
    console.log("File:", file.name, file.size);
    
    // Read content
    const content = await file.text();
    console.log("Content:", content);
    
    // Store handle for later
    window.currentFileHandle = fileHandle;
    
    return content;
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("File picker cancelled");
    } else {
      console.error("Error opening file:", error);
    }
  }
}

// Open multiple files
async function openMultipleFiles() {
  try {
    const fileHandles = await window.showOpenFilePicker({
      "multiple": true,
      "types": [
        {
          "description": "Images",
          "accept": {
            "image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"]
          }
        }
      ]
    });
    
    for (const handle of fileHandles) {
      const file = await handle.getFile();
      console.log("Selected:", file.name);
    }
  } catch (error) {
    console.error("Error:", error);
  }
}

Example: Save file

// Save file
async function saveFile(content) {
  try {
    const handle = await window.showSaveFilePicker({
      "suggestedName": "document.txt",
      "types": [
        {
          "description": "Text Files",
          "accept": {
            "text/plain": [".txt"]
          }
        }
      ]
    });
    
    // Create writable stream
    const writable = await handle.createWritable();
    
    // Write content
    await writable.write(content);
    
    // Close file
    await writable.close();
    
    console.log("File saved");
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Save cancelled");
    } else {
      console.error("Error saving file:", error);
    }
  }
}

// Save with current handle (no picker)
async function saveToCurrentFile(content) {
  if (!window.currentFileHandle) {
    return saveFile(content);
  }
  
  try {
    const writable = await window.currentFileHandle.createWritable();
    await writable.write(content);
    await writable.close();
    console.log("File updated");
  } catch (error) {
    console.error("Error updating file:", error);
  }
}

// Write binary data
async function saveBinaryFile(blob) {
  try {
    const handle = await window.showSaveFilePicker({
      "suggestedName": "image.png",
      "types": [
        {
          "description": "PNG Image",
          "accept": { "image/png": [".png"] }
        }
      ]
    });
    
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (error) {
    console.error("Error saving image:", error);
  }
}
Note: File System Access API requires user gesture (click event). Only works in secure contexts (HTTPS). Browser shows permission prompt for each file access. File handles can be persisted in IndexedDB for later use. Very limited browser support (mainly Chrome).

7.6 Directory Handle and File Handle APIs

FileSystemDirectoryHandle Method Description
getFileHandle(name, options) Gets file in directory. Options: create: true to create if missing.
getDirectoryHandle(name, options) Gets subdirectory. Options: create: true to create if missing.
removeEntry(name, options) Removes file/directory. Options: recursive: true for directories.
values() Returns async iterator of entries in directory.
keys() Returns async iterator of entry names.
entries() Returns async iterator of [name, handle] pairs.

Example: Directory operations

// Pick directory
async function pickDirectory() {
  try {
    const dirHandle = await window.showDirectoryPicker();
    console.log("Directory:", dirHandle.name);
    
    // List contents
    await listDirectory(dirHandle);
    
    return dirHandle;
  } catch (error) {
    console.error("Error picking directory:", error);
  }
}

// List directory contents
async function listDirectory(dirHandle) {
  console.log(`Contents of ${dirHandle.name}:`);
  
  for await (const entry of dirHandle.values()) {
    console.log(`  ${entry.kind}: ${entry.name}`);
    
    if (entry.kind === "file") {
      const file = await entry.getFile();
      console.log(`    Size: ${file.size} bytes`);
    }
  }
}

// Read all files in directory
async function readAllFiles(dirHandle) {
  const files = [];
  
  for await (const entry of dirHandle.values()) {
    if (entry.kind === "file") {
      const file = await entry.getFile();
      const content = await file.text();
      files.push({ name: file.name, content });
    }
  }
  
  return files;
}

// Create file in directory
async function createFileInDirectory(dirHandle, fileName, content) {
  try {
    const fileHandle = await dirHandle.getFileHandle(fileName, {
      "create": true
    });
    
    const writable = await fileHandle.createWritable();
    await writable.write(content);
    await writable.close();
    
    console.log(`Created: ${fileName}`);
  } catch (error) {
    console.error("Error creating file:", error);
  }
}

// Create subdirectory
async function createSubdirectory(dirHandle, dirName) {
  try {
    const subDirHandle = await dirHandle.getDirectoryHandle(dirName, {
      "create": true
    });
    console.log(`Created directory: ${dirName}`);
    return subDirHandle;
  } catch (error) {
    console.error("Error creating directory:", error);
  }
}

// Delete file
async function deleteFile(dirHandle, fileName) {
  try {
    await dirHandle.removeEntry(fileName);
    console.log(`Deleted: ${fileName}`);
  } catch (error) {
    console.error("Error deleting file:", error);
  }
}

// Recursively list directory tree
async function listDirectoryTree(dirHandle, indent = "") {
  for await (const [name, handle] of dirHandle.entries()) {
    console.log(`${indent}${handle.kind === "directory" ? "📁" : "📄"} ${name}`);
    
    if (handle.kind === "directory") {
      await listDirectoryTree(handle, indent + "  ");
    }
  }
}

// Search for files
async function findFiles(dirHandle, pattern) {
  const results = [];
  
  for await (const entry of dirHandle.values()) {
    if (entry.kind === "file" && entry.name.includes(pattern)) {
      results.push(entry);
    } else if (entry.kind === "directory") {
      const subResults = await findFiles(entry, pattern);
      results.push(...subResults);
    }
  }
  
  return results;
}
Warning: File System Access API has very limited browser support (mainly Chrome/Edge). Always check if ("showOpenFilePicker" in window). Requires user permission for each directory access. Writing files requires explicit user consent. Not available in iframes or insecure contexts.

File and Clipboard API Best Practices

  • Validate file type and size before processing - check file.type and file.size
  • Use modern file.text() and file.arrayBuffer() instead of FileReader when possible
  • Use object URLs (URL.createObjectURL) instead of data URLs for better performance
  • Always revoke object URLs with URL.revokeObjectURL() when done to free memory
  • For clipboard, provide fallback to execCommand for older browser support
  • Request clipboard permission with clear explanation - users are wary of clipboard access
  • For drag and drop, always preventDefault() on dragover to enable drop
  • Validate dropped files immediately - check type, size, count before processing
  • File System Access API requires user gesture - call from click handler
  • Store file handles in IndexedDB to reuse without showing picker again
  • Always check browser support for File System Access API - very limited
  • Handle user cancellation gracefully - catch AbortError on picker APIs

8. Service Workers and Background APIs

8.1 Service Worker Registration and Lifecycle

Method Syntax Description Browser Support
register navigator.serviceWorker.register(scriptURL, options) Registers service worker. Returns Promise<ServiceWorkerRegistration>. Requires HTTPS. All Modern Browsers
getRegistration navigator.serviceWorker.getRegistration(scope) Gets existing registration for scope. Returns Promise<ServiceWorkerRegistration>. All Modern Browsers
getRegistrations navigator.serviceWorker.getRegistrations() Gets all registrations. Returns Promise<ServiceWorkerRegistration[]>. All Modern Browsers
Lifecycle Event When Fired Use Case
install Service worker first installed. Runs only once per version. Cache static assets, setup
activate Service worker activated. After install or on page reload for updated worker. Clean old caches, claim clients
fetch Network request intercepted. Fires for all page requests. Cache strategies, offline support
message Message received from client (page/worker). Client-worker communication
sync Background sync triggered (when online). Retry failed requests
push Push notification received. Show notifications
ServiceWorkerRegistration Property Type Description
installing ServiceWorker Service worker currently installing. null if none.
waiting ServiceWorker Service worker installed, waiting to activate. null if none.
active ServiceWorker Active service worker controlling pages. null if none.
scope string URL scope of service worker (e.g., "/app/").
updateViaCache string Cache mode for updates: "imports", "all", "none".

Example: Register service worker

// Check support
if ("serviceWorker" in navigator) {
  console.log("Service Workers supported");
} else {
  console.log("Service Workers not supported");
}

// Register service worker
async function registerServiceWorker() {
  try {
    const registration = await navigator.serviceWorker.register("/sw.js", {
      "scope": "/" // Default is script location
    });
    
    console.log("Service Worker registered:", registration.scope);
    
    // Check state
    if (registration.installing) {
      console.log("Service Worker installing");
    } else if (registration.waiting) {
      console.log("Service Worker waiting");
    } else if (registration.active) {
      console.log("Service Worker active");
    }
    
    return registration;
  } catch (error) {
    console.error("Service Worker registration failed:", error);
  }
}

// Register on page load
window.addEventListener("load", () => {
  registerServiceWorker();
});

// Listen for updates
navigator.serviceWorker.addEventListener("controllerchange", () => {
  console.log("Service Worker controller changed");
  // New service worker took control
  window.location.reload();
});

// Check for updates manually
async function checkForUpdates() {
  const registration = await navigator.serviceWorker.getRegistration();
  if (registration) {
    await registration.update();
    console.log("Checked for updates");
  }
}

// Unregister service worker
async function unregisterServiceWorker() {
  const registration = await navigator.serviceWorker.getRegistration();
  if (registration) {
    const success = await registration.unregister();
    console.log("Unregistered:", success);
  }
}

Example: Service worker lifecycle (sw.js)

const CACHE_NAME = "my-app-v1";
const STATIC_ASSETS = [
  "/",
  "/index.html",
  "/styles.css",
  "/app.js",
  "/logo.png"
];

// Install event - cache static assets
self.addEventListener("install", (event) => {
  console.log("Service Worker installing");
  
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("Caching static assets");
      return cache.addAll(STATIC_ASSETS);
    }).then(() => {
      // Skip waiting to activate immediately
      return self.skipWaiting();
    })
  );
});

// Activate event - clean old caches
self.addEventListener("activate", (event) => {
  console.log("Service Worker activating");
  
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => {
            console.log("Deleting old cache:", name);
            return caches.delete(name);
          })
      );
    }).then(() => {
      // Take control of all clients immediately
      return self.clients.claim();
    })
  );
});

// Fetch event - serve from cache or network
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Cache hit - return cached response
      if (response) {
        return response;
      }
      
      // Cache miss - fetch from network
      return fetch(event.request).then((response) => {
        // Don't cache non-GET requests or non-ok responses
        if (event.request.method !== "GET" || !response.ok) {
          return response;
        }
        
        // Clone response (can only read once)
        const responseToCache = response.clone();
        
        // Cache for next time
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, responseToCache);
        });
        
        return response;
      });
    }).catch(() => {
      // Network failed - return offline page
      return caches.match("/offline.html");
    })
  );
});
Note: Service Workers require HTTPS (localhost OK for development). Only one service worker active per scope. Use skipWaiting() to activate immediately, clients.claim() to control existing pages. Updates check every 24 hours or on navigation. Use versioned cache names to manage updates.
Warning: Service worker has no DOM access - can't manipulate page directly. Runs in separate thread. Use event.waitUntil() to extend event lifetime for async operations. Service worker can be terminated anytime - don't rely on global state. Test offline behavior thoroughly.

8.2 Service Worker Message Passing and Communication

Method From To Syntax
postMessage Page Service Worker navigator.serviceWorker.controller.postMessage(data)
postMessage Service Worker Page client.postMessage(data)
postMessage Service Worker All Pages clients.matchAll().then(clients => clients.forEach(c => c.postMessage(data)))
Clients Method Description Use Case
clients.matchAll(options) Gets all client windows/tabs. Options: includeUncontrolled, type. Broadcast to all tabs
clients.get(id) Gets specific client by ID. Returns Promise<Client>. Reply to specific tab
clients.openWindow(url) Opens new window/tab. Returns Promise<WindowClient>. Requires user interaction. Open notification click
clients.claim() Makes service worker control all clients immediately (without reload). Take control on activate

Example: Page to service worker communication

// From page to service worker
if (navigator.serviceWorker.controller) {
  // Send message
  navigator.serviceWorker.controller.postMessage({
    "type": "CACHE_URLS",
    "urls": ["/page1.html", "/page2.html"]
  });
  
  console.log("Message sent to service worker");
} else {
  console.log("No active service worker");
}

// Listen for messages from service worker
navigator.serviceWorker.addEventListener("message", (event) => {
  console.log("Message from service worker:", event.data);
  
  if (event.data.type === "CACHE_UPDATED") {
    console.log("Cache updated:", event.data.urls);
  } else if (event.data.type === "NEW_VERSION") {
    showUpdatePrompt();
  }
});

// Request-response pattern with message channel
function sendMessageWithResponse(message) {
  return new Promise((resolve, reject) => {
    const messageChannel = new MessageChannel();
    
    messageChannel.port1.onmessage = (event) => {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };
    
    navigator.serviceWorker.controller.postMessage(
      message,
      [messageChannel.port2]
    );
  });
}

// Usage
async function getCacheInfo() {
  try {
    const response = await sendMessageWithResponse({
      "type": "GET_CACHE_INFO"
    });
    console.log("Cache info:", response);
  } catch (error) {
    console.error("Error:", error);
  }
}

Example: Service worker to page communication

// In service worker (sw.js)

// Listen for messages from pages
self.addEventListener("message", (event) => {
  console.log("Message received:", event.data);
  
  if (event.data.type === "CACHE_URLS") {
    // Cache requested URLs
    cacheUrls(event.data.urls).then(() => {
      // Reply to sender
      event.source.postMessage({
        "type": "CACHE_UPDATED",
        "urls": event.data.urls
      });
    });
  } else if (event.data.type === "GET_CACHE_INFO") {
    // Handle request-response with message channel
    caches.keys().then((cacheNames) => {
      event.ports[0].postMessage({
        "caches": cacheNames,
        "count": cacheNames.length
      });
    });
  } else if (event.data.type === "SKIP_WAITING") {
    self.skipWaiting();
  }
});

// Broadcast to all clients
async function notifyAllClients(message) {
  const clients = await self.clients.matchAll({
    "includeUncontrolled": true,
    "type": "window"
  });
  
  clients.forEach((client) => {
    client.postMessage(message);
  });
}

// Notify when cache updated
async function cacheUrls(urls) {
  const cache = await caches.open(CACHE_NAME);
  await cache.addAll(urls);
  
  // Notify all tabs
  await notifyAllClients({
    "type": "CACHE_UPDATED",
    "urls": urls
  });
}

// Notify specific client
async function notifyClient(clientId, message) {
  const client = await self.clients.get(clientId);
  if (client) {
    client.postMessage(message);
  }
}

// Open window on notification click
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  
  event.waitUntil(
    clients.openWindow("/notifications")
  );
});
Note: postMessage only works if service worker is active and controlling page. Check navigator.serviceWorker.controller before sending. Messages are structured clones - can't send functions, DOM nodes. Use MessageChannel for request-response patterns.

8.3 Background Sync and Periodic Sync APIs

API Method Description Browser Support
Background Sync registration.sync.register(tag) Registers one-time sync when online. Returns Promise. Chrome, Edge
Periodic Sync registration.periodicSync.register(tag, options) Registers periodic sync. Options: minInterval in ms. Requires installed PWA. Limited (Chrome)
Get Tags registration.sync.getTags() Gets pending sync tags. Returns Promise<string[]>. Chrome, Edge
Unregister registration.periodicSync.unregister(tag) Unregisters periodic sync. Returns Promise. Limited (Chrome)

Example: Background Sync for retry

// In page - register background sync
async function sendMessage(message) {
  try {
    // Try sending immediately
    await fetch("/api/messages", {
      "method": "POST",
      "body": JSON.stringify(message)
    });
    console.log("Message sent");
  } catch (error) {
    // Failed - save to IndexedDB
    await saveMessageForLater(message);
    
    // Register sync to retry when online
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register("sync-messages");
    console.log("Background sync registered");
  }
}

async function saveMessageForLater(message) {
  const db = await openDatabase();
  const tx = db.transaction("pending", "readwrite");
  await tx.objectStore("pending").add(message);
}

// In service worker - handle sync event
self.addEventListener("sync", (event) => {
  console.log("Sync event:", event.tag);
  
  if (event.tag === "sync-messages") {
    event.waitUntil(syncMessages());
  }
});

async function syncMessages() {
  const db = await openDatabase();
  const messages = await db.getAll("pending");
  
  console.log(`Syncing ${messages.length} pending messages`);
  
  for (const message of messages) {
    try {
      await fetch("/api/messages", {
        "method": "POST",
        "body": JSON.stringify(message)
      });
      
      // Success - remove from pending
      await db.delete("pending", message.id);
      console.log("Message synced:", message.id);
    } catch (error) {
      // Still offline or error - will retry on next sync
      console.error("Sync failed:", error);
      throw error; // Retry sync later
    }
  }
  
  // Notify page
  await notifyAllClients({
    "type": "SYNC_COMPLETE",
    "count": messages.length
  });
}

// Check for pending syncs
async function checkPendingSyncs() {
  const registration = await navigator.serviceWorker.ready;
  const tags = await registration.sync.getTags();
  console.log("Pending syncs:", tags);
}

Example: Periodic Background Sync

// Register periodic sync (requires installed PWA)
async function registerPeriodicSync() {
  try {
    const registration = await navigator.serviceWorker.ready;
    
    // Check permission
    const status = await navigator.permissions.query({
      "name": "periodic-background-sync"
    });
    
    if (status.state === "granted") {
      // Register periodic sync (every 24 hours minimum)
      await registration.periodicSync.register("content-sync", {
        "minInterval": 24 * 60 * 60 * 1000 // 24 hours in ms
      });
      console.log("Periodic sync registered");
    }
  } catch (error) {
    console.error("Periodic sync failed:", error);
  }
}

// List periodic syncs
async function listPeriodicSyncs() {
  const registration = await navigator.serviceWorker.ready;
  const tags = await registration.periodicSync.getTags();
  console.log("Periodic syncs:", tags);
}

// Unregister periodic sync
async function unregisterPeriodicSync(tag) {
  const registration = await navigator.serviceWorker.ready;
  await registration.periodicSync.unregister(tag);
  console.log("Unregistered:", tag);
}

// In service worker - handle periodic sync
self.addEventListener("periodicsync", (event) => {
  console.log("Periodic sync event:", event.tag);
  
  if (event.tag === "content-sync") {
    event.waitUntil(syncContent());
  }
});

async function syncContent() {
  try {
    // Fetch fresh content
    const response = await fetch("/api/content");
    const data = await response.json();
    
    // Update cache
    const cache = await caches.open("content-cache");
    await cache.put("/api/content", new Response(JSON.stringify(data)));
    
    console.log("Content synced");
    
    // Notify clients
    await notifyAllClients({
      "type": "CONTENT_UPDATED",
      "timestamp": Date.now()
    });
  } catch (error) {
    console.error("Sync failed:", error);
  }
}
Note: Background Sync has limited browser support (Chrome, Edge). Periodic Sync requires installed PWA and permission. Browser controls actual sync timing - minInterval is minimum, not guaranteed. Sync events retry automatically if promise rejects.
Warning: Don't rely on Background Sync for critical operations - not supported everywhere. Periodic Sync can be delayed/skipped by browser to save battery. Always provide fallback (manual refresh button). Test offline scenarios thoroughly.

8.4 Push API and Push Notifications

Method Description Browser Support
registration.pushManager.subscribe(options) Subscribes to push notifications. Returns Promise<PushSubscription>. Requires permission. All Modern Browsers
registration.pushManager.getSubscription() Gets existing subscription. Returns Promise<PushSubscription | null>. All Modern Browsers
subscription.unsubscribe() Unsubscribes from push. Returns Promise<boolean>. All Modern Browsers
Notification.requestPermission() Requests notification permission. Returns Promise<"granted" | "denied" | "default">. All Browsers
PushSubscription Property Type Description
endpoint string Push service URL. Send push messages to this endpoint.
keys object Encryption keys: p256dh (public key), auth (authentication secret).
expirationTime number | null Subscription expiration timestamp. null if no expiration.

Example: Subscribe to push notifications

// Request notification permission
async function requestNotificationPermission() {
  const permission = await Notification.requestPermission();
  console.log("Notification permission:", permission);
  return permission === "granted";
}

// Subscribe to push
async function subscribeToPush() {
  try {
    // Check permission
    if (Notification.permission !== "granted") {
      const granted = await requestNotificationPermission();
      if (!granted) {
        console.log("Notification permission denied");
        return;
      }
    }
    
    // Get service worker registration
    const registration = await navigator.serviceWorker.ready;
    
    // Check existing subscription
    let subscription = await registration.pushManager.getSubscription();
    
    if (!subscription) {
      // Subscribe (need VAPID public key from server)
      subscription = await registration.pushManager.subscribe({
        "userVisibleOnly": true,
        "applicationServerKey": urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
      });
      
      console.log("Subscribed to push");
    } else {
      console.log("Already subscribed");
    }
    
    // Send subscription to server
    await sendSubscriptionToServer(subscription);
    
    return subscription;
  } catch (error) {
    console.error("Push subscription failed:", error);
  }
}

// Convert VAPID key
function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, "+")
    .replace(/_/g, "/");
  
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

// Send subscription to server
async function sendSubscriptionToServer(subscription) {
  const response = await fetch("/api/subscribe", {
    "method": "POST",
    "headers": { "Content-Type": "application/json" },
    "body": JSON.stringify(subscription)
  });
  
  if (response.ok) {
    console.log("Subscription saved on server");
  }
}

// Unsubscribe
async function unsubscribeFromPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();
  
  if (subscription) {
    await subscription.unsubscribe();
    console.log("Unsubscribed from push");
    
    // Notify server
    await fetch("/api/unsubscribe", {
      "method": "POST",
      "body": JSON.stringify({ "endpoint": subscription.endpoint })
    });
  }
}

Example: Handle push events in service worker

// In service worker - listen for push events
self.addEventListener("push", (event) => {
  console.log("Push received");
  
  let data = {
    "title": "New Notification",
    "body": "You have a new message",
    "icon": "/icon.png",
    "badge": "/badge.png"
  };
  
  // Parse push data
  if (event.data) {
    data = event.data.json();
  }
  
  // Show notification
  event.waitUntil(
    self.registration.showNotification(data.title, {
      "body": data.body,
      "icon": data.icon,
      "badge": data.badge,
      "data": data.url,
      "tag": data.tag || "default",
      "requireInteraction": false,
      "actions": [
        { "action": "open", "title": "Open" },
        { "action": "close", "title": "Close" }
      ]
    })
  );
});

// Handle notification click
self.addEventListener("notificationclick", (event) => {
  console.log("Notification clicked:", event.action);
  
  event.notification.close();
  
  if (event.action === "open") {
    // Open URL from notification data
    event.waitUntil(
      clients.openWindow(event.notification.data || "/")
    );
  } else if (event.action === "close") {
    // Just close (default behavior)
  } else {
    // Default click (no action button)
    event.waitUntil(
      clients.openWindow(event.notification.data || "/")
    );
  }
});

// Handle notification close
self.addEventListener("notificationclose", (event) => {
  console.log("Notification closed:", event.notification.tag);
  
  // Track analytics
  fetch("/api/analytics/notification-closed", {
    "method": "POST",
    "body": JSON.stringify({
      "tag": event.notification.tag,
      "timestamp": Date.now()
    })
  });
});
Note: Push API requires HTTPS and service worker. Need VAPID keys from server for applicationServerKey. Must show notification when push received (userVisibleOnly: true). Subscription can expire - check expirationTime and resubscribe.
Warning: Always request permission with user gesture (button click). Don't spam notifications - users will block. Test notification display on different platforms - varies by OS. Handle subscription expiration and errors gracefully. Safari has different push implementation (APNs).

8.5 Background Fetch for Large Downloads

Method Description Browser Support
registration.backgroundFetch.fetch(id, requests, options) Starts background fetch. Returns Promise<BackgroundFetchRegistration>. Limited (Chrome)
registration.backgroundFetch.get(id) Gets background fetch by ID. Returns Promise<BackgroundFetchRegistration>. Limited (Chrome)
registration.backgroundFetch.getIds() Gets all background fetch IDs. Returns Promise<string[]>. Limited (Chrome)

Example: Background fetch for offline downloads

// Start background fetch
async function downloadFiles(urls) {
  try {
    const registration = await navigator.serviceWorker.ready;
    
    const bgFetch = await registration.backgroundFetch.fetch(
      "download-videos",
      urls,
      {
        "title": "Downloading videos",
        "icons": [{ "src": "/icon.png", "sizes": "192x192" }],
        "downloadTotal": 50 * 1024 * 1024 // 50 MB estimate
      }
    );
    
    console.log("Background fetch started:", bgFetch.id);
    
    // Listen for progress
    bgFetch.addEventListener("progress", () => {
      const percent = (bgFetch.downloaded / bgFetch.downloadTotal) * 100;
      console.log(`Progress: ${percent.toFixed(2)}%`);
    });
    
    return bgFetch;
  } catch (error) {
    console.error("Background fetch failed:", error);
  }
}

// Check background fetch status
async function checkDownloadStatus(id) {
  const registration = await navigator.serviceWorker.ready;
  const bgFetch = await registration.backgroundFetch.get(id);
  
  if (bgFetch) {
    console.log(`Downloaded: ${bgFetch.downloaded} / ${bgFetch.downloadTotal}`);
    console.log(`Result: ${bgFetch.result}`); // "", "success", or "failure"
    console.log(`Failure reason: ${bgFetch.failureReason}`);
  }
}

// In service worker - handle background fetch events
self.addEventListener("backgroundfetchsuccess", (event) => {
  console.log("Background fetch succeeded:", event.registration.id);
  
  event.waitUntil(async function() {
    // Get downloaded files
    const records = await event.registration.matchAll();
    const files = await Promise.all(
      records.map(async (record) => {
        const response = await record.responseReady;
        const blob = await response.blob();
        return { "url": record.request.url, "blob": blob };
      })
    );
    
    // Cache downloaded files
    const cache = await caches.open("downloads");
    for (const file of files) {
      await cache.put(file.url, new Response(file.blob));
    }
    
    // Update UI badge
    await event.updateUI({ "title": "Download complete!" });
    
    // Show notification
    await self.registration.showNotification("Download Complete", {
      "body": `${files.length} file(s) downloaded`,
      "icon": "/icon.png"
    });
  }());
});

self.addEventListener("backgroundfetchfail", (event) => {
  console.log("Background fetch failed:", event.registration.id);
  
  event.waitUntil(
    self.registration.showNotification("Download Failed", {
      "body": "Please try again later",
      "icon": "/icon.png"
    })
  );
});

self.addEventListener("backgroundfetchabort", (event) => {
  console.log("Background fetch aborted:", event.registration.id);
});

self.addEventListener("backgroundfetchclick", (event) => {
  console.log("Background fetch UI clicked:", event.registration.id);
  
  event.waitUntil(
    clients.openWindow("/downloads")
  );
});
Note: Background Fetch has very limited support (Chrome only). Good for large file downloads that continue even if page closed. Browser shows download UI to user. Files available after download completes via backgroundfetchsuccess event.

8.6 Workbox Integration Patterns

Workbox Strategy Description Use Case
NetworkFirst Network request first, fallback to cache if offline. Dynamic content, API responses
CacheFirst Cache first, fallback to network if missing. Static assets, fonts, images
StaleWhileRevalidate Return cache immediately, update cache in background. Frequently updated content
NetworkOnly Always fetch from network, no caching. Analytics, real-time data
CacheOnly Only use cache, never network. Pre-cached app shell

Example: Workbox setup

// Import Workbox (in service worker)
importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js");

if (workbox) {
  console.log("Workbox loaded");
  
  // Precache static assets
  workbox.precaching.precacheAndRoute([
    { "url": "/", "revision": "v1" },
    { "url": "/styles.css", "revision": "v1" },
    { "url": "/app.js", "revision": "v1" }
  ]);
  
  // Cache images - CacheFirst
  workbox.routing.registerRoute(
    ({ request }) => request.destination === "image",
    new workbox.strategies.CacheFirst({
      "cacheName": "images",
      "plugins": [
        new workbox.expiration.ExpirationPlugin({
          "maxEntries": 60,
          "maxAgeSeconds": 30 * 24 * 60 * 60 // 30 days
        })
      ]
    })
  );
  
  // Cache CSS/JS - StaleWhileRevalidate
  workbox.routing.registerRoute(
    ({ request }) => 
      request.destination === "style" ||
      request.destination === "script",
    new workbox.strategies.StaleWhileRevalidate({
      "cacheName": "static-resources"
    })
  );
  
  // API calls - NetworkFirst
  workbox.routing.registerRoute(
    ({ url }) => url.pathname.startsWith("/api/"),
    new workbox.strategies.NetworkFirst({
      "cacheName": "api-cache",
      "plugins": [
        new workbox.expiration.ExpirationPlugin({
          "maxEntries": 50,
          "maxAgeSeconds": 5 * 60 // 5 minutes
        })
      ]
    })
  );
  
  // Google Fonts - CacheFirst
  workbox.routing.registerRoute(
    ({ url }) => url.origin === "https://fonts.googleapis.com",
    new workbox.strategies.StaleWhileRevalidate({
      "cacheName": "google-fonts-stylesheets"
    })
  );
  
  workbox.routing.registerRoute(
    ({ url }) => url.origin === "https://fonts.gstatic.com",
    new workbox.strategies.CacheFirst({
      "cacheName": "google-fonts-webfonts",
      "plugins": [
        new workbox.cacheableResponse.CacheableResponsePlugin({
          "statuses": [0, 200]
        }),
        new workbox.expiration.ExpirationPlugin({
          "maxAgeSeconds": 60 * 60 * 24 * 365, // 1 year
          "maxEntries": 30
        })
      ]
    })
  );
  
  // Offline fallback
  workbox.routing.setCatchHandler(({ event }) => {
    if (event.request.destination === "document") {
      return caches.match("/offline.html");
    }
    return Response.error();
  });
  
} else {
  console.log("Workbox failed to load");
}
Note: Workbox simplifies service worker development with built-in strategies and plugins. Use workbox-cli to generate precache manifest. Workbox handles common patterns: caching, routing, expiration, background sync. Consider bundle size when using Workbox - can use module imports instead of CDN.

Service Worker Best Practices

  • Always use versioned cache names - easier to manage updates and cleanup
  • Use skipWaiting() and clients.claim() for immediate activation
  • Implement proper fetch strategies - CacheFirst for static, NetworkFirst for dynamic
  • Always include offline fallback page in precache
  • Test service worker updates - old workers can persist and cause bugs
  • Use event.waitUntil() to extend event lifetime for async operations
  • Don't cache authenticated content unless intentional - privacy/security risk
  • Implement Background Sync for offline form submissions and retries
  • Request notification permission with clear explanation and user gesture
  • Handle push subscription expiration - resubscribe when needed
  • Use Workbox for production - handles edge cases and best practices
  • Monitor service worker errors with analytics - hard to debug in production

9. Performance and Timing APIs

9.1 Performance API and Navigation Timing

Property Type Description Browser Support
performance.now() number Returns high-resolution timestamp in ms since time origin. More accurate than Date.now(). All Browsers
performance.timeOrigin number Unix timestamp when performance measurement started. Use to convert to absolute time. All Modern Browsers
performance.timing PerformanceTiming Navigation timing info (deprecated, use Navigation Timing Level 2). Legacy support. All Browsers
performance.navigation PerformanceNavigation Navigation type and redirect count (deprecated). Use performance.getEntriesByType("navigation"). All Browsers
Navigation Timing Metric Description Calculation
DNS Lookup Time to resolve domain name. domainLookupEnd - domainLookupStart
TCP Connection Time to establish TCP connection. connectEnd - connectStart
TLS Negotiation Time for SSL/TLS handshake. connectEnd - secureConnectionStart
Request Time Time to send request. responseStart - requestStart
Response Time Time to receive response. responseEnd - responseStart
DOM Processing Time to parse and process DOM. domComplete - domLoading
DOM Interactive Time until DOM ready (DOMContentLoaded). domInteractive - navigationStart
DOM Complete Time until page fully loaded. domComplete - navigationStart
Page Load Time Total time from navigation to load complete. loadEventEnd - navigationStart

Example: Navigation timing metrics

// Get navigation timing (modern way)
const [navigation] = performance.getEntriesByType("navigation");

if (navigation) {
  console.log("Navigation type:", navigation.type); // "navigate", "reload", "back_forward", "prerender"
  console.log("Redirect count:", navigation.redirectCount);
  
  // Calculate metrics
  const metrics = {
    "dns": navigation.domainLookupEnd - navigation.domainLookupStart,
    "tcp": navigation.connectEnd - navigation.connectStart,
    "tls": navigation.secureConnectionStart ? 
           navigation.connectEnd - navigation.secureConnectionStart : 0,
    "ttfb": navigation.responseStart - navigation.requestStart, // Time to First Byte
    "responseTime": navigation.responseEnd - navigation.responseStart,
    "domParsing": navigation.domInteractive - navigation.responseEnd,
    "domComplete": navigation.domComplete - navigation.domLoading,
    "pageLoad": navigation.loadEventEnd - navigation.loadEventStart,
    "total": navigation.loadEventEnd - navigation.fetchStart
  };
  
  console.log("Performance Metrics (ms):");
  Object.entries(metrics).forEach(([key, value]) => {
    console.log(`  ${key}: ${value.toFixed(2)}ms`);
  });
}

// High-resolution timestamp
const start = performance.now();
// ... do work ...
const end = performance.now();
console.log(`Execution time: ${(end - start).toFixed(2)}ms`);

// Convert to absolute time
const absoluteTime = performance.timeOrigin + performance.now();
console.log("Absolute timestamp:", new Date(absoluteTime));

// Legacy navigation timing (deprecated but widely supported)
if (performance.timing) {
  const timing = performance.timing;
  const pageLoadTime = timing.loadEventEnd - timing.navigationStart;
  const domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
  
  console.log(`Page load: ${pageLoadTime}ms`);
  console.log(`DOM ready: ${domReadyTime}ms`);
}
Note: Use performance.now() instead of Date.now() for accurate timing - higher resolution and not affected by system clock changes. Navigation Timing Level 1 (performance.timing) is deprecated but still widely supported. Use getEntriesByType("navigation") for modern apps.

9.2 Resource Timing for Asset Loading Analysis

Method Description Returns
performance.getEntriesByType("resource") Gets all resource timing entries (CSS, JS, images, etc.). PerformanceResourceTiming[]
performance.getEntriesByName(name, "resource") Gets resource entries for specific URL. PerformanceResourceTiming[]
performance.clearResourceTimings() Clears resource timing buffer. void
performance.setResourceTimingBufferSize(n) Sets max number of resource entries (default 250). void
PerformanceResourceTiming Property Type Description
name string Resource URL.
initiatorType string How resource loaded: "script", "link", "img", "fetch", "xmlhttprequest", etc.
duration number Total load time in ms (responseEnd - startTime).
transferSize number Size of response including headers (bytes). 0 if cached.
encodedBodySize number Compressed size of response body (bytes).
decodedBodySize number Uncompressed size of response body (bytes).
nextHopProtocol string Network protocol: "http/1.1", "h2", "h3", etc.

Example: Analyze resource loading

// Get all resources
const resources = performance.getEntriesByType("resource");

console.log(`Total resources: ${resources.length}`);

// Group by type
const byType = resources.reduce((acc, resource) => {
  const type = resource.initiatorType;
  acc[type] = acc[type] || [];
  acc[type].push(resource);
  return acc;
}, {});

// Analyze each type
Object.entries(byType).forEach(([type, items]) => {
  const totalSize = items.reduce((sum, r) => sum + r.transferSize, 0);
  const totalTime = items.reduce((sum, r) => sum + r.duration, 0);
  
  console.log(`\n${type}:`);
  console.log(`  Count: ${items.length}`);
  console.log(`  Total size: ${(totalSize / 1024).toFixed(2)} KB`);
  console.log(`  Avg time: ${(totalTime / items.length).toFixed(2)}ms`);
});

// Find slow resources
const slowResources = resources
  .filter(r => r.duration > 1000) // > 1 second
  .sort((a, b) => b.duration - a.duration);

console.log("\nSlow resources (>1s):");
slowResources.forEach(r => {
  console.log(`  ${r.name}: ${r.duration.toFixed(2)}ms (${(r.transferSize / 1024).toFixed(2)} KB)`);
});

// Check cache hits
const cached = resources.filter(r => r.transferSize === 0);
console.log(`\nCached resources: ${cached.length} / ${resources.length}`);

// Check compression
resources.forEach(r => {
  if (r.encodedBodySize > 0 && r.decodedBodySize > 0) {
    const ratio = (1 - r.encodedBodySize / r.decodedBodySize) * 100;
    if (ratio > 50) {
      console.log(`Well compressed (${ratio.toFixed(0)}%): ${r.name}`);
    } else if (ratio < 10 && r.decodedBodySize > 10000) {
      console.log(`Poor compression (${ratio.toFixed(0)}%): ${r.name}`);
    }
  }
});

// Check HTTP/2
const http2Resources = resources.filter(r => r.nextHopProtocol === "h2");
console.log(`\nHTTP/2 resources: ${http2Resources.length} / ${resources.length}`);

// Timing breakdown for specific resource
const scriptUrl = "/app.js";
const [script] = performance.getEntriesByName(scriptUrl, "resource");

if (script) {
  console.log(`\nTiming for ${scriptUrl}:`);
  console.log(`  DNS: ${(script.domainLookupEnd - script.domainLookupStart).toFixed(2)}ms`);
  console.log(`  TCP: ${(script.connectEnd - script.connectStart).toFixed(2)}ms`);
  console.log(`  Request: ${(script.responseStart - script.requestStart).toFixed(2)}ms`);
  console.log(`  Response: ${(script.responseEnd - script.responseStart).toFixed(2)}ms`);
  console.log(`  Total: ${script.duration.toFixed(2)}ms`);
}
Note: transferSize is 0 for cached resources. For cross-origin resources, need Timing-Allow-Origin header to get detailed timing. Resource buffer has default limit of 250 entries - increase with setResourceTimingBufferSize() if needed.
Warning: Resource timing can consume memory with many resources. Clear periodically with clearResourceTimings() in SPAs. Cross-origin resources show limited timing without CORS headers. Don't rely on exact values - use for relative comparisons and trends.

9.3 User Timing API for Custom Metrics

Method Description Use Case
performance.mark(name, options) Creates named timestamp marker. Options: startTime, detail. Mark important events
performance.measure(name, options) Measures duration between marks/events. Options: start, end, duration. Measure custom metrics
performance.clearMarks(name) Clears specific or all marks. Omit name to clear all. Cleanup
performance.clearMeasures(name) Clears specific or all measures. Omit name to clear all. Cleanup
performance.getEntriesByType("mark") Gets all marks. Returns PerformanceMark[]. Retrieve marks
performance.getEntriesByType("measure") Gets all measures. Returns PerformanceMeasure[]. Retrieve measures

Example: User timing for custom metrics

// Mark events
performance.mark("page-start");

// Simulate work
await loadData();
performance.mark("data-loaded");

await renderUI();
performance.mark("ui-rendered");

await initializeFeatures();
performance.mark("features-ready");

// Measure durations
performance.measure("data-load-time", "page-start", "data-loaded");
performance.measure("ui-render-time", "data-loaded", "ui-rendered");
performance.measure("feature-init-time", "ui-rendered", "features-ready");
performance.measure("total-init-time", "page-start", "features-ready");

// Get measurements
const measures = performance.getEntriesByType("measure");

console.log("Custom Metrics:");
measures.forEach(measure => {
  console.log(`  ${measure.name}: ${measure.duration.toFixed(2)}ms`);
});

// Measure with navigation timing reference
performance.measure("time-to-interactive", {
  "start": 0, // Navigation start
  "end": "features-ready"
});

// Measure with explicit duration
performance.measure("render-budget", {
  "start": "data-loaded",
  "duration": 16.67 // 60fps budget
});

// Add custom data to marks
performance.mark("api-call", {
  "detail": {
    "endpoint": "/api/users",
    "method": "GET",
    "status": 200
  }
});

// Retrieve with detail
const marks = performance.getEntriesByName("api-call", "mark");
console.log("API call detail:", marks[0].detail);

// Clear old marks/measures
performance.clearMarks("page-start");
performance.clearMeasures("data-load-time");

// Clear all
performance.clearMarks();
performance.clearMeasures();

Example: Performance monitoring helper

class PerformanceMonitor {
  constructor() {
    this.metrics = new Map();
  }
  
  // Start timing
  start(name) {
    const markName = `${name}-start`;
    performance.mark(markName);
    this.metrics.set(name, { "startMark": markName });
  }
  
  // End timing and measure
  end(name) {
    const markName = `${name}-end`;
    performance.mark(markName);
    
    const metric = this.metrics.get(name);
    if (metric) {
      performance.measure(name, metric.startMark, markName);
      
      const [measure] = performance.getEntriesByName(name, "measure");
      metric.duration = measure.duration;
      
      console.log(`${name}: ${measure.duration.toFixed(2)}ms`);
      return measure.duration;
    }
  }
  
  // Get metric
  get(name) {
    return this.metrics.get(name);
  }
  
  // Get all metrics
  getAll() {
    return Object.fromEntries(this.metrics);
  }
  
  // Report to analytics
  report() {
    const measures = performance.getEntriesByType("measure");
    
    measures.forEach(measure => {
      // Send to analytics service
      sendAnalytics({
        "metric": measure.name,
        "value": measure.duration,
        "timestamp": Date.now()
      });
    });
  }
  
  // Clear all
  clear() {
    performance.clearMarks();
    performance.clearMeasures();
    this.metrics.clear();
  }
}

// Usage
const monitor = new PerformanceMonitor();

monitor.start("database-query");
const data = await fetchFromDatabase();
monitor.end("database-query");

monitor.start("render-component");
renderComponent(data);
monitor.end("render-component");

console.log("All metrics:", monitor.getAll());
monitor.report();
Note: User Timing API is perfect for custom metrics specific to your app. Integrates with browser DevTools performance timeline. Use descriptive names for marks and measures. Can add custom data with detail option.

9.4 Paint Timing and Largest Contentful Paint

Paint Metric Description Good Value
First Paint (FP) Time when browser first renders anything (background, border, etc.). < 1s
First Contentful Paint (FCP) Time when first text/image renders. Core Web Vital. < 1.8s
Largest Contentful Paint (LCP) Time when largest content element renders. Core Web Vital. < 2.5s
First Input Delay (FID) Time from first interaction to browser response. Core Web Vital. < 100ms
Cumulative Layout Shift (CLS) Visual stability - sum of unexpected layout shifts. Core Web Vital. < 0.1

Example: Measure paint timing

// Get paint timing
const paintEntries = performance.getEntriesByType("paint");

paintEntries.forEach(entry => {
  console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
});

// First Paint and First Contentful Paint
const fp = paintEntries.find(e => e.name === "first-paint");
const fcp = paintEntries.find(e => e.name === "first-contentful-paint");

if (fp) console.log(`FP: ${fp.startTime.toFixed(2)}ms`);
if (fcp) console.log(`FCP: ${fcp.startTime.toFixed(2)}ms`);

// Largest Contentful Paint (LCP)
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  
  console.log(`LCP: ${lastEntry.startTime.toFixed(2)}ms`);
  console.log("LCP element:", lastEntry.element);
  console.log("LCP size:", lastEntry.size);
  console.log("LCP URL:", lastEntry.url);
});

observer.observe({ "type": "largest-contentful-paint", "buffered": true });

// First Input Delay (FID)
const fidObserver = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  entries.forEach(entry => {
    const fid = entry.processingStart - entry.startTime;
    console.log(`FID: ${fid.toFixed(2)}ms`);
    console.log("Input type:", entry.name);
  });
});

fidObserver.observe({ "type": "first-input", "buffered": true });

// Cumulative Layout Shift (CLS)
let clsScore = 0;

const clsObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Only count layout shifts without recent user input
    if (!entry.hadRecentInput) {
      clsScore += entry.value;
      console.log(`Layout shift: ${entry.value.toFixed(4)}`);
      console.log("Shifted elements:", entry.sources);
    }
  }
  
  console.log(`Total CLS: ${clsScore.toFixed(4)}`);
});

clsObserver.observe({ "type": "layout-shift", "buffered": true });

Example: Web Vitals monitoring

class WebVitalsMonitor {
  constructor() {
    this.vitals = {};
    this.observers = [];
  }
  
  // Measure LCP
  measureLCP(callback) {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      
      this.vitals.lcp = {
        "value": lastEntry.startTime,
        "rating": this.getRating(lastEntry.startTime, [2500, 4000]),
        "element": lastEntry.element,
        "url": lastEntry.url
      };
      
      callback && callback(this.vitals.lcp);
    });
    
    observer.observe({ "type": "largest-contentful-paint", "buffered": true });
    this.observers.push(observer);
  }
  
  // Measure FID
  measureFID(callback) {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => {
        const fid = entry.processingStart - entry.startTime;
        
        this.vitals.fid = {
          "value": fid,
          "rating": this.getRating(fid, [100, 300]),
          "eventType": entry.name
        };
        
        callback && callback(this.vitals.fid);
      });
    });
    
    observer.observe({ "type": "first-input", "buffered": true });
    this.observers.push(observer);
  }
  
  // Measure CLS
  measureCLS(callback) {
    let clsScore = 0;
    
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
        }
      }
      
      this.vitals.cls = {
        "value": clsScore,
        "rating": this.getRating(clsScore, [0.1, 0.25])
      };
      
      callback && callback(this.vitals.cls);
    });
    
    observer.observe({ "type": "layout-shift", "buffered": true });
    this.observers.push(observer);
  }
  
  // Get rating (good, needs-improvement, poor)
  getRating(value, [good, poor]) {
    if (value <= good) return "good";
    if (value <= poor) return "needs-improvement";
    return "poor";
  }
  
  // Report all vitals
  report() {
    console.log("Web Vitals:", this.vitals);
    
    // Send to analytics
    Object.entries(this.vitals).forEach(([name, data]) => {
      sendAnalytics({
        "metric": name.toUpperCase(),
        "value": data.value,
        "rating": data.rating
      });
    });
  }
  
  // Disconnect all observers
  disconnect() {
    this.observers.forEach(o => o.disconnect());
  }
}

// Usage
const vitalsMonitor = new WebVitalsMonitor();

vitalsMonitor.measureLCP((lcp) => {
  console.log(`LCP: ${lcp.value.toFixed(2)}ms (${lcp.rating})`);
});

vitalsMonitor.measureFID((fid) => {
  console.log(`FID: ${fid.value.toFixed(2)}ms (${fid.rating})`);
});

vitalsMonitor.measureCLS((cls) => {
  console.log(`CLS: ${cls.value.toFixed(4)} (${cls.rating})`);
});

// Report on page unload
window.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    vitalsMonitor.report();
  }
});
Note: Core Web Vitals (LCP, FID, CLS) are Google ranking factors. Use PerformanceObserver with buffered: true to get historical entries. LCP can change as page loads - track latest value. Consider using web-vitals library for production.
Warning: Paint timing varies greatly by device and network. Don't compare absolute values across users - track trends. LCP element must be visible in viewport. CLS excludes shifts within 500ms of user input. Test on real devices, not just desktop.

9.5 Long Tasks API for Performance Monitoring

Property Description Browser Support
Long Task Task that blocks main thread for >50ms. Causes jank and poor responsiveness. Chrome, Edge
entry.duration Task duration in ms. Long tasks are >50ms. Chrome, Edge
entry.attribution Array of task attribution (container, script URL, etc.). Chrome, Edge

Example: Monitor long tasks

// Observe long tasks (>50ms)
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  
  entries.forEach(entry => {
    console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`);
    console.log("Start time:", entry.startTime);
    console.log("Attribution:", entry.attribution);
    
    // Identify source
    if (entry.attribution && entry.attribution.length > 0) {
      const attribution = entry.attribution[0];
      console.log("Container:", attribution.containerType);
      console.log("Name:", attribution.containerName);
      console.log("Source:", attribution.containerSrc);
    }
    
    // Track for analytics
    sendAnalytics({
      "event": "long-task",
      "duration": entry.duration,
      "timestamp": entry.startTime
    });
  });
});

// Check support
if (PerformanceObserver.supportedEntryTypes.includes("longtask")) {
  observer.observe({ "type": "longtask", "buffered": true });
} else {
  console.log("Long Tasks API not supported");
}

// Track total long task time
let totalBlockingTime = 0;

const tbtObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Only count blocking portion (>50ms)
    const blockingTime = Math.max(0, entry.duration - 50);
    totalBlockingTime += blockingTime;
  }
  
  console.log(`Total Blocking Time: ${totalBlockingTime.toFixed(2)}ms`);
});

if (PerformanceObserver.supportedEntryTypes.includes("longtask")) {
  tbtObserver.observe({ "type": "longtask", "buffered": true });
}
Note: Long Tasks API has limited browser support (Chrome, Edge). Long tasks (>50ms) cause poor interactivity. Total Blocking Time (TBT) is sum of blocking portions of all long tasks. Use to identify performance bottlenecks.

9.6 Performance Observer for Metrics Collection

Method Description
new PerformanceObserver(callback) Creates observer. Callback receives PerformanceObserverEntryList.
observer.observe(options) Start observing. Options: type, entryTypes, buffered.
observer.disconnect() Stop observing and clear buffer.
observer.takeRecords() Returns and clears buffered entries.
PerformanceObserver.supportedEntryTypes Static property - array of supported entry types in browser.
Entry Type Description Browser Support
navigation Navigation timing. All Modern
resource Resource timing. All Modern
mark User timing marks. All Modern
measure User timing measures. All Modern
paint Paint timing (FP, FCP). All Modern
largest-contentful-paint LCP timing. Chrome, Edge
first-input FID timing. Chrome, Edge
layout-shift CLS events. Chrome, Edge
longtask Long tasks (>50ms). Chrome, Edge
element Element timing. Limited

Example: Performance observer patterns

// Observe single type
const paintObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
  }
});

paintObserver.observe({ "type": "paint", "buffered": true });

// Observe multiple types (legacy way)
const multiObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`[${entry.entryType}] ${entry.name}: ${entry.duration || entry.startTime}ms`);
  }
});

multiObserver.observe({ 
  "entryTypes": ["mark", "measure", "resource"] 
});

// Check supported types
console.log("Supported types:", PerformanceObserver.supportedEntryTypes);

// Feature detection
function observeIfSupported(type, callback) {
  if (PerformanceObserver.supportedEntryTypes.includes(type)) {
    const observer = new PerformanceObserver(callback);
    observer.observe({ "type": type, "buffered": true });
    return observer;
  } else {
    console.warn(`${type} not supported`);
    return null;
  }
}

// Usage
const lcpObserver = observeIfSupported("largest-contentful-paint", (list) => {
  const entries = list.getEntries();
  const lcp = entries[entries.length - 1];
  console.log(`LCP: ${lcp.startTime.toFixed(2)}ms`);
});

// Buffered option gets historical entries
const resourceObserver = new PerformanceObserver((list) => {
  console.log(`New resources loaded: ${list.getEntries().length}`);
});

// buffered: true includes entries before observe() called
resourceObserver.observe({ "type": "resource", "buffered": true });

// Disconnect observer
setTimeout(() => {
  resourceObserver.disconnect();
  console.log("Observer disconnected");
}, 10000);

// Take records manually
const manualObserver = new PerformanceObserver(() => {});
manualObserver.observe({ "type": "mark" });

performance.mark("test-1");
performance.mark("test-2");

const records = manualObserver.takeRecords();
console.log("Manual records:", records);
manualObserver.disconnect();

Example: Comprehensive performance monitoring

class PerformanceTracker {
  constructor() {
    this.observers = [];
    this.metrics = {};
  }
  
  // Initialize all observers
  init() {
    this.observeNavigation();
    this.observeResources();
    this.observePaint();
    this.observeWebVitals();
    this.observeLongTasks();
  }
  
  observeNavigation() {
    this.observe("navigation", (list) => {
      const [nav] = list.getEntries();
      this.metrics.navigation = {
        "type": nav.type,
        "redirectCount": nav.redirectCount,
        "dns": nav.domainLookupEnd - nav.domainLookupStart,
        "tcp": nav.connectEnd - nav.connectStart,
        "ttfb": nav.responseStart - nav.requestStart,
        "domInteractive": nav.domInteractive,
        "domComplete": nav.domComplete,
        "loadComplete": nav.loadEventEnd
      };
    });
  }
  
  observeResources() {
    this.observe("resource", (list) => {
      const resources = list.getEntries();
      
      if (!this.metrics.resources) {
        this.metrics.resources = { "count": 0, "totalSize": 0, "byType": {} };
      }
      
      resources.forEach(r => {
        this.metrics.resources.count++;
        this.metrics.resources.totalSize += r.transferSize;
        
        const type = r.initiatorType;
        if (!this.metrics.resources.byType[type]) {
          this.metrics.resources.byType[type] = { "count": 0, "size": 0 };
        }
        this.metrics.resources.byType[type].count++;
        this.metrics.resources.byType[type].size += r.transferSize;
      });
    });
  }
  
  observePaint() {
    this.observe("paint", (list) => {
      for (const entry of list.getEntries()) {
        this.metrics[entry.name] = entry.startTime;
      }
    });
  }
  
  observeWebVitals() {
    // LCP
    this.observe("largest-contentful-paint", (list) => {
      const entries = list.getEntries();
      const lcp = entries[entries.length - 1];
      this.metrics.lcp = lcp.startTime;
    });
    
    // FID
    this.observe("first-input", (list) => {
      const [entry] = list.getEntries();
      this.metrics.fid = entry.processingStart - entry.startTime;
    });
    
    // CLS
    let clsScore = 0;
    this.observe("layout-shift", (list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
        }
      }
      this.metrics.cls = clsScore;
    });
  }
  
  observeLongTasks() {
    this.observe("longtask", (list) => {
      if (!this.metrics.longTasks) {
        this.metrics.longTasks = { "count": 0, "totalTime": 0 };
      }
      
      for (const entry of list.getEntries()) {
        this.metrics.longTasks.count++;
        this.metrics.longTasks.totalTime += entry.duration;
      }
    });
  }
  
  // Helper to observe with feature detection
  observe(type, callback) {
    if (PerformanceObserver.supportedEntryTypes.includes(type)) {
      const observer = new PerformanceObserver(callback);
      observer.observe({ "type": type, "buffered": true });
      this.observers.push(observer);
    }
  }
  
  // Get all metrics
  getMetrics() {
    return this.metrics;
  }
  
  // Report to analytics
  report() {
    console.log("Performance Metrics:", this.metrics);
    
    // Send to analytics service
    sendAnalytics({
      "event": "performance-report",
      "metrics": this.metrics,
      "timestamp": Date.now()
    });
  }
  
  // Cleanup
  disconnect() {
    this.observers.forEach(o => o.disconnect());
  }
}

// Initialize tracker
const tracker = new PerformanceTracker();
tracker.init();

// Report on visibility change
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    tracker.report();
  }
});

// Report on page unload
window.addEventListener("pagehide", () => {
  tracker.report();
});
Note: Always use feature detection with PerformanceObserver.supportedEntryTypes. Use buffered: true to get entries that occurred before observe() called. Modern approach uses type (single type), legacy uses entryTypes (array).
Warning: Performance observers can impact performance if callback is expensive. Keep callbacks lightweight. Disconnect observers when done to prevent memory leaks. Don't observe in tight loops. Use debouncing for resource observer in SPAs.

Performance API Best Practices

  • Use performance.now() for accurate timing - higher resolution than Date.now()
  • Monitor Core Web Vitals (LCP, FID, CLS) - Google ranking factors
  • Use PerformanceObserver with buffered: true to capture early events
  • Always check PerformanceObserver.supportedEntryTypes before observing
  • User Timing API perfect for custom metrics - use descriptive mark/measure names
  • Track Resource Timing to identify slow or large assets
  • Monitor long tasks (>50ms) to identify responsiveness issues
  • Send performance data to analytics - track trends over time, not absolute values
  • Clear marks/measures periodically in SPAs to prevent memory growth
  • Report metrics on page hide/unload - use visibilitychange event
  • Test performance on real devices and networks - desktop not representative
  • Consider using web-vitals library for production Web Vitals tracking

10. Intersection and Resize Observer APIs

10.1 Intersection Observer for Visibility Detection

Method/Property Description Browser Support
new IntersectionObserver(callback, options) Creates intersection observer. Callback receives IntersectionObserverEntry[]. All Modern Browsers
observer.observe(element) Start observing element for intersection changes. All Modern Browsers
observer.unobserve(element) Stop observing specific element. All Modern Browsers
observer.disconnect() Stop observing all elements. All Modern Browsers
observer.takeRecords() Returns array of queued entries and clears queue. All Modern Browsers
IntersectionObserverEntry Property Type Description
target Element The observed element.
isIntersecting boolean true if element is intersecting root/viewport.
intersectionRatio number Ratio of intersection (0.0 to 1.0). 1.0 = fully visible.
intersectionRect DOMRectReadOnly Bounding rect of visible portion of element.
boundingClientRect DOMRectReadOnly Bounding rect of entire element.
rootBounds DOMRectReadOnly Bounding rect of root element (or viewport if root is null).
time number Timestamp when intersection change occurred.

Example: Basic intersection observer

// Create intersection observer
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    console.log("Element:", entry.target);
    console.log("Is intersecting:", entry.isIntersecting);
    console.log("Intersection ratio:", entry.intersectionRatio);
    
    if (entry.isIntersecting) {
      // Element entered viewport
      entry.target.classList.add("visible");
      console.log("Element became visible");
    } else {
      // Element left viewport
      entry.target.classList.remove("visible");
      console.log("Element became hidden");
    }
  });
});

// Observe elements
const elements = document.querySelectorAll(".observe-me");
elements.forEach(el => observer.observe(el));

// Unobserve specific element
observer.unobserve(elements[0]);

// Disconnect all
observer.disconnect();

// One-time visibility check
const oneTimeObserver = new IntersectionObserver((entries, obs) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Do something once
      console.log("Element appeared!");
      
      // Stop observing after first intersection
      obs.unobserve(entry.target);
    }
  });
});

const heroElement = document.querySelector(".hero");
oneTimeObserver.observe(heroElement);

Example: Trigger animations on visibility

// Animate elements when they enter viewport
const animateObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Add animation class
      entry.target.classList.add("animate-in");
      
      // Optional: stop observing after animation
      animateObserver.unobserve(entry.target);
    }
  });
});

// Observe all elements with data-animate attribute
document.querySelectorAll("[data-animate]").forEach(el => {
  animateObserver.observe(el);
});

// Track visibility percentage
const percentageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const percent = Math.round(entry.intersectionRatio * 100);
    entry.target.textContent = `Visible: ${percent}%`;
    
    // Trigger at different thresholds
    if (percent >= 75) {
      entry.target.style.background = "green";
    } else if (percent >= 50) {
      entry.target.style.background = "yellow";
    } else if (percent > 0) {
      entry.target.style.background = "red";
    } else {
      entry.target.style.background = "gray";
    }
  });
}, { "threshold": [0, 0.25, 0.5, 0.75, 1.0] });

const trackedElement = document.querySelector(".tracked");
percentageObserver.observe(trackedElement);
Note: Intersection Observer is asynchronous and efficient - much better than scroll events. Callback fires when intersection changes, not continuously. Use isIntersecting for simple visibility checks, intersectionRatio for percentage visible.

10.2 Intersection Observer Root Margin and Thresholds

Option Type Description Default
root Element | null Element to use as viewport. null = browser viewport. null
rootMargin string Margin around root (CSS syntax). Can be negative. Example: "50px 0px -50px 0px". "0px"
threshold number | number[] Intersection ratio(s) to trigger callback. 0.0 to 1.0. Can be array for multiple thresholds. 0
Threshold Value Meaning Use Case
0 Callback fires when any pixel is visible. Detect element entering/leaving viewport
0.5 Callback fires when 50% of element is visible. Trigger when element half-visible
1.0 Callback fires when 100% of element is visible. Trigger when element fully-visible
[0, 0.25, 0.5, 0.75, 1.0] Callback fires at 0%, 25%, 50%, 75%, 100% visibility. Track visibility percentage changes

Example: Root margin and thresholds

// Preload images before they enter viewport (positive rootMargin)
const preloadObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      // Load image 200px before it enters viewport
      if (img.dataset.src) {
        img.src = img.dataset.src;
        img.removeAttribute("data-src");
      }
      preloadObserver.unobserve(img);
    }
  });
}, {
  "rootMargin": "200px" // Expand viewport by 200px on all sides
});

document.querySelectorAll("img[data-src]").forEach(img => {
  preloadObserver.observe(img);
});

// Trigger only when element fully visible
const fullyVisibleObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.intersectionRatio === 1.0) {
      console.log("Element 100% visible:", entry.target);
      entry.target.classList.add("fully-visible");
    } else {
      entry.target.classList.remove("fully-visible");
    }
  });
}, {
  "threshold": 1.0
});

// Negative rootMargin to shrink viewport
const stickyHeaderObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    // Trigger 80px before element reaches top
    if (!entry.isIntersecting) {
      document.body.classList.add("sticky-header");
    } else {
      document.body.classList.remove("sticky-header");
    }
  });
}, {
  "rootMargin": "-80px 0px 0px 0px" // Shrink top by 80px
});

const sentinelElement = document.querySelector(".sentinel");
stickyHeaderObserver.observe(sentinelElement);

// Multiple thresholds for progress tracking
const progressObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const percent = Math.round(entry.intersectionRatio * 100);
    console.log(`Element ${percent}% visible`);
    
    // Update progress bar
    entry.target.querySelector(".progress").style.width = `${percent}%`;
  });
}, {
  "threshold": Array.from({ "length": 101 }, (_, i) => i / 100) // 0, 0.01, 0.02, ..., 1.0
});

// Custom root container
const scrollContainer = document.querySelector(".scroll-container");
const containerObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    console.log("Visible in container:", entry.isIntersecting);
  });
}, {
  "root": scrollContainer, // Use custom container instead of viewport
  "threshold": 0.5
});

const containerItem = scrollContainer.querySelector(".item");
containerObserver.observe(containerItem);
Note: rootMargin uses CSS margin syntax - can be 1, 2, or 4 values. Positive values expand viewport (preload), negative shrink (trigger earlier). threshold can be single number or array. More thresholds = more callback invocations.
Warning: Don't use too many thresholds (e.g., 100+ values) - impacts performance. threshold: 1.0 may never trigger if element larger than viewport. Use rootMargin carefully with negative values - can cause immediate firing.

10.3 Resize Observer for Element Size Changes

Method/Property Description Browser Support
new ResizeObserver(callback) Creates resize observer. Callback receives ResizeObserverEntry[]. All Modern Browsers
observer.observe(element, options) Start observing element size. Options: box ("content-box", "border-box", "device-pixel-content-box"). All Modern Browsers
observer.unobserve(element) Stop observing specific element. All Modern Browsers
observer.disconnect() Stop observing all elements. All Modern Browsers
ResizeObserverEntry Property Type Description
target Element The observed element.
contentRect DOMRectReadOnly Content box dimensions (excludes padding, border, scrollbar).
borderBoxSize ResizeObserverSize[] Border box dimensions (includes padding and border). Array for multi-column.
contentBoxSize ResizeObserverSize[] Content box dimensions. Array for multi-column.
devicePixelContentBoxSize ResizeObserverSize[] Content box in device pixels (for high-DPI displays).
ResizeObserverSize Property Type Description
inlineSize number Inline size (width in horizontal writing mode).
blockSize number Block size (height in horizontal writing mode).

Example: Basic resize observer

// Observe element size changes
const resizeObserver = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    // Legacy way (deprecated but widely supported)
    const { "width": width, "height": height } = entry.contentRect;
    console.log(`Content size: ${width}x${height}`);
    
    // Modern way (preferred)
    if (entry.contentBoxSize) {
      // contentBoxSize is array (for multi-column)
      const contentBoxSize = entry.contentBoxSize[0];
      console.log(`Inline size: ${contentBoxSize.inlineSize}px`);
      console.log(`Block size: ${contentBoxSize.blockSize}px`);
    }
    
    // Border box size (includes padding and border)
    if (entry.borderBoxSize) {
      const borderBoxSize = entry.borderBoxSize[0];
      console.log(`Border box: ${borderBoxSize.inlineSize}x${borderBoxSize.blockSize}`);
    }
    
    // React to size changes
    const element = entry.target;
    if (contentBoxSize.inlineSize < 400) {
      element.classList.add("small");
      element.classList.remove("large");
    } else {
      element.classList.add("large");
      element.classList.remove("small");
    }
  });
});

// Observe element
const box = document.querySelector(".resizable-box");
resizeObserver.observe(box);

// Observe with specific box model
resizeObserver.observe(box, { "box": "border-box" });

// Stop observing
resizeObserver.unobserve(box);
resizeObserver.disconnect();

Example: Responsive component behavior

// Container queries with ResizeObserver
class ResponsiveComponent {
  constructor(element) {
    this.element = element;
    this.observer = new ResizeObserver(this.handleResize.bind(this));
    this.observer.observe(this.element);
  }
  
  handleResize(entries) {
    const entry = entries[0];
    const width = entry.contentBoxSize[0].inlineSize;
    
    // Remove all size classes
    this.element.classList.remove("size-sm", "size-md", "size-lg", "size-xl");
    
    // Add appropriate class based on width
    if (width < 400) {
      this.element.classList.add("size-sm");
    } else if (width < 768) {
      this.element.classList.add("size-md");
    } else if (width < 1024) {
      this.element.classList.add("size-lg");
    } else {
      this.element.classList.add("size-xl");
    }
    
    // Dispatch custom event
    this.element.dispatchEvent(new CustomEvent("sizechange", {
      "detail": { "width": width, "breakpoint": this.getBreakpoint(width) }
    }));
  }
  
  getBreakpoint(width) {
    if (width < 400) return "sm";
    if (width < 768) return "md";
    if (width < 1024) return "lg";
    return "xl";
  }
  
  disconnect() {
    this.observer.disconnect();
  }
}

// Usage
const component = new ResponsiveComponent(document.querySelector(".component"));

component.element.addEventListener("sizechange", (e) => {
  console.log(`Breakpoint: ${e.detail.breakpoint}, Width: ${e.detail.width}px`);
});

// Sync chart size with container
const chartContainer = document.querySelector(".chart-container");
const chartObserver = new ResizeObserver((entries) => {
  const entry = entries[0];
  const { "inlineSize": width, "blockSize": height } = entry.contentBoxSize[0];
  
  // Resize chart to match container
  myChart.resize(width, height);
});

chartObserver.observe(chartContainer);
Note: Resize Observer is much more efficient than window resize events or polling. Fires when element size changes due to any reason (CSS, content, viewport). Use contentBoxSize array (modern) instead of contentRect (legacy).
Warning: Avoid infinite loops - don't resize observed element inside callback (can cause circular updates). ResizeObserver fires frequently - debounce expensive operations. Disconnect observers when component unmounts to prevent memory leaks.

10.4 Observer Patterns and Performance Best Practices

Best Practice Why Implementation
Single observer instance More efficient than multiple observers for same purpose. Reuse one observer for all similar elements.
Unobserve after use Reduces overhead for one-time observations. Call unobserve() after first trigger.
Disconnect on cleanup Prevents memory leaks in SPAs. Call disconnect() when component unmounts.
Lightweight callbacks Observers fire frequently - expensive callbacks cause jank. Keep callback logic minimal, defer heavy work.
Debounce expensive operations ResizeObserver can fire many times per second. Use debounce/throttle for expensive updates.

Example: Observer management patterns

// Singleton pattern - reuse observer across app
class ObserverManager {
  constructor() {
    this.intersectionObserver = null;
    this.resizeObserver = null;
    this.observedElements = new Map();
  }
  
  // Get or create intersection observer
  getIntersectionObserver(options = {}) {
    if (!this.intersectionObserver) {
      this.intersectionObserver = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          const callback = this.observedElements.get(entry.target);
          if (callback) {
            callback(entry);
          }
        });
      }, options);
    }
    return this.intersectionObserver;
  }
  
  // Observe with custom callback
  observeIntersection(element, callback) {
    const observer = this.getIntersectionObserver();
    this.observedElements.set(element, callback);
    observer.observe(element);
  }
  
  // Unobserve
  unobserveIntersection(element) {
    if (this.intersectionObserver) {
      this.intersectionObserver.unobserve(element);
      this.observedElements.delete(element);
    }
  }
  
  // Cleanup
  destroy() {
    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
      this.intersectionObserver = null;
    }
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }
    this.observedElements.clear();
  }
}

// Global instance
const observerManager = new ObserverManager();

// Usage across app
observerManager.observeIntersection(element1, (entry) => {
  console.log("Element 1 intersecting:", entry.isIntersecting);
});

observerManager.observeIntersection(element2, (entry) => {
  console.log("Element 2 intersecting:", entry.isIntersecting);
});

// Debounce helper for resize observer
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

// Debounced resize handling
const resizeObserver = new ResizeObserver(debounce((entries) => {
  entries.forEach(entry => {
    // Expensive operation - only runs after 200ms of no resize
    console.log("Resize complete:", entry.contentBoxSize);
    updateChart(entry.target);
  });
}, 200));

// Cleanup pattern for React/Vue/etc
class ComponentWithObserver {
  constructor(element) {
    this.element = element;
    this.observer = new IntersectionObserver(this.handleIntersection.bind(this));
    this.observer.observe(this.element);
  }
  
  handleIntersection(entries) {
    entries.forEach(entry => {
      console.log("Intersecting:", entry.isIntersecting);
    });
  }
  
  // Call on component unmount
  destroy() {
    this.observer.disconnect();
    this.observer = null;
  }
}

// Usage in framework
class MyComponent {
  mounted() {
    this.componentObserver = new ComponentWithObserver(this.$el);
  }
  
  unmounted() {
    this.componentObserver.destroy();
  }
}
Note: Observers are more efficient than event listeners for visibility/size tracking. Use one observer instance for multiple elements when possible. Always disconnect observers on cleanup to prevent memory leaks in SPAs.

10.5 Lazy Loading Implementation with Observers

Lazy Loading Strategy Description
Image Lazy Loading Load images only when they enter or approach viewport using data-src attribute.
Native Lazy Loading Use <img loading="lazy"> for simple cases (less control).
Background Image Loading Apply background images via CSS class when element becomes visible.
Component Lazy Loading Dynamically import and render components when they enter viewport.
Video/Iframe Lazy Loading Defer loading heavy media until needed to improve initial page load.
rootMargin Preloading Use positive rootMargin (e.g., "100px") to start loading before element visible.

Example: Lazy load images

// Simple lazy loading
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      
      // Load image
      img.src = img.dataset.src;
      
      // Optional: load srcset
      if (img.dataset.srcset) {
        img.srcset = img.dataset.srcset;
      }
      
      // Remove loading class when loaded
      img.addEventListener("load", () => {
        img.classList.remove("lazy-loading");
        img.classList.add("lazy-loaded");
      });
      
      // Handle error
      img.addEventListener("error", () => {
        img.classList.add("lazy-error");
        console.error("Failed to load:", img.dataset.src);
      });
      
      // Stop observing this image
      observer.unobserve(img);
    }
  });
}, {
  "rootMargin": "50px" // Start loading 50px before entering viewport
});

// Observe all lazy images
document.querySelectorAll("img[data-src]").forEach(img => {
  img.classList.add("lazy-loading");
  imageObserver.observe(img);
});

// Advanced lazy loading with priority
class LazyLoader {
  constructor(options = {}) {
    this.options = {
      "rootMargin": "100px",
      "threshold": 0.01,
      ...options
    };
    
    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      this.options
    );
  }
  
  handleIntersection(entries, observer) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        this.loadElement(entry.target);
        observer.unobserve(entry.target);
      }
    });
  }
  
  loadElement(element) {
    const type = element.dataset.lazyType || "image";
    
    switch (type) {
      case "image":
        this.loadImage(element);
        break;
      case "video":
        this.loadVideo(element);
        break;
      case "iframe":
        this.loadIframe(element);
        break;
      case "background":
        this.loadBackground(element);
        break;
    }
  }
  
  loadImage(img) {
    const src = img.dataset.src;
    const srcset = img.dataset.srcset;
    
    return new Promise((resolve, reject) => {
      img.addEventListener("load", resolve);
      img.addEventListener("error", reject);
      
      if (srcset) img.srcset = srcset;
      img.src = src;
      
      img.removeAttribute("data-src");
      img.removeAttribute("data-srcset");
    }).then(() => {
      img.classList.add("loaded");
    }).catch(() => {
      img.classList.add("error");
    });
  }
  
  loadVideo(video) {
    const sources = video.querySelectorAll("source[data-src]");
    
    sources.forEach(source => {
      source.src = source.dataset.src;
      source.removeAttribute("data-src");
    });
    
    video.load();
    video.classList.add("loaded");
  }
  
  loadIframe(iframe) {
    iframe.src = iframe.dataset.src;
    iframe.removeAttribute("data-src");
    iframe.classList.add("loaded");
  }
  
  loadBackground(element) {
    const bgImage = element.dataset.bgSrc;
    element.style.backgroundImage = `url(${bgImage})`;
    element.removeAttribute("data-bg-src");
    element.classList.add("loaded");
  }
  
  observe(elements) {
    if (elements instanceof NodeList || Array.isArray(elements)) {
      elements.forEach(el => this.observer.observe(el));
    } else {
      this.observer.observe(elements);
    }
  }
  
  disconnect() {
    this.observer.disconnect();
  }
}

// Usage
const lazyLoader = new LazyLoader({ "rootMargin": "200px" });

// Lazy load images
lazyLoader.observe(document.querySelectorAll("img[data-src]"));

// Lazy load videos
lazyLoader.observe(document.querySelectorAll("video[data-lazy-type='video']"));

// Lazy load iframes (embeds, maps, etc)
lazyLoader.observe(document.querySelectorAll("iframe[data-src]"));
Note: Native lazy loading available with <img loading="lazy"> but Intersection Observer provides more control. Use rootMargin to preload before entering viewport. Always unobserve after loading to free resources.

10.6 Infinite Scrolling with Intersection Observer

Infinite Scroll Pattern Description
Sentinel Element Place invisible element at bottom of list; observe it to trigger loading.
Loading State Track loading state to prevent multiple simultaneous requests.
hasMore Flag Boolean flag to stop loading when no more items available.
rootMargin Trigger Use rootMargin to trigger loading before sentinel reaches viewport (better UX).
Error Handling Show error message and retry button if loading fails.
End of List Indicator Display message when all items loaded and stop observing.

Example: Infinite scroll implementation

// Simple infinite scroll
class InfiniteScroll {
  constructor(options) {
    this.container = options.container;
    this.loadMore = options.loadMore;
    this.loading = false;
    this.hasMore = true;
    
    // Create sentinel element at bottom
    this.sentinel = document.createElement("div");
    this.sentinel.className = "infinite-scroll-sentinel";
    this.container.appendChild(this.sentinel);
    
    // Observe sentinel
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting && !this.loading && this.hasMore) {
          this.load();
        }
      });
    }, {
      "rootMargin": "100px" // Trigger 100px before sentinel visible
    });
    
    this.observer.observe(this.sentinel);
  }
  
  async load() {
    if (this.loading || !this.hasMore) return;
    
    this.loading = true;
    this.showLoader();
    
    try {
      const result = await this.loadMore();
      
      // Check if more items available
      if (!result || result.items.length === 0) {
        this.hasMore = false;
        this.showEndMessage();
      } else {
        this.appendItems(result.items);
      }
    } catch (error) {
      console.error("Load more failed:", error);
      this.showError(error);
    } finally {
      this.loading = false;
      this.hideLoader();
    }
  }
  
  appendItems(items) {
    const fragment = document.createDocumentFragment();
    
    items.forEach(item => {
      const element = this.createItemElement(item);
      fragment.appendChild(element);
    });
    
    // Insert before sentinel
    this.container.insertBefore(fragment, this.sentinel);
  }
  
  createItemElement(item) {
    const div = document.createElement("div");
    div.className = "item";
    div.textContent = item.title;
    return div;
  }
  
  showLoader() {
    this.sentinel.classList.add("loading");
    this.sentinel.innerHTML = "<div class='loader'>Loading...</div>";
  }
  
  hideLoader() {
    this.sentinel.classList.remove("loading");
    this.sentinel.innerHTML = "";
  }
  
  showEndMessage() {
    this.sentinel.innerHTML = "<div class='end-message'>No more items</div>";
    this.observer.unobserve(this.sentinel);
  }
  
  showError(error) {
    this.sentinel.innerHTML = `<div class='error'>Error: ${error.message}</div>`;
  }
  
  destroy() {
    this.observer.disconnect();
    this.sentinel.remove();
  }
}

// Usage
let currentPage = 1;

const infiniteScroll = new InfiniteScroll({
  "container": document.querySelector(".items-container"),
  "loadMore": async () => {
    const response = await fetch(`/api/items?page=${currentPage}`);
    const data = await response.json();
    currentPage++;
    return data;
  }
});

// Advanced infinite scroll with bi-directional loading
class BiDirectionalScroll {
  constructor(options) {
    this.container = options.container;
    this.loadNewer = options.loadNewer;
    this.loadOlder = options.loadOlder;
    
    // Create sentinels
    this.topSentinel = document.createElement("div");
    this.bottomSentinel = document.createElement("div");
    
    this.container.insertBefore(this.topSentinel, this.container.firstChild);
    this.container.appendChild(this.bottomSentinel);
    
    // Observe both sentinels
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) return;
        
        if (entry.target === this.topSentinel) {
          this.loadNewer && this.loadNewer();
        } else if (entry.target === this.bottomSentinel) {
          this.loadOlder && this.loadOlder();
        }
      });
    }, { "rootMargin": "200px" });
    
    this.observer.observe(this.topSentinel);
    this.observer.observe(this.bottomSentinel);
  }
  
  destroy() {
    this.observer.disconnect();
  }
}

// Cleanup on page navigation
window.addEventListener("beforeunload", () => {
  infiniteScroll.destroy();
});
Note: Use sentinel element at bottom of list to trigger loading. Add rootMargin to start loading before user reaches end. Track loading state to prevent duplicate requests. Always provide visual feedback (loading spinner).
Warning: Infinite scroll can hurt performance with thousands of DOM nodes. Consider virtualization for very long lists. Provide "back to top" button for usability. Be careful with browser history - users expect back button to work.

Observer API Best Practices

  • Use Intersection Observer instead of scroll events - much more efficient
  • Use Resize Observer instead of window resize events or polling
  • Reuse single observer instance for multiple elements of same type
  • Call unobserve() after one-time observations to reduce overhead
  • Always disconnect() observers on component unmount to prevent memory leaks
  • Keep observer callbacks lightweight - defer expensive operations
  • Use rootMargin for preloading - positive values expand trigger area
  • Use appropriate threshold values - 0 for enter/exit, 1.0 for fully visible
  • Debounce expensive operations in ResizeObserver callbacks
  • For lazy loading, unobserve elements after loading to free resources
  • For infinite scroll, use sentinel element and track loading state
  • Test observer performance with many elements - consider pagination if needed

11. Web Components and Custom Elements APIs

11.1 Custom Elements Registration and Lifecycle

Method Description Browser Support
customElements.define(name, constructor, options) Registers custom element. Name must contain hyphen. Options: extends for customized built-ins. All Modern Browsers
customElements.get(name) Gets constructor for custom element. Returns undefined if not defined. All Modern Browsers
customElements.whenDefined(name) Returns Promise that resolves when element is defined. All Modern Browsers
customElements.upgrade(root) Upgrades custom elements in subtree that were created before definition. All Modern Browsers
Lifecycle Callback When Called Use Case
constructor() Element instance created. Called before attached to DOM. Initialize state, create shadow root
connectedCallback() Element inserted into DOM. Can be called multiple times. Setup, add event listeners, fetch data
disconnectedCallback() Element removed from DOM. Cleanup, remove listeners, cancel requests
attributeChangedCallback(name, oldValue, newValue) Observed attribute changed. React to attribute changes
adoptedCallback() Element moved to new document. Update references (rare)
Static Property Type Description
static observedAttributes string[] Array of attribute names to observe. Required for attributeChangedCallback.

Example: Basic custom element

// Define custom element
class MyElement extends HTMLElement {
  constructor() {
    super();
    console.log("Constructor called");
    
    // Initialize state
    this.count = 0;
    
    // Create shadow DOM
    this.attachShadow({ "mode": "open" });
    this.render();
  }
  
  // Lifecycle: element added to DOM
  connectedCallback() {
    console.log("Connected to DOM");
    
    // Add event listeners
    this.addEventListener("click", this.handleClick);
    
    // Can access attributes here
    console.log("Initial count:", this.getAttribute("count"));
  }
  
  // Lifecycle: element removed from DOM
  disconnectedCallback() {
    console.log("Disconnected from DOM");
    
    // Remove event listeners
    this.removeEventListener("click", this.handleClick);
  }
  
  // Lifecycle: attribute changed
  static get observedAttributes() {
    return ["count", "disabled"];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} changed: ${oldValue} → ${newValue}`);
    
    if (name === "count") {
      this.count = parseInt(newValue) || 0;
      this.render();
    }
  }
  
  // Methods
  handleClick = () => {
    this.count++;
    this.setAttribute("count", this.count);
  };
  
  render() {
    this.shadowRoot.innerHTML = `
      <style>
        button { padding: 10px 20px; font-size: 16px; }
      </style>
      <button>Count: ${this.count}</button>
    `;
  }
}

// Register custom element (name must have hyphen)
customElements.define("my-element", MyElement);

// Use in HTML: <my-element count="5"></my-element>

// Check if defined
const isDefined = customElements.get("my-element");
console.log("Defined:", isDefined !== undefined);

// Wait for definition
customElements.whenDefined("my-element").then(() => {
  console.log("my-element is defined");
  
  // Create programmatically
  const el = document.createElement("my-element");
  el.setAttribute("count", "10");
  document.body.appendChild(el);
});

Example: Customized built-in element

// Extend built-in element (not supported in Safari)
class FancyButton extends HTMLButtonElement {
  constructor() {
    super();
    this.addEventListener("click", this.handleClick);
  }
  
  handleClick() {
    this.style.transform = "scale(0.95)";
    setTimeout(() => {
      this.style.transform = "";
    }, 100);
  }
}

// Register with extends option
customElements.define("fancy-button", FancyButton, { "extends": "button" });

// Use with "is" attribute: <button is="fancy-button">Click me</button>

// Create programmatically
const btn = document.createElement("button", { "is": "fancy-button" });
btn.textContent = "Fancy";
document.body.appendChild(btn);
Note: Custom element names must contain hyphen (e.g., "my-element" not "myelement"). Constructor runs before element attached to DOM - don't access children or attributes there. Use connectedCallback for setup. Customized built-ins not supported in Safari.
Warning: Don't call super() without arguments in constructor. Don't add attributes in constructor - use connectedCallback. connectedCallback can be called multiple times if element moved. Always cleanup in disconnectedCallback.

11.2 Shadow DOM Encapsulation and Styling

Method/Property Description Browser Support
element.attachShadow(options) Creates shadow root. Options: mode ("open" or "closed"), delegatesFocus. All Modern Browsers
element.shadowRoot Returns shadow root if mode is "open", null if "closed". All Modern Browsers
shadowRoot.mode "open" or "closed". Determines if shadowRoot accessible from outside. All Modern Browsers
shadowRoot.host Returns host element of shadow root. All Modern Browsers
Shadow DOM Feature Description Benefit
Style Encapsulation Styles inside shadow DOM don't affect outside, and vice versa. Prevent CSS conflicts
DOM Encapsulation Shadow DOM not accessible via querySelector from outside. Implementation hiding
Event Retargeting Events from shadow DOM appear to come from host element. Maintain encapsulation
CSS Pseudo-elements/Selectors Description Use Case
:host Selects shadow host from inside shadow DOM. Style host element
:host(selector) Selects host if it matches selector. Conditional host styling
:host-context(selector) Selects host if ancestor matches selector. Theme-based styling
::slotted(selector) Selects slotted content. Style projected content
::part(name) Selects element with part attribute from outside. Allow external styling of parts

Example: Shadow DOM with styling

class StyledCard extends HTMLElement {
  constructor() {
    super();
    
    // Create shadow DOM (mode: "open" for accessibility)
    const shadow = this.attachShadow({ "mode": "open" });
    
    shadow.innerHTML = `
      <style>
        /* Styles scoped to shadow DOM */
        :host {
          display: block;
          border: 2px solid #ccc;
          border-radius: 8px;
          padding: 20px;
          background: white;
        }
        
        /* Style host when has .featured class */
        :host(.featured) {
          border-color: gold;
          border-width: 4px;
        }
        
        /* Style host when disabled attribute present */
        :host([disabled]) {
          opacity: 0.5;
          pointer-events: none;
        }
        
        /* Style host based on ancestor */
        :host-context(.dark-theme) {
          background: #333;
          color: white;
          border-color: #666;
        }
        
        h2 {
          margin: 0 0 10px 0;
          color: #333;
        }
        
        /* Expose styling hook with ::part */
        .title {
          font-size: 24px;
        }
        
        .content {
          color: #666;
        }
      </style>
      
      <div class="card">
        <h2 class="title" part="title">
          <slot name="title">Default Title</slot>
        </h2>
        <div class="content" part="content">
          <slot>Default content</slot>
        </div>
      </div>
    `;
  }
}

customElements.define("styled-card", StyledCard);

// Usage in HTML:
// <styled-card class="featured">
//   <span slot="title">My Title</span>
//   <p>Card content here</p>
// </styled-card>

// Style parts from outside:
// styled-card::part(title) { color: blue; }
// styled-card::part(content) { font-size: 14px; }

// Closed shadow DOM (shadowRoot not accessible)
class PrivateComponent extends HTMLElement {
  constructor() {
    super();
    
    // mode: "closed" - element.shadowRoot will be null
    const shadow = this.attachShadow({ "mode": "closed" });
    
    // Keep reference if needed internally
    this._shadowRoot = shadow;
    
    shadow.innerHTML = `<p>Private implementation</p>`;
  }
}

customElements.define("private-component", PrivateComponent);
Note: Use mode: "open" for better accessibility and debugging. Shadow DOM styles don't inherit from outside except inheritable properties (color, font-family, etc.). Use ::part() to expose styling hooks. :host-context() has limited browser support.
Warning: Not all elements can have shadow DOM (e.g., <input>, <img>). Closed shadow DOM still accessible via DevTools - not security feature. Events from shadow DOM appear to originate from host (event retargeting).

11.3 HTML Templates and Template Element

Element/Property Description Browser Support
<template> HTML element that holds client-side content not rendered on page load. All Browsers
template.content Returns DocumentFragment containing template's DOM subtree. All Browsers
document.importNode(node, deep) Clones node from another document. Use with template content. All Browsers

Example: Template element usage

// HTML template
// <template id="card-template">
//   <style>
//     .card { border: 1px solid #ccc; padding: 20px; }
//   </style>
//   <div class="card">
//     <h3 class="title"></h3>
//     <p class="description"></p>
//   </div>
// </template>

// Use template in custom element
class CardElement extends HTMLElement {
  constructor() {
    super();
    
    // Get template
    const template = document.getElementById("card-template");
    
    // Clone template content
    const content = template.content.cloneNode(true);
    
    // Attach to shadow DOM
    this.attachShadow({ "mode": "open" });
    this.shadowRoot.appendChild(content);
  }
  
  connectedCallback() {
    // Populate from attributes
    const title = this.getAttribute("title");
    const description = this.getAttribute("description");
    
    this.shadowRoot.querySelector(".title").textContent = title;
    this.shadowRoot.querySelector(".description").textContent = description;
  }
}

customElements.define("card-element", CardElement);

// Template-based component factory
class TemplateComponent extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ "mode": "open" });
    
    // Define template inline
    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        :host { display: block; }
        .container { padding: 20px; background: #f5f5f5; }
      </style>
      <div class="container">
        <slot></slot>
      </div>
    `;
    
    // Clone and append
    shadow.appendChild(template.content.cloneNode(true));
  }
}

customElements.define("template-component", TemplateComponent);

// Reusable template pattern
const createTemplate = (html) => {
  const template = document.createElement("template");
  template.innerHTML = html;
  return template;
};

const buttonTemplate = createTemplate(`
  <style>
    button {
      padding: 10px 20px;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover { background: #0056b3; }
  </style>
  <button><slot>Click me</slot></button>
`);

class CustomButton extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ "mode": "open" });
    shadow.appendChild(buttonTemplate.content.cloneNode(true));
  }
}

customElements.define("custom-button", CustomButton);
Note: Template content is inert - scripts don't run, images don't load, styles don't apply until cloned into document. Use cloneNode(true) to deep clone template content. Templates can be defined in HTML or created programmatically.

11.4 Slot API for Content Projection

Element/Attribute Description Use Case
<slot> Placeholder for projected content from light DOM. Content projection
<slot name="..."> Named slot for specific content. Multiple projection points
slot="name" Attribute on light DOM element to target named slot. Target specific slot
Slot Method/Property Description
slot.assignedNodes(options) Returns array of nodes assigned to slot. Options: flatten (include nested slots).
slot.assignedElements(options) Returns array of elements (not text nodes) assigned to slot.
element.assignedSlot Returns <slot> element that element is assigned to.
Slot Event When Fired Use Case
slotchange Assigned nodes of slot change. React to content changes

Example: Slots for content projection

// Custom element with slots
class UserCard extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ "mode": "open" });
    
    shadow.innerHTML = `
      <style>
        .card { border: 1px solid #ccc; padding: 20px; }
        .header { display: flex; align-items: center; gap: 10px; }
        .avatar { width: 50px; height: 50px; border-radius: 50%; }
        .name { font-size: 20px; font-weight: bold; }
        .bio { margin-top: 10px; color: #666; }
        .actions { margin-top: 15px; }
      </style>
      
      <div class="card">
        <div class="header">
          <!-- Named slot for avatar -->
          <slot name="avatar">
            <div class="avatar">👤</div>
          </slot>
          
          <!-- Named slot for name -->
          <div class="name">
            <slot name="name">Anonymous</slot>
          </div>
        </div>
        
        <!-- Default slot for bio -->
        <div class="bio">
          <slot>No bio provided</slot>
        </div>
        
        <!-- Named slot for actions -->
        <div class="actions">
          <slot name="actions"></slot>
        </div>
      </div>
    `;
    
    // Listen for slot changes
    shadow.querySelectorAll("slot").forEach(slot => {
      slot.addEventListener("slotchange", (e) => {
        console.log("Slot changed:", e.target.name || "default");
        console.log("Assigned nodes:", e.target.assignedNodes());
      });
    });
  }
}

customElements.define("user-card", UserCard);

// Usage:
// <user-card>
//   <img slot="avatar" src="avatar.jpg" class="avatar">
//   <span slot="name">John Doe</span>
//   <p>Software developer with passion for web technologies.</p>
//   <div slot="actions">
//     <button>Follow</button>
//     <button>Message</button>
//   </div>
// </user-card>

// Access slotted content
class SlotObserver extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ "mode": "open" });
    shadow.innerHTML = `
      <style>::slotted(h2) { color: blue; }</style>
      <slot></slot>
    `;
    
    const slot = shadow.querySelector("slot");
    
    // Get assigned nodes
    slot.addEventListener("slotchange", () => {
      const nodes = slot.assignedNodes();
      console.log("Assigned nodes:", nodes);
      
      // Only elements (not text nodes)
      const elements = slot.assignedElements();
      console.log("Assigned elements:", elements);
      
      // Flatten nested slots
      const flattenedNodes = slot.assignedNodes({ "flatten": true });
      console.log("Flattened:", flattenedNodes);
    });
  }
  
  connectedCallback() {
    // Access assigned slot from light DOM
    const children = this.children;
    Array.from(children).forEach(child => {
      console.log("Element's assigned slot:", child.assignedSlot);
    });
  }
}

customElements.define("slot-observer", SlotObserver);
Note: Slots enable content projection - light DOM content rendered in shadow DOM. Unnamed slots are default slots. Multiple elements can target same named slot. Use ::slotted() to style slotted content from shadow DOM.
Warning: ::slotted() can only select direct children, not descendants. Slotted elements remain in light DOM - only rendered location changes. Slot fallback content shown when no content assigned to slot.

11.5 Element Internals for Form Integration

Method/Property Description Browser Support
element.attachInternals() Returns ElementInternals object for form/accessibility integration. All Modern Browsers
static formAssociated = true Marks element as form-associated. Required for form integration. All Modern Browsers
ElementInternals Property/Method Description
form Returns associated <form> element.
setFormValue(value, state) Sets element's form value. Optional state for internal representation.
setValidity(flags, message, anchor) Sets custom validity state. Flags object, validation message, anchor element.
willValidate true if element participates in form validation.
validity Returns ValidityState object.
validationMessage Returns validation message string.
checkValidity() Checks validity, fires invalid event if invalid.
reportValidity() Checks validity and reports to user.
Form Lifecycle Callback When Called
formAssociatedCallback(form) Element associated with form.
formDisabledCallback(disabled) Element's disabled state changed via <fieldset>.
formResetCallback() Form reset.
formStateRestoreCallback(state, mode) Browser restores state. Mode: "restore" or "autocomplete".

Example: Form-associated custom element

// Custom input element with form integration
class CustomInput extends HTMLElement {
  // Must set formAssociated to true
  static formAssociated = true;
  
  constructor() {
    super();
    
    // Attach internals for form integration
    this._internals = this.attachInternals();
    
    const shadow = this.attachShadow({ "mode": "open" });
    shadow.innerHTML = `
      <style>
        input {
          padding: 8px;
          border: 1px solid #ccc;
          border-radius: 4px;
        }
        input:invalid { border-color: red; }
        .error { color: red; font-size: 12px; }
      </style>
      <input type="text" />
      <div class="error"></div>
    `;
    
    this._input = shadow.querySelector("input");
    this._error = shadow.querySelector(".error");
    
    // Sync internal input with form value
    this._input.addEventListener("input", () => {
      this.value = this._input.value;
    });
  }
  
  // Form lifecycle callbacks
  formAssociatedCallback(form) {
    console.log("Associated with form:", form);
  }
  
  formResetCallback() {
    console.log("Form reset");
    this.value = "";
  }
  
  formDisabledCallback(disabled) {
    console.log("Disabled:", disabled);
    this._input.disabled = disabled;
  }
  
  formStateRestoreCallback(state, mode) {
    console.log("State restore:", state, mode);
    this.value = state;
  }
  
  // Value getter/setter
  get value() {
    return this._input.value;
  }
  
  set value(val) {
    this._input.value = val;
    
    // Update form value
    this._internals.setFormValue(val);
    
    // Validate
    this.validate();
  }
  
  // Validation
  validate() {
    const value = this.value;
    
    if (this.hasAttribute("required") && !value) {
      this._internals.setValidity(
        { "valueMissing": true },
        "This field is required",
        this._input
      );
      this._error.textContent = "This field is required";
      return false;
    }
    
    const minLength = parseInt(this.getAttribute("minlength")) || 0;
    if (value.length < minLength) {
      this._internals.setValidity(
        { "tooShort": true },
        `Minimum ${minLength} characters required`,
        this._input
      );
      this._error.textContent = `Minimum ${minLength} characters required`;
      return false;
    }
    
    // Valid
    this._internals.setValidity({});
    this._error.textContent = "";
    return true;
  }
  
  // Standard form methods
  checkValidity() {
    return this._internals.checkValidity();
  }
  
  reportValidity() {
    return this._internals.reportValidity();
  }
  
  // Access to form
  get form() {
    return this._internals.form;
  }
  
  get validity() {
    return this._internals.validity;
  }
  
  get validationMessage() {
    return this._internals.validationMessage;
  }
}

customElements.define("custom-input", CustomInput);

// Usage:
// <form>
//   <custom-input name="username" required minlength="3"></custom-input>
//   <button type="submit">Submit</button>
// </form>

// Access form value
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault();
  
  const formData = new FormData(form);
  console.log("Username:", formData.get("username"));
});
Note: Set static formAssociated = true to enable form integration. Use attachInternals() to get ElementInternals object. Custom elements integrate with native form features - validation, FormData, submit events. Use setFormValue() to sync with form.
Warning: Can only call attachInternals() once. Must set formAssociated before element registered. Form callbacks may not fire in all scenarios - test thoroughly. ElementInternals has good browser support but check for older browsers.

11.6 Adoptable Stylesheets for Shadow DOM

Method/Property Description Browser Support
new CSSStyleSheet() Creates new stylesheet object (constructable). All Modern Browsers
sheet.replace(css) Replaces stylesheet content. Returns Promise. Async. All Modern Browsers
sheet.replaceSync(css) Replaces stylesheet content synchronously. All Modern Browsers
document.adoptedStyleSheets Array of stylesheets for document. All Modern Browsers
shadowRoot.adoptedStyleSheets Array of stylesheets for shadow root. All Modern Browsers

Example: Adoptable stylesheets

// Create shared stylesheet
const sharedStyles = new CSSStyleSheet();
sharedStyles.replaceSync(`
  .card {
    border: 1px solid #ccc;
    border-radius: 8px;
    padding: 20px;
    background: white;
  }
  
  .card:hover {
    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  }
  
  h2 { margin: 0 0 10px 0; }
  p { color: #666; }
`);

// Use in multiple components
class Card1 extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ "mode": "open" });
    
    // Adopt shared stylesheet
    shadow.adoptedStyleSheets = [sharedStyles];
    
    shadow.innerHTML = `
      <div class="card">
        <h2>Card 1</h2>
        <p>Content</p>
      </div>
    `;
  }
}

class Card2 extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ "mode": "open" });
    
    // Reuse same stylesheet (no duplication)
    shadow.adoptedStyleSheets = [sharedStyles];
    
    shadow.innerHTML = `
      <div class="card">
        <h2>Card 2</h2>
        <p>Content</p>
      </div>
    `;
  }
}

customElements.define("card-1", Card1);
customElements.define("card-2", Card2);

// Multiple stylesheets
const baseStyles = new CSSStyleSheet();
baseStyles.replaceSync(`:host { display: block; }`);

const themeStyles = new CSSStyleSheet();
themeStyles.replaceSync(`
  .dark { background: #333; color: white; }
`);

class ThemedComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ "mode": "open" });
    
    // Use multiple stylesheets
    shadow.adoptedStyleSheets = [baseStyles, themeStyles];
    
    shadow.innerHTML = `<div class="dark">Themed</div>`;
  }
}

customElements.define("themed-component", ThemedComponent);

// Dynamic stylesheet updates
const dynamicStyles = new CSSStyleSheet();
dynamicStyles.replaceSync(`.text { color: blue; }`);

class DynamicStyled extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ "mode": "open" });
    shadow.adoptedStyleSheets = [dynamicStyles];
    shadow.innerHTML = `<p class="text">Dynamic</p>`;
  }
  
  setColor(color) {
    // Update shared stylesheet (affects all instances)
    dynamicStyles.replaceSync(`.text { color: ${color}; }`);
  }
}

customElements.define("dynamic-styled", DynamicStyled);

// Async stylesheet loading
async function loadStylesheet(url) {
  const response = await fetch(url);
  const css = await response.text();
  
  const sheet = new CSSStyleSheet();
  await sheet.replace(css);
  
  return sheet;
}

// Usage
const externalStyles = await loadStylesheet("/styles/component.css");

class StyledFromExternal extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ "mode": "open" });
    shadow.adoptedStyleSheets = [externalStyles];
    shadow.innerHTML = `<div>Styled from external CSS</div>`;
  }
}

customElements.define("styled-from-external", StyledFromExternal);
Note: Adoptable Stylesheets enable stylesheet reuse across shadow roots - better performance than duplicating <style> tags. Stylesheets created once, shared across components. Use replaceSync() for initial load, replace() for async updates.
Warning: Changes to shared stylesheet affect all components using it - be careful with dynamic updates. adoptedStyleSheets replaces array - use spread to add: [...shadow.adoptedStyleSheets, newSheet]. Constructable stylesheets not available in older browsers.

Web Components Best Practices

  • Always use hyphenated names for custom elements - required by spec
  • Use mode: "open" for shadow DOM - better accessibility and debugging
  • Initialize in constructor, setup in connectedCallback
  • Always cleanup in disconnectedCallback - remove listeners, cancel timers
  • Use attributeChangedCallback with observedAttributes for reactive attributes
  • Use templates to avoid creating DOM strings repeatedly
  • Use slots for content projection - more flexible than imperative DOM manipulation
  • Use ::part() to expose styling hooks for component consumers
  • Set formAssociated = true for form-integrated custom elements
  • Use adoptable stylesheets to share styles across components efficiently
  • Avoid customized built-ins - not supported in Safari
  • Test components in isolation and with various content projections

12. Authentication and Security APIs

12.1 Web Authentication (WebAuthn) API

Method Description Browser Support
navigator.credentials.create(options) Creates new credential (registration). Options include publicKey for WebAuthn. All Modern Browsers
navigator.credentials.get(options) Gets credential for authentication. Options include publicKey for WebAuthn. All Modern Browsers
PublicKeyCredential Property Type Description
id string Base64url-encoded credential ID.
rawId ArrayBuffer Raw credential ID bytes.
type string Always "public-key" for WebAuthn.
response AuthenticatorResponse Authenticator response (attestation or assertion).

Example: WebAuthn registration

// Registration (create new credential)
async function registerWebAuthn(username) {
  try {
    // Get challenge from server
    const challengeResponse = await fetch("/auth/register/challenge", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({ "username": username })
    });
    
    const { "challenge": challenge, "userId": userId } = await challengeResponse.json();
    
    // Create credential
    const credential = await navigator.credentials.create({
      "publicKey": {
        "challenge": base64ToArrayBuffer(challenge),
        "rp": {
          "name": "My App",
          "id": "example.com" // Domain without protocol/port
        },
        "user": {
          "id": base64ToArrayBuffer(userId),
          "name": username,
          "displayName": username
        },
        "pubKeyCredParams": [
          { "type": "public-key", "alg": -7 }, // ES256
          { "type": "public-key", "alg": -257 } // RS256
        ],
        "timeout": 60000, // 60 seconds
        "attestation": "none", // "none", "indirect", or "direct"
        "authenticatorSelection": {
          "authenticatorAttachment": "platform", // "platform" (built-in) or "cross-platform" (USB key)
          "requireResidentKey": false,
          "userVerification": "preferred" // "required", "preferred", or "discouraged"
        }
      }
    });
    
    // Send credential to server for verification
    const registrationResponse = await fetch("/auth/register/verify", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "id": credential.id,
        "rawId": arrayBufferToBase64(credential.rawId),
        "type": credential.type,
        "response": {
          "clientDataJSON": arrayBufferToBase64(credential.response.clientDataJSON),
          "attestationObject": arrayBufferToBase64(credential.response.attestationObject)
        }
      })
    });
    
    const result = await registrationResponse.json();
    console.log("Registration successful:", result);
    
    return result;
  } catch (error) {
    console.error("WebAuthn registration failed:", error);
    throw error;
  }
}

// Helper functions
function base64ToArrayBuffer(base64) {
  const binary = atob(base64.replace(/-/g, "+").replace(/_/g, "/"));
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = "";
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

Example: WebAuthn authentication

// Authentication (get existing credential)
async function authenticateWebAuthn(username) {
  try {
    // Get challenge from server
    const challengeResponse = await fetch("/auth/login/challenge", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({ "username": username })
    });
    
    const { "challenge": challenge, "allowCredentials": allowCredentials } = await challengeResponse.json();
    
    // Get credential
    const credential = await navigator.credentials.get({
      "publicKey": {
        "challenge": base64ToArrayBuffer(challenge),
        "timeout": 60000,
        "rpId": "example.com",
        "allowCredentials": allowCredentials.map(cred => ({
          "type": "public-key",
          "id": base64ToArrayBuffer(cred.id)
        })),
        "userVerification": "preferred"
      }
    });
    
    // Send assertion to server for verification
    const authResponse = await fetch("/auth/login/verify", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "id": credential.id,
        "rawId": arrayBufferToBase64(credential.rawId),
        "type": credential.type,
        "response": {
          "clientDataJSON": arrayBufferToBase64(credential.response.clientDataJSON),
          "authenticatorData": arrayBufferToBase64(credential.response.authenticatorData),
          "signature": arrayBufferToBase64(credential.response.signature),
          "userHandle": credential.response.userHandle ? 
                        arrayBufferToBase64(credential.response.userHandle) : null
        }
      })
    });
    
    const result = await authResponse.json();
    console.log("Authentication successful:", result);
    
    return result;
  } catch (error) {
    console.error("WebAuthn authentication failed:", error);
    throw error;
  }
}

// Check browser support
if (window.PublicKeyCredential) {
  console.log("WebAuthn supported");
  
  // Check platform authenticator availability
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
    .then(available => {
      console.log("Platform authenticator available:", available);
    });
} else {
  console.log("WebAuthn not supported");
}
Note: WebAuthn enables passwordless authentication using biometrics or security keys. More secure than passwords - phishing resistant. Requires HTTPS. Server must validate attestation/assertion. Use platform authenticators (Face ID, Touch ID, Windows Hello) for best UX.
Warning: WebAuthn requires server-side implementation for challenge generation and verification. Always use proper challenge validation to prevent replay attacks. Test on multiple platforms - behavior varies. Fallback to password auth for browsers without support.

12.2 Credential Management API for Password Storage

Method Description Browser Support
navigator.credentials.create(options) Creates credential. Options: password for PasswordCredential. Chrome, Edge
navigator.credentials.get(options) Gets stored credential. Options: password, mediation. Chrome, Edge
navigator.credentials.store(credential) Stores credential in browser's password manager. Chrome, Edge
navigator.credentials.preventSilentAccess() Prevents automatic sign-in after sign-out. Chrome, Edge
PasswordCredential Property Type Description
id string Username/email.
password string Password.
name string Display name.
iconURL string Icon URL.

Example: Password credential management

// Auto sign-in on page load
async function autoSignIn() {
  try {
    // Get stored credential
    const credential = await navigator.credentials.get({
      "password": true,
      "mediation": "optional" // "optional", "silent", or "required"
    });
    
    if (credential) {
      console.log("Credential found:", credential.id);
      
      // Use credential to sign in
      const response = await fetch("/auth/login", {
        "method": "POST",
        "headers": { "Content-Type": "application/json" },
        "body": JSON.stringify({
          "username": credential.id,
          "password": credential.password
        })
      });
      
      if (response.ok) {
        console.log("Auto sign-in successful");
        return true;
      }
    } else {
      console.log("No stored credentials");
    }
  } catch (error) {
    console.error("Auto sign-in failed:", error);
  }
  
  return false;
}

// Store credential after successful registration/login
async function storeCredential(username, password, name) {
  try {
    // Create password credential
    const credential = new PasswordCredential({
      "id": username,
      "password": password,
      "name": name,
      "iconURL": "/images/avatar.png"
    });
    
    // Store credential
    await navigator.credentials.store(credential);
    console.log("Credential stored");
    
    return true;
  } catch (error) {
    console.error("Failed to store credential:", error);
    return false;
  }
}

// Alternative: create from form
async function storeCredentialFromForm() {
  const form = document.querySelector("form");
  
  try {
    const credential = await navigator.credentials.create({
      "password": form
    });
    
    await navigator.credentials.store(credential);
    console.log("Credential from form stored");
  } catch (error) {
    console.error("Failed to store credential from form:", error);
  }
}

// Prevent auto sign-in after logout
async function signOut() {
  // Prevent silent access on next visit
  await navigator.credentials.preventSilentAccess();
  
  console.log("Signed out - auto sign-in disabled");
  
  // Clear session
  await fetch("/auth/logout", { "method": "POST" });
}

// Check support
if (window.PasswordCredential) {
  console.log("Credential Management API supported");
  
  // Try auto sign-in
  autoSignIn().then(success => {
    if (!success) {
      // Show login form
      document.querySelector(".login-form").style.display = "block";
    }
  });
} else {
  console.log("Credential Management API not supported");
}
Note: Credential Management API has limited browser support (Chrome, Edge). Integrates with browser's password manager. Use mediation: "silent" for automatic sign-in, "required" to always show account picker. Always call preventSilentAccess() on sign-out.
Warning: API doesn't store passwords itself - relies on browser's password manager. Not available in Safari/Firefox. Always provide fallback manual login. Don't rely on this for security - still validate on server.

12.3 Payment Request API for Web Payments

Method Description Browser Support
new PaymentRequest(methods, details, options) Creates payment request. Methods: payment methods, details: items/total, options: shipping. Chrome, Edge, Safari
request.show() Shows payment UI. Returns Promise<PaymentResponse>. Requires user gesture. Chrome, Edge, Safari
request.canMakePayment() Checks if user can make payment. Returns Promise<boolean>. Chrome, Edge, Safari
request.abort() Aborts payment request. Chrome, Edge, Safari
PaymentResponse Method Description
response.complete(result) Completes payment. Result: "success", "fail", or "unknown".
response.retry(errors) Retries payment with error messages.

Example: Payment Request API

// Process payment
async function processPayment() {
  try {
    // Check support
    if (!window.PaymentRequest) {
      console.log("Payment Request API not supported");
      return;
    }
    
    // Define payment methods
    const paymentMethods = [
      {
        "supportedMethods": "basic-card",
        "data": {
          "supportedNetworks": ["visa", "mastercard", "amex"],
          "supportedTypes": ["debit", "credit"]
        }
      }
    ];
    
    // Define payment details
    const paymentDetails = {
      "total": {
        "label": "Total",
        "amount": { "currency": "USD", "value": "99.99" }
      },
      "displayItems": [
        {
          "label": "Product",
          "amount": { "currency": "USD", "value": "89.99" }
        },
        {
          "label": "Shipping",
          "amount": { "currency": "USD", "value": "10.00" }
        }
      ]
    };
    
    // Optional: shipping options
    const options = {
      "requestShipping": true,
      "requestPayerName": true,
      "requestPayerEmail": true,
      "requestPayerPhone": true,
      "shippingOptions": [
        {
          "id": "standard",
          "label": "Standard Shipping",
          "amount": { "currency": "USD", "value": "10.00" },
          "selected": true
        },
        {
          "id": "express",
          "label": "Express Shipping",
          "amount": { "currency": "USD", "value": "20.00" }
        }
      ]
    };
    
    // Create payment request
    const request = new PaymentRequest(paymentMethods, paymentDetails, options);
    
    // Check if user can make payment
    const canMakePayment = await request.canMakePayment();
    if (!canMakePayment) {
      console.log("User cannot make payment");
      return;
    }
    
    // Listen for shipping address change
    request.addEventListener("shippingaddresschange", async (e) => {
      // Update shipping options based on address
      e.updateWith(new Promise((resolve) => {
        // Calculate shipping based on address
        const shippingAddress = request.shippingAddress;
        console.log("Shipping to:", shippingAddress.country);
        
        // Update details
        resolve({
          "total": {
            "label": "Total",
            "amount": { "currency": "USD", "value": "99.99" }
          },
          "shippingOptions": options.shippingOptions
        });
      }));
    });
    
    // Listen for shipping option change
    request.addEventListener("shippingoptionchange", async (e) => {
      const shippingOption = request.shippingOption;
      console.log("Shipping option:", shippingOption);
      
      // Update total
      const shippingCost = shippingOption === "express" ? "20.00" : "10.00";
      const total = (89.99 + parseFloat(shippingCost)).toFixed(2);
      
      e.updateWith({
        "total": {
          "label": "Total",
          "amount": { "currency": "USD", "value": total }
        }
      });
    });
    
    // Show payment UI
    const response = await request.show();
    
    // Get payment details
    console.log("Card number:", response.details.cardNumber);
    console.log("Payer name:", response.payerName);
    console.log("Payer email:", response.payerEmail);
    console.log("Shipping address:", response.shippingAddress);
    
    // Process payment on server
    const serverResponse = await fetch("/api/process-payment", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "paymentMethod": response.methodName,
        "details": response.details,
        "shippingAddress": response.shippingAddress,
        "total": paymentDetails.total.amount.value
      })
    });
    
    if (serverResponse.ok) {
      // Complete payment successfully
      await response.complete("success");
      console.log("Payment successful");
    } else {
      // Payment failed
      await response.complete("fail");
      console.log("Payment failed");
    }
    
  } catch (error) {
    console.error("Payment error:", error);
    
    if (error.name === "AbortError") {
      console.log("Payment cancelled by user");
    }
  }
}

// Trigger on button click (requires user gesture)
document.getElementById("pay-button").addEventListener("click", processPayment);
Note: Payment Request API provides native payment UI across browsers. Supports credit cards, digital wallets (Apple Pay, Google Pay). Must call show() from user gesture. Always call complete() to close payment UI.
Warning: Payment Request API only handles UI - doesn't process payments. Must implement server-side payment processing. Never trust client-side data - validate on server. Test on mobile - UX varies by platform.

12.4 Permissions API for Feature Access Control

Method Description Browser Support
navigator.permissions.query(descriptor) Queries permission state. Descriptor: { name: "camera" }. Returns Promise<PermissionStatus>. All Modern Browsers
Permission Name Description Support
camera Camera access (getUserMedia). Good
microphone Microphone access (getUserMedia). Good
geolocation Location access. Good
notifications Notification permission. Good
push Push notification permission. Modern
persistent-storage Persistent storage quota. Limited
PermissionStatus Property Type Description
state string "granted", "denied", or "prompt".
onchange EventHandler Fires when permission state changes.

Example: Check and request permissions

// Check permission status
async function checkPermission(name) {
  try {
    const status = await navigator.permissions.query({ "name": name });
    
    console.log(`${name} permission:`, status.state);
    
    // Listen for changes
    status.addEventListener("change", () => {
      console.log(`${name} permission changed to:`, status.state);
    });
    
    return status.state;
  } catch (error) {
    console.error(`Failed to query ${name} permission:`, error);
    return null;
  }
}

// Camera permission
async function requestCameraAccess() {
  const status = await checkPermission("camera");
  
  if (status === "granted") {
    console.log("Camera already granted");
    return true;
  } else if (status === "denied") {
    console.log("Camera denied - show instructions to enable");
    return false;
  } else if (status === "prompt") {
    console.log("Camera will prompt user");
    
    // Request access (triggers permission prompt)
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ "video": true });
      stream.getTracks().forEach(track => track.stop());
      return true;
    } catch (error) {
      console.error("Camera access denied:", error);
      return false;
    }
  }
}

// Geolocation permission
async function requestLocationAccess() {
  const status = await navigator.permissions.query({ "name": "geolocation" });
  
  console.log("Geolocation permission:", status.state);
  
  if (status.state === "granted") {
    // Already granted - get location
    navigator.geolocation.getCurrentPosition(
      (position) => console.log("Location:", position.coords),
      (error) => console.error("Location error:", error)
    );
  } else {
    // Will prompt or is denied
    navigator.geolocation.getCurrentPosition(
      (position) => console.log("Location granted:", position.coords),
      (error) => console.error("Location denied:", error)
    );
  }
}

// Notification permission
async function requestNotificationAccess() {
  const status = await navigator.permissions.query({ "name": "notifications" });
  
  console.log("Notification permission:", status.state);
  
  if (status.state === "prompt") {
    // Request permission
    const result = await Notification.requestPermission();
    console.log("Notification permission result:", result);
  }
}

// Check multiple permissions
async function checkMultiplePermissions() {
  const permissions = ["camera", "microphone", "geolocation", "notifications"];
  
  const results = await Promise.all(
    permissions.map(async (name) => {
      try {
        const status = await navigator.permissions.query({ "name": name });
        return { "name": name, "state": status.state };
      } catch (error) {
        return { "name": name, "state": "unsupported" };
      }
    })
  );
  
  console.log("Permission states:");
  results.forEach(({ "name": name, "state": state }) => {
    console.log(`  ${name}: ${state}`);
  });
  
  return results;
}

// Permission-aware UI
async function updateUIBasedOnPermissions() {
  const cameraStatus = await checkPermission("camera");
  
  const cameraButton = document.getElementById("camera-button");
  
  if (cameraStatus === "granted") {
    cameraButton.textContent = "Open Camera";
    cameraButton.disabled = false;
  } else if (cameraStatus === "denied") {
    cameraButton.textContent = "Camera Denied";
    cameraButton.disabled = true;
    // Show instructions to enable in settings
  } else {
    cameraButton.textContent = "Allow Camera";
    cameraButton.disabled = false;
  }
}
Note: Permissions API only queries state - doesn't request permission. To request, use the actual API (getUserMedia, Notification.requestPermission, etc.). Use onchange to update UI when user changes permission in browser settings.
Warning: Not all permissions queryable on all browsers. Some permissions always return "prompt" state. Don't rely solely on Permissions API - handle permission errors when using actual APIs. Browser support varies by permission type.

12.5 Content Security Policy (CSP) Reporting API

Event Description Browser Support
securitypolicyviolation Fires when CSP violation occurs. Event has violation details. All Modern Browsers
SecurityPolicyViolationEvent Property Type Description
violatedDirective string CSP directive that was violated (e.g., "script-src").
effectiveDirective string Effective directive that caused violation.
blockedURI string URI that was blocked.
sourceFile string File where violation occurred.
lineNumber number Line number of violation.
columnNumber number Column number of violation.
disposition string "enforce" or "report" (report-only mode).

Example: CSP violation monitoring

// Listen for CSP violations
document.addEventListener("securitypolicyviolation", (e) => {
  console.warn("CSP Violation detected:");
  console.log("Violated directive:", e.violatedDirective);
  console.log("Effective directive:", e.effectiveDirective);
  console.log("Blocked URI:", e.blockedURI);
  console.log("Source file:", e.sourceFile);
  console.log("Line:", e.lineNumber, "Column:", e.columnNumber);
  console.log("Disposition:", e.disposition);
  console.log("Original policy:", e.originalPolicy);
  
  // Send violation to analytics
  sendCSPViolation({
    "directive": e.violatedDirective,
    "blockedURI": e.blockedURI,
    "sourceFile": e.sourceFile,
    "lineNumber": e.lineNumber,
    "disposition": e.disposition,
    "timestamp": Date.now(),
    "userAgent": navigator.userAgent
  });
});

// Send CSP violations to server
function sendCSPViolation(violation) {
  // Use sendBeacon for reliability
  if (navigator.sendBeacon) {
    navigator.sendBeacon("/api/csp-violations", JSON.stringify(violation));
  } else {
    fetch("/api/csp-violations", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify(violation)
    }).catch(error => console.error("Failed to send CSP violation:", error));
  }
}

// CSP violation aggregator
class CSPMonitor {
  constructor() {
    this.violations = [];
    this.listeners = [];
    
    document.addEventListener("securitypolicyviolation", (e) => {
      this.handleViolation(e);
    });
  }
  
  handleViolation(event) {
    const violation = {
      "directive": event.violatedDirective,
      "effectiveDirective": event.effectiveDirective,
      "blockedURI": event.blockedURI,
      "sourceFile": event.sourceFile,
      "lineNumber": event.lineNumber,
      "columnNumber": event.columnNumber,
      "disposition": event.disposition,
      "timestamp": Date.now()
    };
    
    this.violations.push(violation);
    
    // Notify listeners
    this.listeners.forEach(callback => callback(violation));
    
    // Auto-report after 10 violations or 5 seconds
    if (this.violations.length >= 10) {
      this.report();
    } else if (this.violations.length === 1) {
      setTimeout(() => this.report(), 5000);
    }
  }
  
  report() {
    if (this.violations.length === 0) return;
    
    const violations = this.violations.slice();
    this.violations = [];
    
    console.log(`Reporting ${violations.length} CSP violations`);
    
    fetch("/api/csp-violations/batch", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({ "violations": violations })
    }).catch(error => console.error("Failed to report CSP violations:", error));
  }
  
  onViolation(callback) {
    this.listeners.push(callback);
  }
  
  getViolations() {
    return this.violations.slice();
  }
}

// Initialize monitor
const cspMonitor = new CSPMonitor();

cspMonitor.onViolation((violation) => {
  console.warn("CSP violation:", violation);
  
  // Show warning in dev mode
  if (process.env.NODE_ENV === "development") {
    console.warn("Fix CSP violation:", violation.blockedURI);
  }
});
Note: CSP violations fire securitypolicyviolation event. Use to monitor and fix CSP issues. Set CSP in report-only mode during development with Content-Security-Policy-Report-Only header. Aggregate violations before sending to reduce requests.

12.6 SubtleCrypto API for Cryptographic Operations

Method Description Browser Support
crypto.subtle.encrypt(algorithm, key, data) Encrypts data. Returns Promise<ArrayBuffer>. All Modern Browsers
crypto.subtle.decrypt(algorithm, key, data) Decrypts data. Returns Promise<ArrayBuffer>. All Modern Browsers
crypto.subtle.digest(algorithm, data) Hashes data. Algorithm: "SHA-1", "SHA-256", "SHA-384", "SHA-512". All Modern Browsers
crypto.subtle.generateKey(algorithm, extractable, keyUsages) Generates key. Returns Promise<CryptoKey>. All Modern Browsers
crypto.subtle.importKey(format, keyData, algorithm, extractable, keyUsages) Imports key. Format: "raw", "pkcs8", "spki", "jwk". All Modern Browsers
crypto.subtle.exportKey(format, key) Exports key. Returns Promise<ArrayBuffer | JsonWebKey>. All Modern Browsers
crypto.subtle.sign(algorithm, key, data) Signs data. Returns Promise<ArrayBuffer>. All Modern Browsers
crypto.subtle.verify(algorithm, key, signature, data) Verifies signature. Returns Promise<boolean>. All Modern Browsers

Example: Hash data with SHA-256

// Hash string with SHA-256
async function hashString(message) {
  // Encode string to bytes
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  
  // Hash data
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  
  // Convert to hex string
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
  
  return hashHex;
}

// Usage
const hash = await hashString("Hello, World!");
console.log("SHA-256 hash:", hash);

Example: Encrypt/decrypt with AES-GCM

// Generate AES-GCM key
async function generateAESKey() {
  const key = await crypto.subtle.generateKey(
    {
      "name": "AES-GCM",
      "length": 256
    },
    true, // extractable
    ["encrypt", "decrypt"]
  );
  
  return key;
}

// Encrypt data
async function encryptData(key, data) {
  const encoder = new TextEncoder();
  const encoded = encoder.encode(data);
  
  // Generate random IV (initialization vector)
  const iv = crypto.getRandomValues(new Uint8Array(12));
  
  // Encrypt
  const encrypted = await crypto.subtle.encrypt(
    {
      "name": "AES-GCM",
      "iv": iv
    },
    key,
    encoded
  );
  
  // Return IV + encrypted data
  return {
    "iv": Array.from(iv),
    "data": Array.from(new Uint8Array(encrypted))
  };
}

// Decrypt data
async function decryptData(key, encryptedData) {
  const decrypted = await crypto.subtle.decrypt(
    {
      "name": "AES-GCM",
      "iv": new Uint8Array(encryptedData.iv)
    },
    key,
    new Uint8Array(encryptedData.data)
  );
  
  const decoder = new TextDecoder();
  return decoder.decode(decrypted);
}

// Usage
const key = await generateAESKey();
const encrypted = await encryptData(key, "Secret message");
const decrypted = await decryptData(key, encrypted);
console.log("Decrypted:", decrypted);
Note: SubtleCrypto API is for cryptographic operations - hashing, encryption, signing. Only available in secure contexts (HTTPS). Use AES-GCM for encryption, SHA-256 for hashing, RSA/ECDSA for signing. All methods return Promises.
Warning: Crypto is complex - use established libraries when possible. Never implement your own crypto algorithms. Always use random IVs for encryption. Don't use for password storage - use server-side bcrypt/argon2. SubtleCrypto requires HTTPS.

Authentication and Security Best Practices

  • Use WebAuthn for passwordless authentication - more secure than passwords
  • Implement proper challenge validation for WebAuthn on server
  • Credential Management API has limited support - provide fallback
  • Payment Request API only handles UI - implement server-side processing
  • Use Permissions API to check state before requesting - better UX
  • Monitor CSP violations with securitypolicyviolation event
  • Use SubtleCrypto for client-side encryption - requires HTTPS
  • Never implement custom crypto - use established algorithms
  • Always validate and sanitize data on server - never trust client
  • Use HTTPS for all security-sensitive operations
  • Test security features across browsers - support varies
  • Provide clear error messages when permissions denied

13. Communication and Sharing APIs

13.1 Broadcast Channel API for Tab Communication

Method/Property Description Browser Support
new BroadcastChannel(name) Creates channel with specified name. All same-origin contexts with same name can communicate. All Modern Browsers
channel.postMessage(data) Sends message to all listeners on same channel. Data is cloned using structured clone algorithm. All Modern Browsers
channel.onmessage Event handler for incoming messages. Receives MessageEvent with data property. All Modern Browsers
channel.close() Closes channel and stops receiving messages. Should be called when done to free resources. All Modern Browsers
channel.name Read-only property returning channel name. All Modern Browsers

Example: Tab synchronization with BroadcastChannel

// Create broadcast channel for tab sync
const channel = new BroadcastChannel("app-sync");

// Listen for messages
channel.onmessage = (event) => {
  console.log("Received from another tab:", event.data);
  
  const { "type": type, "payload": payload } = event.data;
  
  switch (type) {
    case "USER_LOGGED_IN":
      updateUIForLoggedInUser(payload.user);
      break;
    case "USER_LOGGED_OUT":
      updateUIForLoggedOutUser();
      break;
    case "CART_UPDATED":
      updateCart(payload.items);
      break;
    case "THEME_CHANGED":
      applyTheme(payload.theme);
      break;
    default:
      console.log("Unknown message type:", type);
  }
};

// Error handling
channel.onerror = (event) => {
  console.error("BroadcastChannel error:", event);
};

// Send message to other tabs
function notifyOtherTabs(type, payload) {
  channel.postMessage({
    "type": type,
    "payload": payload,
    "timestamp": Date.now(),
    "tabId": sessionStorage.getItem("tabId")
  });
}

// Example usage
document.getElementById("loginBtn").addEventListener("click", () => {
  const user = { "id": "123", "name": "John Doe" };
  
  // Update current tab
  updateUIForLoggedInUser(user);
  
  // Notify other tabs
  notifyOtherTabs("USER_LOGGED_IN", { "user": user });
});

// Clean up when page unloads
window.addEventListener("beforeunload", () => {
  channel.close();
});

Example: Real-time notification sync across tabs

// Notification synchronization across tabs
class TabNotificationSync {
  constructor() {
    this.channel = new BroadcastChannel("notifications");
    this.notifications = [];
    
    this.channel.onmessage = this.handleMessage.bind(this);
  }
  
  handleMessage(event) {
    const { "action": action, "notification": notification } = event.data;
    
    switch (action) {
      case "ADD":
        this.addNotification(notification, false); // false = don't broadcast again
        break;
      case "REMOVE":
        this.removeNotification(notification.id, false);
        break;
      case "CLEAR_ALL":
        this.clearAll(false);
        break;
      case "MARK_READ":
        this.markAsRead(notification.id, false);
        break;
    }
  }
  
  addNotification(notification, broadcast = true) {
    this.notifications.push(notification);
    this.updateUI();
    
    if (broadcast) {
      this.channel.postMessage({
        "action": "ADD",
        "notification": notification
      });
    }
  }
  
  removeNotification(id, broadcast = true) {
    this.notifications = this.notifications.filter(n => n.id !== id);
    this.updateUI();
    
    if (broadcast) {
      this.channel.postMessage({
        "action": "REMOVE",
        "notification": { "id": id }
      });
    }
  }
  
  markAsRead(id, broadcast = true) {
    const notification = this.notifications.find(n => n.id === id);
    if (notification) {
      notification.read = true;
      this.updateUI();
      
      if (broadcast) {
        this.channel.postMessage({
          "action": "MARK_READ",
          "notification": { "id": id }
        });
      }
    }
  }
  
  clearAll(broadcast = true) {
    this.notifications = [];
    this.updateUI();
    
    if (broadcast) {
      this.channel.postMessage({ "action": "CLEAR_ALL" });
    }
  }
  
  updateUI() {
    const badge = document.getElementById("notification-badge");
    const unreadCount = this.notifications.filter(n => !n.read).length;
    badge.textContent = unreadCount || "";
    badge.style.display = unreadCount > 0 ? "block" : "none";
  }
  
  destroy() {
    this.channel.close();
  }
}

// Initialize
const notificationSync = new TabNotificationSync();

// Add notification
notificationSync.addNotification({
  "id": Date.now(),
  "message": "New message received",
  "read": false
});
Note: BroadcastChannel enables same-origin tab communication without a server. Messages are not persistent - lost if no tabs listening. Use for real-time sync (login state, cart, settings). Works across tabs, windows, iframes. Data must be structured-cloneable (no functions).

13.2 Web Share API for Native Sharing

Method/Property Description Browser Support
navigator.canShare(data) Returns boolean indicating if data can be shared. Check before calling share(). Modern Browsers
navigator.share(data) Opens native share dialog. Data object can include title, text, url, files. Modern Browsers
Share Data Property Type Description
title string Title for shared content. Optional.
text string Text/description to share. Optional.
url string URL to share. Optional.
files File[] Array of File objects to share. Optional. Check support with canShare().

Example: Share text and URL

// Check if Web Share API is supported
if (navigator.share) {
  console.log("Web Share API supported");
} else {
  console.log("Web Share API not supported - show fallback UI");
}

// Share content
async function shareContent() {
  // Must be called from user gesture (click, tap)
  const shareData = {
    "title": "Amazing Article",
    "text": "Check out this great article I found!",
    "url": "https://example.com/article/123"
  };
  
  try {
    await navigator.share(shareData);
    console.log("Content shared successfully");
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("User cancelled share");
    } else {
      console.error("Error sharing:", error);
      // Show fallback share options (copy link, social media buttons)
      showFallbackShareUI(shareData);
    }
  }
}

// Add event listener
document.getElementById("shareBtn").addEventListener("click", shareContent);

Example: Share files

// Share image file
async function shareImage() {
  try {
    // Get or create file
    const response = await fetch("/images/photo.jpg");
    const blob = await response.blob();
    const file = new File([blob], "photo.jpg", { "type": "image/jpeg" });
    
    // Check if files can be shared
    const shareData = {
      "files": [file],
      "title": "Beautiful Photo",
      "text": "Check out this photo!"
    };
    
    if (!navigator.canShare) {
      throw new Error("Web Share API not supported");
    }
    
    if (!navigator.canShare(shareData)) {
      throw new Error("Cannot share this file type");
    }
    
    // Share
    await navigator.share(shareData);
    console.log("File shared successfully");
    
  } catch (error) {
    console.error("Error sharing file:", error);
    // Fallback: download file or copy link
    showDownloadOption(file);
  }
}

// Share multiple files
async function shareMultipleFiles(files) {
  const shareData = {
    "files": files,
    "title": "My Files"
  };
  
  if (navigator.canShare && navigator.canShare(shareData)) {
    try {
      await navigator.share(shareData);
      console.log("Files shared");
    } catch (error) {
      console.error("Share failed:", error);
    }
  } else {
    console.log("Cannot share files - browser limitation");
    // Fallback to individual downloads
  }
}

// Share canvas as image
async function shareCanvas(canvas) {
  canvas.toBlob(async (blob) => {
    const file = new File([blob], "drawing.png", { "type": "image/png" });
    
    if (navigator.canShare && navigator.canShare({ "files": [file] })) {
      await navigator.share({
        "files": [file],
        "title": "My Drawing"
      });
    }
  });
}
Note: Web Share API provides native sharing - opens OS share sheet on mobile. Must be triggered by user gesture (click/tap). Only works on HTTPS. Always provide fallback for unsupported browsers. File sharing support varies by platform.
Warning: Always check navigator.share availability before using. Use canShare() to verify data is shareable. User can cancel share - handle AbortError. Desktop browsers have limited support - mobile is primary use case. Provide copy-link fallback.

13.3 Web Share Target API for Receiving Shares

Manifest Property Description
share_target Defines how app receives shared content. Object with action, method, enctype, params.
share_target.action URL to handle share (e.g., /share). Relative to app scope.
share_target.method HTTP method: "GET" or "POST". Default: "GET".
share_target.enctype For POST: "application/x-www-form-urlencoded" or "multipart/form-data" (for files).
share_target.params Maps share data to query/form parameters: title, text, url, files.

Example: Web app manifest for share target

{
  "name": "My Sharing App",
  "short_name": "ShareApp",
  "start_url": "/",
  "display": "standalone",
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
        {
          "name": "media",
          "accept": ["image/*", "video/*", "audio/*"]
        }
      ]
    }
  }
}

Example: Handle shared content in service worker

// service-worker.js - Handle share target
self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);
  
  // Check if this is a share request
  if (url.pathname === "/share" && event.request.method === "POST") {
    event.respondWith(handleShare(event.request));
  }
});

async function handleShare(request) {
  try {
    const formData = await request.formData();
    
    const title = formData.get("title") || "";
    const text = formData.get("text") || "";
    const url = formData.get("url") || "";
    const mediaFiles = formData.getAll("media");
    
    // Store shared content
    const shareData = {
      "title": title,
      "text": text,
      "url": url,
      "files": [],
      "timestamp": Date.now()
    };
    
    // Process files
    for (const file of mediaFiles) {
      // Store file (could use IndexedDB, Cache API, etc.)
      const arrayBuffer = await file.arrayBuffer();
      shareData.files.push({
        "name": file.name,
        "type": file.type,
        "size": file.size,
        "data": arrayBuffer
      });
    }
    
    // Store in IndexedDB
    const db = await openDatabase();
    await storeSharedContent(db, shareData);
    
    // Redirect to app with share ID
    const shareId = Date.now();
    return Response.redirect(`/app?share=${shareId}`, 303);
    
  } catch (error) {
    console.error("Error handling share:", error);
    return new Response("Error processing shared content", { "status": 500 });
  }
}

Example: Process shared content in app

// app.js - Process shared content
async function checkForSharedContent() {
  const params = new URLSearchParams(window.location.search);
  const shareId = params.get("share");
  
  if (shareId) {
    try {
      // Retrieve shared content from IndexedDB
      const db = await openDatabase();
      const shareData = await getSharedContent(db, shareId);
      
      if (shareData) {
        console.log("Received shared content:", shareData);
        
        // Display shared content
        if (shareData.title) {
          document.getElementById("title").value = shareData.title;
        }
        if (shareData.text) {
          document.getElementById("content").value = shareData.text;
        }
        if (shareData.url) {
          document.getElementById("link").value = shareData.url;
        }
        if (shareData.files.length > 0) {
          displaySharedFiles(shareData.files);
        }
        
        // Clear URL parameter
        window.history.replaceState({}, "", "/app");
      }
    } catch (error) {
      console.error("Error loading shared content:", error);
    }
  }
}

// Run on page load
checkForSharedContent();
Note: Web Share Target API makes PWAs share targets - appear in OS share sheet. Requires web app manifest with share_target field. App must be installed to appear as share target. Use service worker to handle POST requests with files.
Warning: Only works for installed PWAs - not regular web pages. Limited browser support (mainly Chrome/Edge on Android). Files require POST with multipart/form-data. Test thoroughly on target platforms. Provide clear UI feedback for shared content.

13.4 MessageChannel and MessagePort APIs

Class/Method Description Browser Support
new MessageChannel() Creates channel with two MessagePort objects for bidirectional communication. All Browsers
channel.port1 First MessagePort of the channel. All Browsers
channel.port2 Second MessagePort of the channel. All Browsers
port.postMessage(data, [transfer]) Sends message through port. Optional transfer array for transferable objects. All Browsers
port.onmessage Event handler for messages. Port must be started via start() or setting onmessage. All Browsers
port.start() Starts port (enables message delivery). Called automatically when setting onmessage. All Browsers
port.close() Closes port and stops message delivery. All Browsers

Example: Communication between iframe and parent

// Parent page
const iframe = document.getElementById("myFrame");

// Create channel
const channel = new MessageChannel();

// Listen on port1
channel.port1.onmessage = (event) => {
  console.log("Parent received:", event.data);
  
  // Send response
  channel.port1.postMessage({
    "type": "RESPONSE",
    "data": "Hello from parent"
  });
};

// Transfer port2 to iframe
iframe.contentWindow.postMessage(
  { "type": "INIT", "message": "Here's your port" },
  "*",
  [channel.port2] // Transfer port ownership
);

// Send message to iframe
setTimeout(() => {
  channel.port1.postMessage({
    "type": "REQUEST",
    "data": "What's your status?"
  });
}, 1000);


// ===== iframe page =====
// Listen for port from parent
window.addEventListener("message", (event) => {
  if (event.data.type === "INIT") {
    // Get transferred port
    const port = event.ports[0];
    
    // Listen on port
    port.onmessage = (e) => {
      console.log("Iframe received:", e.data);
      
      if (e.data.type === "REQUEST") {
        // Send response
        port.postMessage({
          "type": "STATUS",
          "data": "All systems operational"
        });
      }
    };
    
    // Send initial message
    port.postMessage({
      "type": "READY",
      "data": "Iframe initialized"
    });
  }
});

Example: Worker communication with MessageChannel

// Main thread
const worker = new Worker("worker.js");
const channel = new MessageChannel();

// Listen on port1
channel.port1.onmessage = (event) => {
  console.log("Main thread received:", event.data);
  document.getElementById("result").textContent = event.data.result;
};

// Send port2 to worker
worker.postMessage(
  { "type": "INIT_PORT" },
  [channel.port2]
);

// Send work request via channel
function processData(data) {
  channel.port1.postMessage({
    "type": "PROCESS",
    "data": data
  });
}

document.getElementById("processBtn").addEventListener("click", () => {
  processData([1, 2, 3, 4, 5]);
});


// ===== worker.js =====
let port = null;

self.onmessage = (event) => {
  if (event.data.type === "INIT_PORT") {
    // Get transferred port
    port = event.ports[0];
    
    // Listen on port
    port.onmessage = (e) => {
      if (e.data.type === "PROCESS") {
        // Process data
        const result = e.data.data.reduce((sum, num) => sum + num, 0);
        
        // Send result back
        port.postMessage({
          "type": "RESULT",
          "result": result
        });
      }
    };
    
    // Notify ready
    port.postMessage({
      "type": "READY",
      "message": "Worker ready for processing"
    });
  }
};

Example: Dedicated communication channels

// Create multiple channels for different purposes
class ChannelManager {
  constructor() {
    this.channels = new Map();
  }
  
  createChannel(name) {
    const channel = new MessageChannel();
    this.channels.set(name, channel);
    return channel;
  }
  
  getChannel(name) {
    return this.channels.get(name);
  }
  
  sendMessage(channelName, portNumber, data) {
    const channel = this.channels.get(channelName);
    if (channel) {
      const port = portNumber === 1 ? channel.port1 : channel.port2;
      port.postMessage(data);
    }
  }
  
  cleanup() {
    this.channels.forEach(channel => {
      channel.port1.close();
      channel.port2.close();
    });
    this.channels.clear();
  }
}

// Usage
const manager = new ChannelManager();

// Create channels for different modules
const authChannel = manager.createChannel("auth");
const dataChannel = manager.createChannel("data");

// Setup listeners
authChannel.port1.onmessage = (event) => {
  console.log("Auth message:", event.data);
};

dataChannel.port1.onmessage = (event) => {
  console.log("Data message:", event.data);
};

// Transfer ports to worker
worker.postMessage(
  { "type": "SETUP" },
  [authChannel.port2, dataChannel.port2]
);

// Clean up when done
window.addEventListener("beforeunload", () => {
  manager.cleanup();
});
Note: MessageChannel creates dedicated communication pipe with two ports. Use for structured communication between contexts (workers, iframes). Ports are transferable - ownership can be transferred. More efficient than postMessage for ongoing communication.

13.5 Cross-Origin Communication Patterns

Method Description Use Case
window.postMessage(message, targetOrigin, [transfer]) Sends message to another window/iframe. Always specify targetOrigin for security. Cross-origin iframe communication
window.addEventListener("message", handler) Listens for postMessage events. Always validate event.origin. Receiving cross-origin messages
event.origin Origin of message sender. Validate before processing message. Security check
event.source Reference to window that sent message. Use to send response. Bidirectional communication
event.data Message data (structured clone). Payload
event.ports Array of transferred MessagePort objects. Channel establishment

Example: Secure cross-origin iframe communication

// Parent page (https://parent.com)
const iframe = document.getElementById("childFrame");
const ALLOWED_ORIGIN = "https://child.com";

// Listen for messages from iframe
window.addEventListener("message", (event) => {
  // CRITICAL: Always validate origin
  if (event.origin !== ALLOWED_ORIGIN) {
    console.warn("Message from unauthorized origin:", event.origin);
    return;
  }
  
  console.log("Received from iframe:", event.data);
  
  // Process message
  const { "type": type, "payload": payload } = event.data;
  
  switch (type) {
    case "REQUEST_USER_DATA":
      // Send user data to iframe
      event.source.postMessage({
        "type": "USER_DATA",
        "payload": {
          "userId": "123",
          "name": "John Doe"
        }
      }, ALLOWED_ORIGIN);
      break;
      
    case "HEIGHT_CHANGED":
      // Resize iframe
      iframe.style.height = payload.height + "px";
      break;
      
    default:
      console.log("Unknown message type:", type);
  }
});

// Send message to iframe
function sendToIframe(data) {
  iframe.contentWindow.postMessage(data, ALLOWED_ORIGIN);
}

// Wait for iframe to load
iframe.addEventListener("load", () => {
  sendToIframe({
    "type": "INIT",
    "payload": { "theme": "dark" }
  });
});


// ===== Iframe page (https://child.com) =====
const PARENT_ORIGIN = "https://parent.com";

// Listen for messages from parent
window.addEventListener("message", (event) => {
  // Validate origin
  if (event.origin !== PARENT_ORIGIN) {
    console.warn("Message from unauthorized origin:", event.origin);
    return;
  }
  
  const { "type": type, "payload": payload } = event.data;
  
  switch (type) {
    case "INIT":
      // Initialize with config from parent
      applyTheme(payload.theme);
      
      // Request user data
      window.parent.postMessage({
        "type": "REQUEST_USER_DATA"
      }, PARENT_ORIGIN);
      break;
      
    case "USER_DATA":
      // Use received data
      displayUserInfo(payload);
      break;
  }
});

// Notify parent of height changes
const resizeObserver = new ResizeObserver((entries) => {
  const height = entries[0].contentRect.height;
  window.parent.postMessage({
    "type": "HEIGHT_CHANGED",
    "payload": { "height": height }
  }, PARENT_ORIGIN);
});

resizeObserver.observe(document.body);

Example: Cross-origin authentication flow

// Main app - open OAuth popup
function initiateOAuth() {
  const authWindow = window.open(
    "https://auth-provider.com/oauth",
    "oauth",
    "width=600,height=700"
  );
  
  // Listen for auth result
  const messageHandler = (event) => {
    // Validate origin
    if (event.origin !== "https://auth-provider.com") {
      return;
    }
    
    if (event.data.type === "OAUTH_SUCCESS") {
      console.log("OAuth successful:", event.data.token);
      
      // Store token
      localStorage.setItem("authToken", event.data.token);
      
      // Close popup
      authWindow.close();
      
      // Clean up listener
      window.removeEventListener("message", messageHandler);
      
      // Update UI
      onAuthenticationSuccess(event.data.user);
    } else if (event.data.type === "OAUTH_ERROR") {
      console.error("OAuth failed:", event.data.error);
      authWindow.close();
      window.removeEventListener("message", messageHandler);
      onAuthenticationError(event.data.error);
    }
  };
  
  window.addEventListener("message", messageHandler);
}

// ===== OAuth provider page =====
function completeOAuth(token, user) {
  // Send result to opener
  if (window.opener) {
    window.opener.postMessage({
      "type": "OAUTH_SUCCESS",
      "token": token,
      "user": user
    }, "https://main-app.com"); // Specify target origin
    
    // Close self
    window.close();
  }
}

function failOAuth(error) {
  if (window.opener) {
    window.opener.postMessage({
      "type": "OAUTH_ERROR",
      "error": error
    }, "https://main-app.com");
    
    window.close();
  }
}
Note: postMessage enables secure cross-origin communication. Always specify targetOrigin when sending. Always validate event.origin when receiving. Use structured data with type field. Common for iframes, popups, worker communication.
Warning: NEVER use targetOrigin: "*" with sensitive data - major security risk. Always validate event.origin before processing messages. Don't trust message content - validate and sanitize. Be aware of timing attacks. Use MessageChannel for long-term communication.

13.6 Channel Messaging API for Worker Communication

Pattern Description Use Case
Worker postMessage Direct message to worker via worker.postMessage(). Simple but limited to one-way request-response. Simple worker tasks
MessageChannel with Workers Create dedicated channel for bidirectional communication. More structured than direct postMessage. Complex worker interactions
SharedWorker Communication Multiple tabs/windows connect to same worker. Each gets own MessagePort. Cross-tab shared state
BroadcastChannel in Worker Workers can use BroadcastChannel to communicate with main thread and other workers. Multi-worker coordination

Example: SharedWorker with MessagePort

// shared-worker.js
const connections = new Set();

self.addEventListener("connect", (event) => {
  const port = event.ports[0];
  connections.add(port);
  
  console.log("New connection. Total:", connections.size);
  
  port.addEventListener("message", (e) => {
    const { "type": type, "data": data } = e.data;
    
    switch (type) {
      case "BROADCAST":
        // Send to all connected ports except sender
        connections.forEach(p => {
          if (p !== port) {
            p.postMessage({
              "type": "MESSAGE",
              "data": data
            });
          }
        });
        break;
        
      case "GET_CONNECTION_COUNT":
        port.postMessage({
          "type": "CONNECTION_COUNT",
          "count": connections.size
        });
        break;
    }
  });
  
  port.start();
  
  // Handle disconnect
  port.addEventListener("close", () => {
    connections.delete(port);
    console.log("Connection closed. Remaining:", connections.size);
  });
});


// ===== Main thread usage =====
// Connect to shared worker
const worker = new SharedWorker("shared-worker.js");
const port = worker.port;

port.addEventListener("message", (event) => {
  console.log("Received from shared worker:", event.data);
  
  if (event.data.type === "MESSAGE") {
    displayMessage(event.data.data);
  }
});

port.start();

// Send broadcast message
function broadcastMessage(message) {
  port.postMessage({
    "type": "BROADCAST",
    "data": message
  });
}

// Get connection count
function getConnectionCount() {
  port.postMessage({ "type": "GET_CONNECTION_COUNT" });
}

Example: Multi-worker coordination

// Main thread - coordinate multiple workers
class WorkerPool {
  constructor(scriptUrl, size) {
    this.workers = [];
    this.channels = [];
    this.taskQueue = [];
    this.busyWorkers = new Set();
    
    // Create workers and channels
    for (let i = 0; i < size; i++) {
      const worker = new Worker(scriptUrl);
      const channel = new MessageChannel();
      
      // Setup worker communication
      worker.postMessage(
        { "type": "INIT", "workerId": i },
        [channel.port2]
      );
      
      // Listen on port1
      channel.port1.onmessage = (event) => {
        this.handleWorkerMessage(i, event.data);
      };
      
      this.workers.push(worker);
      this.channels.push(channel);
    }
  }
  
  handleWorkerMessage(workerId, data) {
    if (data.type === "TASK_COMPLETE") {
      console.log(`Worker ${workerId} completed task:`, data.result);
      
      // Mark worker as available
      this.busyWorkers.delete(workerId);
      
      // Process callback
      if (data.taskId && this.callbacks.has(data.taskId)) {
        this.callbacks.get(data.taskId)(data.result);
        this.callbacks.delete(data.taskId);
      }
      
      // Process next task
      this.processNextTask();
    }
  }
  
  async executeTask(taskData) {
    return new Promise((resolve) => {
      const taskId = Date.now() + Math.random();
      
      this.taskQueue.push({
        "id": taskId,
        "data": taskData,
        "callback": resolve
      });
      
      this.callbacks = this.callbacks || new Map();
      this.callbacks.set(taskId, resolve);
      
      this.processNextTask();
    });
  }
  
  processNextTask() {
    if (this.taskQueue.length === 0) return;
    
    // Find available worker
    const availableWorker = this.workers.findIndex(
      (_, index) => !this.busyWorkers.has(index)
    );
    
    if (availableWorker === -1) return; // All busy
    
    const task = this.taskQueue.shift();
    this.busyWorkers.add(availableWorker);
    
    // Send task to worker
    this.channels[availableWorker].port1.postMessage({
      "type": "TASK",
      "taskId": task.id,
      "data": task.data
    });
  }
  
  terminate() {
    this.workers.forEach(worker => worker.terminate());
    this.channels.forEach(channel => {
      channel.port1.close();
    });
  }
}

// Usage
const pool = new WorkerPool("task-worker.js", 4);

// Execute tasks
async function processManyTasks() {
  const tasks = Array.from({ "length": 100 }, (_, i) => ({
    "operation": "calculate",
    "value": i
  }));
  
  const results = await Promise.all(
    tasks.map(task => pool.executeTask(task))
  );
  
  console.log("All tasks completed:", results);
}

processManyTasks();
Note: Use MessageChannel for structured worker communication. SharedWorker enables cross-tab communication via MessagePorts. Each connection gets dedicated port. Good for coordinating work across tabs or managing worker pools.
Warning: SharedWorker has limited browser support (no Safari). Always call port.start() for SharedWorker ports. Clean up ports and workers when done to prevent memory leaks. Test worker communication error scenarios.

Communication and Sharing Best Practices

  • Use BroadcastChannel for simple same-origin tab sync - logout, cart updates
  • Web Share API must be triggered by user gesture - provide fallback
  • Check canShare() before attempting to share files
  • Web Share Target requires installed PWA - test on target platforms
  • MessageChannel more efficient than postMessage for ongoing communication
  • Always validate event.origin in postMessage handlers - critical security
  • Never use targetOrigin: "*" with sensitive data
  • SharedWorker enables cross-tab state but limited browser support
  • Use structured message format with type field for all communication
  • Call port.start() when using addEventListener instead of onmessage
  • Clean up channels, ports, and listeners to prevent memory leaks
  • Provide clear error handling for communication failures

14. Notification and Messaging APIs

14.1 Notification API for Desktop Notifications

Method/Property Description Browser Support
Notification.permission Current permission state: "granted", "denied", or "default". All Modern Browsers
Notification.requestPermission() Requests notification permission. Returns Promise resolving to permission state. All Modern Browsers
new Notification(title, options) Creates and displays notification. Options include body, icon, badge, tag, etc. All Modern Browsers
notification.close() Closes notification programmatically. All Modern Browsers
Notification Option Type Description
body string Notification body text.
icon string URL to icon image. Typically 192x192px.
badge string URL to badge icon for notification area. Monochrome, 96x96px.
tag string ID for notification grouping. Same tag replaces previous notification.
data any Arbitrary data to associate with notification. Accessible in event handlers.
requireInteraction boolean If true, notification persists until user interacts. Default: false.
actions array Array of action objects with action, title, icon. Service Worker only.
silent boolean If true, no sound/vibration. Default: false.
vibrate array Vibration pattern: [200, 100, 200] (vibrate 200ms, pause 100ms, vibrate 200ms).

Example: Request permission and show notification

// Check if notifications are supported
if (!("Notification" in window)) {
  console.log("Notifications not supported");
} else {
  console.log("Current permission:", Notification.permission);
}

// Request permission
async function requestNotificationPermission() {
  if (Notification.permission === "granted") {
    console.log("Permission already granted");
    return true;
  }
  
  if (Notification.permission === "denied") {
    console.log("Permission denied - cannot request again");
    return false;
  }
  
  // Request permission (must be from user gesture)
  const permission = await Notification.requestPermission();
  
  if (permission === "granted") {
    console.log("Permission granted!");
    return true;
  } else {
    console.log("Permission denied");
    return false;
  }
}

// Show notification
function showNotification() {
  if (Notification.permission !== "granted") {
    console.log("No permission to show notifications");
    return;
  }
  
  const notification = new Notification("New Message", {
    "body": "You have a new message from John",
    "icon": "/images/icon-192.png",
    "badge": "/images/badge-96.png",
    "tag": "message-123", // Replace previous notification with same tag
    "data": {
      "messageId": "123",
      "userId": "456"
    },
    "requireInteraction": false,
    "silent": false,
    "vibrate": [200, 100, 200]
  });
  
  // Event handlers
  notification.onclick = (event) => {
    console.log("Notification clicked:", event);
    window.focus();
    notification.close();
    
    // Navigate to message
    const { "messageId": messageId } = notification.data;
    window.location.href = `/messages/${messageId}`;
  };
  
  notification.onclose = (event) => {
    console.log("Notification closed");
  };
  
  notification.onerror = (event) => {
    console.error("Notification error:", event);
  };
  
  notification.onshow = (event) => {
    console.log("Notification shown");
  };
  
  // Auto-close after 5 seconds
  setTimeout(() => {
    notification.close();
  }, 5000);
}

// Usage - must be from user interaction
document.getElementById("notifyBtn").addEventListener("click", async () => {
  const hasPermission = await requestNotificationPermission();
  if (hasPermission) {
    showNotification();
  }
});

Example: Notification with actions (Service Worker)

// service-worker.js - Show notification with actions
self.addEventListener("push", (event) => {
  const data = event.data.json();
  
  const options = {
    "body": data.body,
    "icon": "/images/icon-192.png",
    "badge": "/images/badge-96.png",
    "tag": data.tag,
    "data": data,
    "actions": [
      {
        "action": "view",
        "title": "View",
        "icon": "/images/view-icon.png"
      },
      {
        "action": "dismiss",
        "title": "Dismiss",
        "icon": "/images/dismiss-icon.png"
      }
    ],
    "requireInteraction": true
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

// Handle notification click
self.addEventListener("notificationclick", (event) => {
  console.log("Notification clicked:", event.action);
  
  event.notification.close();
  
  if (event.action === "view") {
    // Open app to specific page
    event.waitUntil(
      clients.openWindow(`/app?id=${event.notification.data.id}`)
    );
  } else if (event.action === "dismiss") {
    // Just close - no action needed
    console.log("Notification dismissed");
  } else {
    // Default click (no action button)
    event.waitUntil(
      clients.openWindow("/app")
    );
  }
});

// Handle notification close
self.addEventListener("notificationclose", (event) => {
  console.log("Notification closed:", event.notification.tag);
  
  // Track analytics
  event.waitUntil(
    fetch("/analytics/notification-closed", {
      "method": "POST",
      "body": JSON.stringify({
        "tag": event.notification.tag,
        "timestamp": Date.now()
      })
    })
  );
});
Note: Notification API shows desktop notifications to users. Requires user permission - request from user gesture. Use tag to replace/update existing notifications. Service Worker notifications support action buttons. Always handle permission denial gracefully.
Warning: Must request permission from user gesture (click). Permission is per-origin. If denied, cannot request again - user must manually enable. Don't spam notifications. Respect requireInteraction - don't force persistent notifications. Test across browsers - behavior varies.

14.2 Push Messaging Registration and Handling

Method/Property Description Browser Support
registration.pushManager.subscribe(options) Subscribes to push notifications. Returns PushSubscription with endpoint and keys. All Modern Browsers
registration.pushManager.getSubscription() Gets existing push subscription or null if not subscribed. All Modern Browsers
subscription.unsubscribe() Unsubscribes from push notifications. All Modern Browsers
subscription.endpoint Push service URL for sending notifications. All Modern Browsers
subscription.getKey(name) Gets encryption key. Name: "p256dh" (public key) or "auth" (auth secret). All Modern Browsers

Example: Subscribe to push notifications

// Subscribe to push notifications
async function subscribeToPush() {
  try {
    // Check support
    if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
      throw new Error("Push notifications not supported");
    }
    
    // Register service worker
    const registration = await navigator.serviceWorker.register("/sw.js");
    await navigator.serviceWorker.ready;
    
    // Check existing subscription
    let subscription = await registration.pushManager.getSubscription();
    
    if (subscription) {
      console.log("Already subscribed:", subscription);
      return subscription;
    }
    
    // Request notification permission
    const permission = await Notification.requestPermission();
    if (permission !== "granted") {
      throw new Error("Notification permission denied");
    }
    
    // Subscribe to push
    // VAPID public key from your server
    const vapidPublicKey = "YOUR_VAPID_PUBLIC_KEY";
    const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
    
    subscription = await registration.pushManager.subscribe({
      "userVisibleOnly": true, // Must be true
      "applicationServerKey": convertedVapidKey
    });
    
    console.log("Push subscription:", subscription);
    
    // Send subscription to server
    await fetch("/api/push/subscribe", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "endpoint": subscription.endpoint,
        "keys": {
          "p256dh": arrayBufferToBase64(subscription.getKey("p256dh")),
          "auth": arrayBufferToBase64(subscription.getKey("auth"))
        }
      })
    });
    
    console.log("Subscription sent to server");
    return subscription;
    
  } catch (error) {
    console.error("Push subscription failed:", error);
    throw error;
  }
}

// Helper: Convert VAPID key
function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, "+")
    .replace(/_/g, "/");
  
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

// Helper: Convert ArrayBuffer to base64
function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = "";
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

// Unsubscribe
async function unsubscribeFromPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();
  
  if (subscription) {
    await subscription.unsubscribe();
    console.log("Unsubscribed from push");
    
    // Notify server
    await fetch("/api/push/unsubscribe", {
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body": JSON.stringify({
        "endpoint": subscription.endpoint
      })
    });
  }
}

Example: Handle push messages in service worker

// service-worker.js
self.addEventListener("push", (event) => {
  console.log("Push received");
  
  let notificationData = {
    "title": "New Notification",
    "body": "You have a new update",
    "icon": "/images/icon-192.png"
  };
  
  // Parse push data if available
  if (event.data) {
    try {
      notificationData = event.data.json();
    } catch (e) {
      notificationData.body = event.data.text();
    }
  }
  
  const options = {
    "body": notificationData.body,
    "icon": notificationData.icon || "/images/icon-192.png",
    "badge": "/images/badge-96.png",
    "tag": notificationData.tag || "default",
    "data": notificationData.data || {},
    "actions": notificationData.actions || [],
    "requireInteraction": notificationData.requireInteraction || false,
    "vibrate": [200, 100, 200]
  };
  
  // Show notification
  event.waitUntil(
    self.registration.showNotification(notificationData.title, options)
  );
});

// Handle notification click
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  
  const urlToOpen = event.notification.data.url || "/";
  
  event.waitUntil(
    clients.matchAll({
      "type": "window",
      "includeUncontrolled": true
    }).then((clientList) => {
      // Check if already open
      for (const client of clientList) {
        if (client.url === urlToOpen && "focus" in client) {
          return client.focus();
        }
      }
      
      // Open new window
      if (clients.openWindow) {
        return clients.openWindow(urlToOpen);
      }
    })
  );
});

// Handle push subscription change
self.addEventListener("pushsubscriptionchange", (event) => {
  console.log("Push subscription changed");
  
  event.waitUntil(
    // Resubscribe
    self.registration.pushManager.subscribe({
      "userVisibleOnly": true,
      "applicationServerKey": urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
    }).then((subscription) => {
      // Send new subscription to server
      return fetch("/api/push/subscribe", {
        "method": "POST",
        "headers": { "Content-Type": "application/json" },
        "body": JSON.stringify({
          "endpoint": subscription.endpoint,
          "keys": {
            "p256dh": arrayBufferToBase64(subscription.getKey("p256dh")),
            "auth": arrayBufferToBase64(subscription.getKey("auth"))
          }
        })
      });
    })
  );
});
Note: Push API enables server-to-client push notifications. Requires Service Worker and notification permission. Use VAPID keys for authentication. Subscription includes endpoint and encryption keys - send to your server. Server uses Web Push protocol to send notifications.
Warning: Requires HTTPS and Service Worker. Must set userVisibleOnly: true - silent push not allowed. Handle pushsubscriptionchange event - subscriptions can expire. Don't send sensitive data in push payload - encrypt or fetch from server. Test subscription renewal flow.

14.3 Badge API for Application Badge Updates

Method Description Browser Support
navigator.setAppBadge(count) Sets app icon badge count. Omit count for generic badge indicator. Chrome, Edge, Safari
navigator.clearAppBadge() Clears app icon badge. Chrome, Edge, Safari

Example: Update app badge

// Check if Badge API is supported
if ("setAppBadge" in navigator) {
  console.log("Badge API supported");
} else {
  console.log("Badge API not supported");
}

// Set badge with count
async function updateBadge(count) {
  try {
    if ("setAppBadge" in navigator) {
      if (count > 0) {
        await navigator.setAppBadge(count);
        console.log(`Badge set to ${count}`);
      } else {
        await navigator.clearAppBadge();
        console.log("Badge cleared");
      }
    }
  } catch (error) {
    console.error("Failed to update badge:", error);
  }
}

// Set generic badge (no number)
async function setGenericBadge() {
  try {
    if ("setAppBadge" in navigator) {
      await navigator.setAppBadge();
      console.log("Generic badge set");
    }
  } catch (error) {
    console.error("Failed to set badge:", error);
  }
}

// Clear badge
async function clearBadge() {
  try {
    if ("clearAppBadge" in navigator) {
      await navigator.clearAppBadge();
      console.log("Badge cleared");
    }
  } catch (error) {
    console.error("Failed to clear badge:", error);
  }
}

// Example: Update badge based on unread messages
let unreadCount = 0;

function receiveMessage(message) {
  unreadCount++;
  updateBadge(unreadCount);
  displayMessage(message);
}

function markAsRead() {
  unreadCount = Math.max(0, unreadCount - 1);
  updateBadge(unreadCount);
}

function markAllAsRead() {
  unreadCount = 0;
  clearBadge();
}

Example: Badge API in Service Worker

// service-worker.js - Update badge on push
self.addEventListener("push", (event) => {
  const data = event.data.json();
  
  // Show notification
  const notificationPromise = self.registration.showNotification(
    data.title,
    {
      "body": data.body,
      "icon": "/images/icon-192.png",
      "badge": "/images/badge-96.png",
      "tag": data.tag,
      "data": data
    }
  );
  
  // Update badge
  const badgePromise = (async () => {
    if ("setAppBadge" in navigator) {
      // Get current unread count from data
      const unreadCount = data.unreadCount || 1;
      await navigator.setAppBadge(unreadCount);
    }
  })();
  
  event.waitUntil(
    Promise.all([notificationPromise, badgePromise])
  );
});

// Clear badge when notification clicked
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  
  const clearBadgePromise = (async () => {
    if ("clearAppBadge" in navigator) {
      await navigator.clearAppBadge();
    }
  })();
  
  const openWindowPromise = clients.openWindow(
    event.notification.data.url || "/"
  );
  
  event.waitUntil(
    Promise.all([clearBadgePromise, openWindowPromise])
  );
});
Note: Badge API sets app icon badge - visible on dock/taskbar/home screen. Works for installed PWAs. Set number for count or no argument for generic indicator. Clear when user views content. Limited browser support but good progressive enhancement.
Warning: Only works for installed PWAs - not regular websites. Limited browser support (Chrome, Edge, Safari). Always check feature support before using. Badge persists across sessions - clear appropriately. Don't use for critical notifications - combine with Notification API.

14.4 Wake Lock API for Screen Wake Management

Method/Property Description Browser Support
navigator.wakeLock.request(type) Requests wake lock. Type: "screen". Returns WakeLockSentinel. Chrome, Edge
wakeLock.release() Releases wake lock, allowing screen to sleep. Chrome, Edge
wakeLock.released Promise that resolves when wake lock is released. Chrome, Edge
wakeLock.type Type of wake lock ("screen"). Chrome, Edge

Example: Prevent screen sleep during video playback

// Wake Lock manager
class WakeLockManager {
  constructor() {
    this.wakeLock = null;
    this.isSupported = "wakeLock" in navigator;
  }
  
  async request() {
    if (!this.isSupported) {
      console.log("Wake Lock API not supported");
      return false;
    }
    
    try {
      this.wakeLock = await navigator.wakeLock.request("screen");
      console.log("Wake lock acquired");
      
      // Listen for release
      this.wakeLock.addEventListener("release", () => {
        console.log("Wake lock released");
        this.wakeLock = null;
      });
      
      return true;
    } catch (error) {
      console.error("Wake lock request failed:", error);
      return false;
    }
  }
  
  async release() {
    if (this.wakeLock) {
      await this.wakeLock.release();
      this.wakeLock = null;
      console.log("Wake lock manually released");
    }
  }
  
  async reacquire() {
    // Reacquire if previously active (e.g., after page visibility change)
    if (this.wakeLock && this.wakeLock.released) {
      await this.request();
    }
  }
}

// Video player with wake lock
const wakeLockManager = new WakeLockManager();
const video = document.getElementById("myVideo");

video.addEventListener("play", async () => {
  await wakeLockManager.request();
});

video.addEventListener("pause", async () => {
  await wakeLockManager.release();
});

video.addEventListener("ended", async () => {
  await wakeLockManager.release();
});

// Reacquire wake lock when page becomes visible
document.addEventListener("visibilitychange", async () => {
  if (document.visibilityState === "visible" && !video.paused) {
    await wakeLockManager.reacquire();
  }
});

// Release on page unload
window.addEventListener("beforeunload", async () => {
  await wakeLockManager.release();
});

Example: Recipe app with wake lock

// Keep screen on while cooking
class RecipeWakeLock {
  constructor() {
    this.wakeLock = null;
    this.isActive = false;
  }
  
  async enable() {
    if ("wakeLock" in navigator) {
      try {
        this.wakeLock = await navigator.wakeLock.request("screen");
        this.isActive = true;
        
        console.log("Screen will stay on during cooking");
        
        // Update UI
        document.getElementById("wakeLockBtn").textContent = "Turn Off Keep Awake";
        document.getElementById("wakeLockStatus").textContent = "Screen will stay on";
        
        // Handle release
        this.wakeLock.addEventListener("release", () => {
          console.log("Wake lock released");
          this.isActive = false;
          this.updateUI();
        });
        
      } catch (error) {
        console.error("Failed to enable wake lock:", error);
        alert("Could not keep screen on. Make sure page is visible.");
      }
    } else {
      alert("Wake Lock not supported in this browser");
    }
  }
  
  async disable() {
    if (this.wakeLock) {
      await this.wakeLock.release();
      this.isActive = false;
      this.updateUI();
    }
  }
  
  async toggle() {
    if (this.isActive) {
      await this.disable();
    } else {
      await this.enable();
    }
  }
  
  updateUI() {
    const btn = document.getElementById("wakeLockBtn");
    const status = document.getElementById("wakeLockStatus");
    
    if (this.isActive) {
      btn.textContent = "Turn Off Keep Awake";
      status.textContent = "Screen will stay on";
    } else {
      btn.textContent = "Turn On Keep Awake";
      status.textContent = "Screen may sleep";
    }
  }
}

// Initialize
const recipeWakeLock = new RecipeWakeLock();

document.getElementById("wakeLockBtn").addEventListener("click", () => {
  recipeWakeLock.toggle();
});

// Auto-enable when starting recipe
document.getElementById("startCookingBtn").addEventListener("click", () => {
  recipeWakeLock.enable();
  startRecipeTimer();
});
Note: Wake Lock API prevents screen from sleeping. Use for video playback, reading, recipes, presentations, etc. Only "screen" type currently supported. Wake lock auto-releases when page hidden or battery low. Reacquire on visibility change if needed.
Warning: Limited browser support (Chrome, Edge). Requires visible page - released when tab hidden. Can be denied by browser (low battery, user settings). Always handle request failure gracefully. Remember to release when done - drains battery. Respect user's battery.

14.5 Idle Detection API for User Activity Monitoring

Method/Property Description Browser Support
new IdleDetector() Creates idle detector instance. Chrome, Edge (Experimental)
detector.start(options) Starts monitoring. Options: threshold (ms) and signal (AbortSignal). Chrome, Edge (Experimental)
detector.userState Current user state: "active" or "idle". Chrome, Edge (Experimental)
detector.screenState Current screen state: "locked" or "unlocked". Chrome, Edge (Experimental)
detector.onchange Event handler for state changes. Chrome, Edge (Experimental)

Example: Detect user idle state

// Check support and request permission
async function setupIdleDetection() {
  // Check support
  if (!("IdleDetector" in window)) {
    console.log("Idle Detection API not supported");
    return;
  }
  
  // Request permission
  try {
    const permission = await IdleDetector.requestPermission();
    
    if (permission !== "granted") {
      console.log("Idle detection permission denied");
      return;
    }
    
    console.log("Idle detection permission granted");
    startIdleDetection();
    
  } catch (error) {
    console.error("Permission request failed:", error);
  }
}

// Start idle detection
async function startIdleDetection() {
  try {
    const idleDetector = new IdleDetector();
    const controller = new AbortController();
    
    // Listen for state changes
    idleDetector.addEventListener("change", () => {
      const userState = idleDetector.userState;
      const screenState = idleDetector.screenState;
      
      console.log(`User: ${userState}, Screen: ${screenState}`);
      
      if (userState === "idle") {
        handleUserIdle();
      } else if (userState === "active") {
        handleUserActive();
      }
      
      if (screenState === "locked") {
        handleScreenLocked();
      } else if (screenState === "unlocked") {
        handleScreenUnlocked();
      }
    });
    
    // Start monitoring
    await idleDetector.start({
      "threshold": 60000, // 60 seconds
      "signal": controller.signal
    });
    
    console.log("Idle detection started (threshold: 60s)");
    
    // Stop detection after 10 minutes
    setTimeout(() => {
      controller.abort();
      console.log("Idle detection stopped");
    }, 10 * 60 * 1000);
    
  } catch (error) {
    console.error("Idle detection failed:", error);
  }
}

function handleUserIdle() {
  console.log("User is idle");
  // Pause background tasks, sync, etc.
  pauseBackgroundSync();
  showIdleMessage();
}

function handleUserActive() {
  console.log("User is active");
  // Resume activities
  resumeBackgroundSync();
  hideIdleMessage();
}

function handleScreenLocked() {
  console.log("Screen locked");
  // Pause video, save work, etc.
  pauseMedia();
  autoSaveWork();
}

function handleScreenUnlocked() {
  console.log("Screen unlocked");
  // Resume activities
  resumeMedia();
}

// Initialize
setupIdleDetection();

Example: Auto-logout on idle

// Auto-logout after extended idle period
class IdleLogoutManager {
  constructor(idleThreshold = 5 * 60 * 1000, logoutDelay = 60 * 1000) {
    this.idleThreshold = idleThreshold;
    this.logoutDelay = logoutDelay;
    this.idleDetector = null;
    this.logoutTimer = null;
    this.warningShown = false;
  }
  
  async start() {
    if (!("IdleDetector" in window)) {
      console.log("Using fallback idle detection");
      this.useFallbackDetection();
      return;
    }
    
    try {
      const permission = await IdleDetector.requestPermission();
      
      if (permission !== "granted") {
        this.useFallbackDetection();
        return;
      }
      
      this.idleDetector = new IdleDetector();
      
      this.idleDetector.addEventListener("change", () => {
        if (this.idleDetector.userState === "idle") {
          this.onIdle();
        } else {
          this.onActive();
        }
      });
      
      await this.idleDetector.start({
        "threshold": this.idleThreshold
      });
      
      console.log("Idle logout protection active");
      
    } catch (error) {
      console.error("Idle detection setup failed:", error);
      this.useFallbackDetection();
    }
  }
  
  onIdle() {
    console.log("User idle - starting logout countdown");
    
    // Show warning
    this.showWarning();
    
    // Start logout timer
    this.logoutTimer = setTimeout(() => {
      this.logout();
    }, this.logoutDelay);
  }
  
  onActive() {
    console.log("User active - cancelling logout");
    
    // Cancel logout
    if (this.logoutTimer) {
      clearTimeout(this.logoutTimer);
      this.logoutTimer = null;
    }
    
    // Hide warning
    this.hideWarning();
  }
  
  showWarning() {
    if (this.warningShown) return;
    
    this.warningShown = true;
    const warning = document.getElementById("idleWarning");
    warning.style.display = "block";
    warning.textContent = `You will be logged out in ${this.logoutDelay / 1000} seconds due to inactivity`;
  }
  
  hideWarning() {
    this.warningShown = false;
    const warning = document.getElementById("idleWarning");
    warning.style.display = "none";
  }
  
  logout() {
    console.log("Logging out due to inactivity");
    
    // Clear session
    localStorage.removeItem("authToken");
    sessionStorage.clear();
    
    // Redirect to login
    window.location.href = "/login?reason=idle";
  }
  
  useFallbackDetection() {
    // Fallback: use mouse/keyboard events
    let lastActivity = Date.now();
    
    const resetTimer = () => {
      lastActivity = Date.now();
      this.hideWarning();
    };
    
    ["mousedown", "mousemove", "keypress", "scroll", "touchstart"].forEach(event => {
      document.addEventListener(event, resetTimer, true);
    });
    
    // Check periodically
    setInterval(() => {
      const idleTime = Date.now() - lastActivity;
      
      if (idleTime > this.idleThreshold) {
        this.onIdle();
      }
    }, 5000);
  }
}

// Initialize
const idleLogout = new IdleLogoutManager(
  5 * 60 * 1000, // 5 minutes idle threshold
  60 * 1000      // 1 minute warning before logout
);

idleLogout.start();
Note: Idle Detection API monitors user activity and screen lock state. Requires permission. Use for auto-logout, pause sync, save battery. Threshold is minimum idle time before detection. Experimental API - limited support.
Warning: Highly experimental - Chrome/Edge only behind flag. Requires permission. Privacy-sensitive - use responsibly. Provide fallback for unsupported browsers. Don't rely solely on this for security - implement server-side session timeout. Test thoroughly before production use.

14.6 Picture-in-Picture API for Video Overlay

Method/Property Description Browser Support
video.requestPictureInPicture() Requests PiP mode for video element. Returns PictureInPictureWindow. All Modern Browsers
document.exitPictureInPicture() Exits PiP mode. All Modern Browsers
document.pictureInPictureElement Currently active PiP element or null. All Modern Browsers
document.pictureInPictureEnabled Boolean indicating if PiP is available. All Modern Browsers
pipWindow.width Width of PiP window. All Modern Browsers
pipWindow.height Height of PiP window. All Modern Browsers

Example: Toggle Picture-in-Picture

// Check PiP support
if (!document.pictureInPictureEnabled) {
  console.log("Picture-in-Picture not supported");
  document.getElementById("pipBtn").disabled = true;
}

// Toggle PiP mode
async function togglePictureInPicture() {
  const video = document.getElementById("myVideo");
  
  try {
    // Exit if already in PiP
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
      console.log("Exited Picture-in-Picture");
      return;
    }
    
    // Enter PiP
    const pipWindow = await video.requestPictureInPicture();
    console.log("Entered Picture-in-Picture");
    console.log(`PiP window size: ${pipWindow.width}x${pipWindow.height}`);
    
    // Listen for resize
    pipWindow.addEventListener("resize", () => {
      console.log(`PiP resized: ${pipWindow.width}x${pipWindow.height}`);
    });
    
  } catch (error) {
    console.error("PiP failed:", error);
    
    if (error.name === "NotAllowedError") {
      alert("Picture-in-Picture not allowed. Check browser permissions.");
    } else if (error.name === "InvalidStateError") {
      alert("Video must be playing to enable Picture-in-Picture.");
    }
  }
}

// Add button listener
document.getElementById("pipBtn").addEventListener("click", togglePictureInPicture);

// Listen for PiP events
const video = document.getElementById("myVideo");

video.addEventListener("enterpictureinpicture", (event) => {
  console.log("Entered PiP");
  document.getElementById("pipBtn").textContent = "Exit PiP";
  
  // Update UI
  document.body.classList.add("pip-active");
});

video.addEventListener("leavepictureinpicture", (event) => {
  console.log("Left PiP");
  document.getElementById("pipBtn").textContent = "Enter PiP";
  
  // Update UI
  document.body.classList.remove("pip-active");
});

Example: Auto PiP on scroll or tab switch

// Auto-enable PiP when video scrolls out of view or tab hidden
class AutoPiPManager {
  constructor(video) {
    this.video = video;
    this.autoPiPEnabled = true;
    this.observer = null;
    
    this.setupIntersectionObserver();
    this.setupVisibilityChange();
  }
  
  setupIntersectionObserver() {
    // Detect when video scrolls out of view
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (!this.autoPiPEnabled) return;
        
        // Video scrolled out of view and is playing
        if (!entry.isIntersecting && !this.video.paused) {
          this.enterPiP();
        }
        // Video back in view - exit PiP
        else if (entry.isIntersecting && document.pictureInPictureElement) {
          this.exitPiP();
        }
      });
    }, {
      "threshold": 0.5 // 50% visibility threshold
    });
    
    this.observer.observe(this.video);
  }
  
  setupVisibilityChange() {
    // Enable PiP when switching tabs
    document.addEventListener("visibilitychange", () => {
      if (!this.autoPiPEnabled) return;
      
      if (document.hidden && !this.video.paused) {
        this.enterPiP();
      } else if (!document.hidden && document.pictureInPictureElement) {
        this.exitPiP();
      }
    });
  }
  
  async enterPiP() {
    if (document.pictureInPictureElement) return;
    
    try {
      await this.video.requestPictureInPicture();
      console.log("Auto PiP enabled");
    } catch (error) {
      console.error("Auto PiP failed:", error);
    }
  }
  
  async exitPiP() {
    if (!document.pictureInPictureElement) return;
    
    try {
      await document.exitPictureInPicture();
      console.log("Auto PiP disabled");
    } catch (error) {
      console.error("Exit PiP failed:", error);
    }
  }
  
  setAutoPiP(enabled) {
    this.autoPiPEnabled = enabled;
    console.log(`Auto PiP ${enabled ? "enabled" : "disabled"}`);
    
    // Exit PiP if disabled
    if (!enabled && document.pictureInPictureElement) {
      this.exitPiP();
    }
  }
  
  destroy() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }
}

// Initialize
const video = document.getElementById("myVideo");
const autoPiP = new AutoPiPManager(video);

// Toggle auto PiP
document.getElementById("autoPiPToggle").addEventListener("change", (event) => {
  autoPiP.setAutoPiP(event.target.checked);
});

Example: Custom PiP controls

// Add custom controls to PiP window (Chrome 116+)
const video = document.getElementById("myVideo");

// Check if Media Session API is available for controls
if ("mediaSession" in navigator) {
  // Set metadata
  navigator.mediaSession.metadata = new MediaMetadata({
    "title": "My Video Title",
    "artist": "Content Creator",
    "album": "Video Series",
    "artwork": [
      { "src": "/images/artwork-96.png", "sizes": "96x96", "type": "image/png" },
      { "src": "/images/artwork-512.png", "sizes": "512x512", "type": "image/png" }
    ]
  });
  
  // Set action handlers (visible in PiP)
  navigator.mediaSession.setActionHandler("play", () => {
    video.play();
  });
  
  navigator.mediaSession.setActionHandler("pause", () => {
    video.pause();
  });
  
  navigator.mediaSession.setActionHandler("previoustrack", () => {
    playPreviousVideo();
  });
  
  navigator.mediaSession.setActionHandler("nexttrack", () => {
    playNextVideo();
  });
  
  navigator.mediaSession.setActionHandler("seekbackward", (details) => {
    video.currentTime = Math.max(video.currentTime - (details.seekOffset || 10), 0);
  });
  
  navigator.mediaSession.setActionHandler("seekforward", (details) => {
    video.currentTime = Math.min(
      video.currentTime + (details.seekOffset || 10),
      video.duration
    );
  });
}

// Update playback state
video.addEventListener("play", () => {
  navigator.mediaSession.playbackState = "playing";
});

video.addEventListener("pause", () => {
  navigator.mediaSession.playbackState = "paused";
});

// Update position
video.addEventListener("timeupdate", () => {
  if ("setPositionState" in navigator.mediaSession) {
    navigator.mediaSession.setPositionState({
      "duration": video.duration,
      "playbackRate": video.playbackRate,
      "position": video.currentTime
    });
  }
});
Note: Picture-in-Picture API enables floating video overlay. Video continues playing while user browses other tabs/apps. Must be triggered by user interaction. Use for video conferencing, tutorials, live streams. Combine with Media Session API for custom controls in PiP window.
Warning: Requires user interaction to trigger (security). Some browsers require video to be playing. Can be disabled by browser or user preferences. Always check document.pictureInPictureEnabled. Handle enter/leave events for UI updates. Only one PiP window at a time.

Notification and Messaging Best Practices

  • Always request notification permission from user gesture - explain why first
  • Use notification tag to replace/update instead of spamming
  • Push notifications require Service Worker and VAPID keys
  • Handle pushsubscriptionchange - subscriptions can expire
  • Badge API only works for installed PWAs - check support
  • Clear app badge when user views content - don't leave stale counts
  • Wake Lock API requires visible page - reacquire after visibility change
  • Release wake lock when done - respects user's battery
  • Idle Detection API is experimental - provide fallback detection
  • Use idle detection for auto-logout, pause sync, save battery
  • PiP requires user interaction to trigger - can't auto-enable on page load
  • Combine PiP with Media Session API for rich controls
  • Handle all notification/PiP events for proper UI state management
  • Test permission flows - handle grant, deny, and "ask later" states

15. Modern Web Platform APIs

15.1 Web Streams API for Streaming Data Processing

Stream Type Description Browser Support
ReadableStream Stream of data that can be read chunk by chunk. Used for response bodies, file reading, etc. All Modern Browsers
WritableStream Stream that can be written to chunk by chunk. Used for file writing, compression, etc. All Modern Browsers
TransformStream Pair of readable and writable streams that transform data. Used for compression, encryption, etc. All Modern Browsers
ReadableStream Method Description
stream.getReader() Gets ReadableStreamDefaultReader for reading chunks.
reader.read() Returns Promise with {value, done}. Done is true when stream ends.
stream.pipeThrough(transform) Pipes stream through TransformStream, returning new ReadableStream.
stream.pipeTo(writable) Pipes stream to WritableStream. Returns Promise.
stream.tee() Splits stream into two independent streams.
reader.cancel(reason) Cancels stream reading.

Example: Read and process fetch response stream

// Fetch and process response as stream
async function fetchAndProcessStream(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    
    // Get readable stream from response
    const reader = response.body.getReader();
    
    // Process chunks as they arrive
    let receivedLength = 0;
    const chunks = [];
    
    while (true) {
      const { "done": done, "value": value } = await reader.read();
      
      if (done) {
        console.log("Stream complete");
        break;
      }
      
      chunks.push(value);
      receivedLength += value.length;
      
      // Update progress
      console.log(`Received ${receivedLength} bytes`);
      updateProgressBar(receivedLength);
    }
    
    // Concatenate chunks
    const chunksAll = new Uint8Array(receivedLength);
    let position = 0;
    for (const chunk of chunks) {
      chunksAll.set(chunk, position);
      position += chunk.length;
    }
    
    // Convert to string
    const result = new TextDecoder("utf-8").decode(chunksAll);
    console.log("Final result:", result);
    
    return result;
    
  } catch (error) {
    console.error("Stream processing failed:", error);
    throw error;
  }
}

// Usage
fetchAndProcessStream("/api/large-data")
  .then(data => {
    console.log("Processing complete");
    processData(data);
  });

Example: Create custom ReadableStream

// Create custom stream that generates data
function createNumberStream(max) {
  let current = 0;
  
  return new ReadableStream({
    start(controller) {
      console.log("Stream started");
    },
    
    pull(controller) {
      if (current < max) {
        // Enqueue next value
        controller.enqueue(current);
        current++;
      } else {
        // Close stream when done
        controller.close();
      }
    },
    
    cancel(reason) {
      console.log("Stream cancelled:", reason);
    }
  });
}

// Read from custom stream
async function readNumberStream() {
  const stream = createNumberStream(10);
  const reader = stream.getReader();
  
  try {
    while (true) {
      const { "done": done, "value": value } = await reader.read();
      
      if (done) {
        console.log("Stream finished");
        break;
      }
      
      console.log("Received number:", value);
    }
  } finally {
    reader.releaseLock();
  }
}

readNumberStream();

Example: Transform stream for data processing

// Create transform stream that uppercases text
class UppercaseTransformStream {
  constructor() {
    return new TransformStream({
      transform(chunk, controller) {
        // Convert chunk to uppercase
        const text = new TextDecoder().decode(chunk);
        const upper = text.toUpperCase();
        const encoded = new TextEncoder().encode(upper);
        controller.enqueue(encoded);
      }
    });
  }
}

// Use transform stream
async function transformFetchResponse(url) {
  const response = await fetch(url);
  
  // Pipe through transform
  const transformed = response.body
    .pipeThrough(new UppercaseTransformStream());
  
  // Read result
  const reader = transformed.getReader();
  const chunks = [];
  
  while (true) {
    const { "done": done, "value": value } = await reader.read();
    if (done) break;
    chunks.push(value);
  }
  
  // Combine chunks
  const combined = new Uint8Array(
    chunks.reduce((acc, chunk) => acc + chunk.length, 0)
  );
  let position = 0;
  for (const chunk of chunks) {
    combined.set(chunk, position);
    position += chunk.length;
  }
  
  return new TextDecoder().decode(combined);
}

// Create JSON parsing transform
class JSONParseTransformStream {
  constructor() {
    let buffer = "";
    
    return new TransformStream({
      transform(chunk, controller) {
        const text = new TextDecoder().decode(chunk);
        buffer += text;
        
        // Try to parse complete JSON objects
        const lines = buffer.split("\n");
        buffer = lines.pop(); // Keep incomplete line in buffer
        
        for (const line of lines) {
          if (line.trim()) {
            try {
              const json = JSON.parse(line);
              controller.enqueue(json);
            } catch (e) {
              console.error("JSON parse error:", e);
            }
          }
        }
      },
      
      flush(controller) {
        // Process remaining buffer
        if (buffer.trim()) {
          try {
            const json = JSON.parse(buffer);
            controller.enqueue(json);
          } catch (e) {
            console.error("JSON parse error:", e);
          }
        }
      }
    });
  }
}

// Use JSON transform
async function streamJSONData(url) {
  const response = await fetch(url);
  const reader = response.body
    .pipeThrough(new JSONParseTransformStream())
    .getReader();
  
  while (true) {
    const { "done": done, "value": value } = await reader.read();
    if (done) break;
    
    console.log("Parsed JSON object:", value);
    processJSONObject(value);
  }
}
Note: Streams API enables processing data chunk by chunk without loading entire dataset into memory. Use ReadableStream for reading, WritableStream for writing, TransformStream for transforming. Great for large files, real-time data, progressive rendering. All fetch responses are ReadableStreams.

15.2 Compression Streams API for Data Compression

Class Description Browser Support
CompressionStream TransformStream that compresses data. Format: "gzip", "deflate", or "deflate-raw". Chrome, Edge, Safari
DecompressionStream TransformStream that decompresses data. Same formats as compression. Chrome, Edge, Safari

Example: Compress and decompress data

// Compress string data
async function compressData(text) {
  // Convert to stream
  const blob = new Blob([text]);
  const stream = blob.stream();
  
  // Compress using gzip
  const compressedStream = stream.pipeThrough(
    new CompressionStream("gzip")
  );
  
  // Convert to Blob
  const compressedBlob = await new Response(compressedStream).blob();
  
  console.log(`Original size: ${text.length} bytes`);
  console.log(`Compressed size: ${compressedBlob.size} bytes`);
  console.log(`Compression ratio: ${(compressedBlob.size / text.length * 100).toFixed(2)}%`);
  
  return compressedBlob;
}

// Decompress data
async function decompressData(compressedBlob) {
  // Get stream from blob
  const stream = compressedBlob.stream();
  
  // Decompress
  const decompressedStream = stream.pipeThrough(
    new DecompressionStream("gzip")
  );
  
  // Convert to text
  const decompressedBlob = await new Response(decompressedStream).blob();
  const text = await decompressedBlob.text();
  
  return text;
}

// Usage
const originalText = "Lorem ipsum dolor sit amet...".repeat(100);

compressData(originalText)
  .then(compressed => {
    console.log("Compressed successfully");
    return decompressData(compressed);
  })
  .then(decompressed => {
    console.log("Decompressed successfully");
    console.log("Match:", decompressed === originalText);
  });

Example: Compress file before upload

// Compress file before upload
async function uploadCompressedFile(file) {
  try {
    // Compress file
    const compressedStream = file.stream()
      .pipeThrough(new CompressionStream("gzip"));
    
    // Create compressed blob
    const compressedBlob = await new Response(compressedStream).blob();
    
    console.log(`Original: ${file.size} bytes`);
    console.log(`Compressed: ${compressedBlob.size} bytes`);
    console.log(`Saved: ${file.size - compressedBlob.size} bytes`);
    
    // Create FormData
    const formData = new FormData();
    formData.append("file", compressedBlob, file.name + ".gz");
    formData.append("originalSize", file.size);
    formData.append("compression", "gzip");
    
    // Upload
    const response = await fetch("/api/upload", {
      "method": "POST",
      "body": formData
    });
    
    if (!response.ok) {
      throw new Error(`Upload failed: ${response.status}`);
    }
    
    const result = await response.json();
    console.log("Upload successful:", result);
    
    return result;
    
  } catch (error) {
    console.error("Upload failed:", error);
    throw error;
  }
}

// Handle file input
document.getElementById("fileInput").addEventListener("change", async (event) => {
  const file = event.target.files[0];
  if (!file) return;
  
  console.log("Compressing and uploading...");
  await uploadCompressedFile(file);
});

Example: Download and decompress on the fly

// Fetch compressed data and decompress while downloading
async function fetchAndDecompressStream(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    
    // Check if content is compressed
    const contentEncoding = response.headers.get("Content-Encoding");
    console.log("Content-Encoding:", contentEncoding);
    
    // Decompress stream
    const decompressedStream = response.body
      .pipeThrough(new DecompressionStream("gzip"));
    
    // Process decompressed data
    const reader = decompressedStream.getReader();
    const decoder = new TextDecoder();
    let result = "";
    
    while (true) {
      const { "done": done, "value": value } = await reader.read();
      
      if (done) {
        console.log("Decompression complete");
        break;
      }
      
      // Decode chunk
      const chunk = decoder.decode(value, { "stream": true });
      result += chunk;
      
      // Process chunk progressively
      console.log(`Processed ${result.length} characters`);
    }
    
    return result;
    
  } catch (error) {
    console.error("Fetch and decompress failed:", error);
    throw error;
  }
}

// Usage
fetchAndDecompressStream("/api/data.gz")
  .then(data => {
    console.log("Data loaded:", data.substring(0, 100));
    processData(data);
  });
Note: Compression Streams API provides built-in compression/decompression. Supports gzip, deflate, and deflate-raw. Use for reducing upload/download sizes, compressing cached data, file compression. Works with any stream - files, fetch responses, etc. No external libraries needed.
Warning: Limited browser support - check before using. Compression is CPU-intensive - may block on large data. Consider Web Workers for large compressions. Server must support receiving compressed data. Always measure - small data may not benefit from compression overhead.

15.3 Web Locks API for Resource Synchronization

Method/Property Description Browser Support
navigator.locks.request(name, callback) Requests lock with name. Callback executes when lock acquired. Returns Promise. Chrome, Edge
navigator.locks.request(name, options, callback) Request with options: mode ("exclusive"/"shared"), ifAvailable, steal, signal. Chrome, Edge
navigator.locks.query() Returns Promise with current lock state (held, pending locks). Chrome, Edge
Lock Option Type Description
mode string "exclusive" (default) or "shared". Exclusive allows one holder, shared allows multiple.
ifAvailable boolean If true, callback runs only if lock immediately available. Otherwise callback receives null.
steal boolean If true, preempts existing lock. Dangerous - use with caution.
signal AbortSignal AbortSignal to cancel lock request.

Example: Exclusive lock for critical section

// Protect critical section with exclusive lock
async function updateSharedResource(data) {
  if (!("locks" in navigator)) {
    console.log("Web Locks API not supported");
    return updateSharedResourceFallback(data);
  }
  
  try {
    await navigator.locks.request("resource-lock", async (lock) => {
      console.log("Lock acquired");
      
      // Critical section - only one execution at a time
      const current = await fetchCurrentData();
      const updated = processData(current, data);
      await saveData(updated);
      
      console.log("Resource updated successfully");
      
      // Lock released when callback completes
    });
    
    console.log("Lock released");
    
  } catch (error) {
    console.error("Lock request failed:", error);
    throw error;
  }
}

// Multiple concurrent calls will be serialized
Promise.all([
  updateSharedResource({ "id": 1 }),
  updateSharedResource({ "id": 2 }),
  updateSharedResource({ "id": 3 })
]).then(() => {
  console.log("All updates complete");
});

Example: Shared locks for read operations

// Use shared lock for concurrent reads, exclusive for writes
class SharedResource {
  constructor(resourceName) {
    this.resourceName = resourceName;
    this.lockName = `lock:${resourceName}`;
  }
  
  async read() {
    // Shared lock - multiple readers allowed
    return await navigator.locks.request(
      this.lockName,
      { "mode": "shared" },
      async (lock) => {
        console.log("Read lock acquired");
        const data = await fetchData(this.resourceName);
        console.log("Read complete");
        return data;
      }
    );
  }
  
  async write(data) {
    // Exclusive lock - blocks all readers and writers
    return await navigator.locks.request(
      this.lockName,
      { "mode": "exclusive" },
      async (lock) => {
        console.log("Write lock acquired");
        await saveData(this.resourceName, data);
        console.log("Write complete");
      }
    );
  }
  
  async tryRead() {
    // Try to read without waiting
    return await navigator.locks.request(
      this.lockName,
      { "mode": "shared", "ifAvailable": true },
      async (lock) => {
        if (!lock) {
          console.log("Lock not available");
          return null;
        }
        
        console.log("Lock acquired immediately");
        return await fetchData(this.resourceName);
      }
    );
  }
}

// Usage
const resource = new SharedResource("user-data");

// Concurrent reads OK
Promise.all([
  resource.read(),
  resource.read(),
  resource.read()
]).then(() => {
  console.log("All reads complete");
});

// Write blocks all operations
resource.write({ "updated": true })
  .then(() => console.log("Write complete"));

Example: Lock with timeout using AbortController

// Request lock with timeout
async function requestLockWithTimeout(lockName, callback, timeout = 5000) {
  const controller = new AbortController();
  
  // Set timeout
  const timeoutId = setTimeout(() => {
    controller.abort();
    console.log("Lock request timed out");
  }, timeout);
  
  try {
    await navigator.locks.request(
      lockName,
      { "signal": controller.signal },
      async (lock) => {
        clearTimeout(timeoutId);
        console.log("Lock acquired within timeout");
        await callback(lock);
      }
    );
  } catch (error) {
    if (error.name === "AbortError") {
      console.error("Lock request aborted (timeout or manual)");
    } else {
      console.error("Lock request failed:", error);
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Query lock state
async function queryLockState() {
  const state = await navigator.locks.query();
  
  console.log("Held locks:", state.held);
  console.log("Pending locks:", state.pending);
  
  // Check specific lock
  const myLocks = state.held.filter(lock => lock.name === "my-lock");
  console.log("My locks:", myLocks);
  
  return state;
}

// Usage
requestLockWithTimeout(
  "timeout-lock",
  async (lock) => {
    await performOperation();
  },
  3000 // 3 second timeout
).catch(error => {
  console.error("Operation failed:", error);
});
Note: Web Locks API provides cross-tab/worker resource synchronization. Use exclusive locks for writes, shared locks for reads. Lock held until callback Promise resolves. Prevents race conditions in IndexedDB, shared state, etc. Works across tabs, windows, workers.
Warning: Limited browser support (Chrome, Edge). Locks are NOT persistent - lost on page refresh. Don't use for long-running operations - blocks other contexts. Always use timeout with AbortController. Avoid deadlocks - don't request multiple locks without careful ordering. Test cross-tab scenarios thoroughly.

15.4 Scheduler API (postTask) for Task Prioritization

Method/Property Description Browser Support
scheduler.postTask(callback, options) Schedules task with priority. Returns Promise that resolves with callback result. Chrome, Edge
scheduler.yield() Yields to browser for higher priority work. Returns Promise. EXPERIMENTAL Experimental
Priority Option Description
"user-blocking" Highest priority. Blocks user interaction. Use for input response, animations.
"user-visible" Default priority. User-visible work. Use for rendering, updating UI.
"background" Lowest priority. Background work. Use for analytics, preloading, cleanup.

Example: Prioritize tasks with scheduler.postTask

// Check support
if ("scheduler" in window && "postTask" in scheduler) {
  console.log("Scheduler API supported");
} else {
  console.log("Scheduler API not supported - using fallback");
}

// Schedule tasks with different priorities
async function schedulePrioritizedTasks() {
  // High priority - user interaction response
  scheduler.postTask(() => {
    console.log("1. User-blocking task (high priority)");
    handleUserInput();
  }, { "priority": "user-blocking" });
  
  // Medium priority - UI update
  scheduler.postTask(() => {
    console.log("2. User-visible task (medium priority)");
    updateUI();
  }, { "priority": "user-visible" });
  
  // Low priority - background work
  scheduler.postTask(() => {
    console.log("3. Background task (low priority)");
    sendAnalytics();
  }, { "priority": "background" });
  
  // Tasks execute in priority order, not call order
}

schedulePrioritizedTasks();

Example: Break up long task with yields

// Process large dataset with yields to prevent blocking
async function processLargeDataset(items) {
  const results = [];
  
  for (let i = 0; i < items.length; i++) {
    // Process item
    const result = await processItem(items[i]);
    results.push(result);
    
    // Yield every 10 items to prevent blocking
    if (i % 10 === 0 && "scheduler" in window) {
      await scheduler.yield();
      console.log(`Processed ${i} items, yielding...`);
    }
  }
  
  return results;
}

// Fallback using setTimeout
async function yieldToMainThread() {
  return new Promise(resolve => setTimeout(resolve, 0));
}

// Universal yield function
async function smartYield() {
  if ("scheduler" in window && "yield" in scheduler) {
    await scheduler.yield();
  } else {
    await yieldToMainThread();
  }
}

// Usage
processLargeDataset(largeArray)
  .then(results => {
    console.log("All items processed");
    displayResults(results);
  });

Example: Task scheduler with abort

// Schedule task with cancellation
class TaskScheduler {
  constructor() {
    this.tasks = new Map();
  }
  
  async scheduleTask(name, callback, priority = "user-visible") {
    const controller = new AbortController();
    
    this.tasks.set(name, controller);
    
    try {
      const result = await scheduler.postTask(
        callback,
        {
          "priority": priority,
          "signal": controller.signal
        }
      );
      
      console.log(`Task "${name}" completed`);
      this.tasks.delete(name);
      return result;
      
    } catch (error) {
      if (error.name === "AbortError") {
        console.log(`Task "${name}" was cancelled`);
      } else {
        console.error(`Task "${name}" failed:`, error);
      }
      this.tasks.delete(name);
      throw error;
    }
  }
  
  cancelTask(name) {
    const controller = this.tasks.get(name);
    if (controller) {
      controller.abort();
      this.tasks.delete(name);
      console.log(`Cancelled task: ${name}`);
    }
  }
  
  cancelAllTasks() {
    for (const [name, controller] of this.tasks) {
      controller.abort();
      console.log(`Cancelled task: ${name}`);
    }
    this.tasks.clear();
  }
}

// Usage
const taskScheduler = new TaskScheduler();

// Schedule low priority task
taskScheduler.scheduleTask(
  "background-sync",
  async () => {
    console.log("Syncing data...");
    await syncData();
  },
  "background"
);

// Schedule high priority task
taskScheduler.scheduleTask(
  "handle-click",
  async () => {
    console.log("Handling user click");
    await handleClick();
  },
  "user-blocking"
);

// Cancel specific task
setTimeout(() => {
  taskScheduler.cancelTask("background-sync");
}, 1000);
Note: Scheduler API enables task prioritization for better responsiveness. Use "user-blocking" for input handlers, "user-visible" for UI updates, "background" for analytics. scheduler.yield() allows long tasks to break up and yield to browser. Better than setTimeout(0) for task scheduling.
Warning: Limited browser support (Chrome, Edge). scheduler.yield() is experimental. Provide fallback using setTimeout. Priorities are hints - browser controls actual scheduling. Don't overuse user-blocking - can hurt performance. Test with Performance Observer for long tasks.

15.5 View Transitions API for Smooth Page Transitions

Method/Property Description Browser Support
document.startViewTransition(callback) Starts view transition. Callback updates DOM. Returns ViewTransition object. Chrome, Edge
transition.finished Promise that resolves when transition completes. Chrome, Edge
transition.ready Promise that resolves when transition is ready to animate. Chrome, Edge
transition.updateCallbackDone Promise that resolves when update callback completes. Chrome, Edge
transition.skipTransition() Skips transition animation. Chrome, Edge

Example: Basic view transition

// Check support
if (!document.startViewTransition) {
  console.log("View Transitions API not supported");
}

// Simple view transition
function updateView() {
  if (!document.startViewTransition) {
    // Fallback - update without transition
    updateDOM();
    return;
  }
  
  // Start transition
  const transition = document.startViewTransition(() => {
    // Update DOM here
    updateDOM();
  });
  
  // Listen for completion
  transition.finished
    .then(() => {
      console.log("Transition complete");
    })
    .catch(error => {
      console.error("Transition failed:", error);
    });
}

function updateDOM() {
  // Example DOM update
  document.getElementById("content").innerHTML = "<h1>New Content</h1>";
  document.body.classList.toggle("theme-dark");
}

// Trigger transition
document.getElementById("updateBtn").addEventListener("click", updateView);

Example: Named transitions with CSS

// HTML: Elements need view-transition-name in CSS
// CSS:
// .card {
//   view-transition-name: card-element;
// }
// .hero {
//   view-transition-name: hero-image;
// }

// Navigate with transition
async function navigateToDetail(itemId) {
  if (!document.startViewTransition) {
    // Fallback
    loadDetailPage(itemId);
    return;
  }
  
  const transition = document.startViewTransition(async () => {
    // Fetch and render new content
    const html = await fetchDetailPage(itemId);
    document.getElementById("main").innerHTML = html;
  });
  
  try {
    await transition.finished;
    console.log("Navigation transition complete");
  } catch (error) {
    console.error("Navigation failed:", error);
  }
}

// List to detail transition
document.querySelectorAll(".item").forEach(item => {
  item.addEventListener("click", (event) => {
    const itemId = event.currentTarget.dataset.id;
    navigateToDetail(itemId);
  });
});

Example: Custom transition with CSS animations

/* Default transition for root */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}

::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

/* Slide transition for specific elements */
::view-transition-old(card) {
  animation: slide-out-right 0.4s ease-out;
}

::view-transition-new(card) {
  animation: slide-in-left 0.4s ease-in;
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slide-out-right {
  from { transform: translateX(0); }
  to { transform: translateX(100%); }
}

@keyframes slide-in-left {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

Example: SPA navigation with view transitions

// Single Page App with view transitions
class SPARouter {
  constructor() {
    this.routes = new Map();
    this.setupNavigation();
  }
  
  addRoute(path, handler) {
    this.routes.set(path, handler);
  }
  
  async navigate(path, skipTransition = false) {
    const handler = this.routes.get(path);
    
    if (!handler) {
      console.error("Route not found:", path);
      return;
    }
    
    // Update with or without transition
    if (document.startViewTransition && !skipTransition) {
      const transition = document.startViewTransition(async () => {
        await handler();
        window.history.pushState({}, "", path);
      });
      
      await transition.finished;
    } else {
      await handler();
      window.history.pushState({}, "", path);
    }
    
    console.log("Navigated to:", path);
  }
  
  setupNavigation() {
    // Handle link clicks
    document.addEventListener("click", (event) => {
      const link = event.target.closest("a[data-spa-link]");
      if (link) {
        event.preventDefault();
        const path = link.getAttribute("href");
        this.navigate(path);
      }
    });
    
    // Handle back/forward
    window.addEventListener("popstate", () => {
      const path = window.location.pathname;
      this.navigate(path, true); // Skip transition for back/forward
    });
  }
}

// Setup router
const router = new SPARouter();

router.addRoute("/", async () => {
  document.getElementById("content").innerHTML = "<h1>Home</h1>";
});

router.addRoute("/about", async () => {
  document.getElementById("content").innerHTML = "<h1>About</h1>";
});

router.addRoute("/contact", async () => {
  document.getElementById("content").innerHTML = "<h1>Contact</h1>";
});
Note: View Transitions API enables smooth animated transitions between DOM states. Browser captures before/after states and animates between them. Use view-transition-name CSS property for element-specific transitions. Great for SPA navigation, theme switches, list-to-detail transitions.
Warning: Limited browser support (Chrome, Edge). Always provide fallback. Transitions are skipped if callback throws error. Don't use for time-critical updates. Large DOM changes can be expensive to capture. Test performance on lower-end devices. CSS animations control transition appearance.

15.6 Container Queries API Integration

CSS Property Description Browser Support
container-type Defines element as query container. Values: size, inline-size, normal. All Modern Browsers
container-name Names container for targeting in queries. All Modern Browsers
container Shorthand: container: name / type. All Modern Browsers
@container Container query rule. Syntax: @container name (condition) { ... }. All Modern Browsers

Example: Basic container queries in CSS

/* Define container */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* Query container size */
@container card (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
  
  .card-image {
    width: 200px;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
  
  .card-image {
    width: 100%;
  }
}

/* Container query units */
.card-title {
  font-size: 5cqw; /* 5% of container width */
  padding: 2cqh;   /* 2% of container height */
}

/* Multiple named containers */
.sidebar {
  container: sidebar / inline-size;
}

.main {
  container: main / inline-size;
}

@container sidebar (min-width: 300px) {
  .sidebar-content {
    padding: 2rem;
  }
}

@container main (min-width: 800px) {
  .main-content {
    column-count: 2;
  }
}

Example: Check container query support in JavaScript

// Check container queries support
function supportsContainerQueries() {
  return CSS.supports("container-type: inline-size") ||
         CSS.supports("container-type", "inline-size");
}

if (supportsContainerQueries()) {
  console.log("Container queries supported");
  document.body.classList.add("supports-container-queries");
} else {
  console.log("Container queries not supported - using fallback");
  document.body.classList.add("no-container-queries");
}

// Observe container size changes
if (window.ResizeObserver) {
  const container = document.querySelector(".card-container");
  
  const observer = new ResizeObserver(entries => {
    for (const entry of entries) {
      const width = entry.contentRect.width;
      console.log("Container width:", width);
      
      // Manual breakpoints as fallback
      if (width >= 400) {
        entry.target.classList.add("wide");
      } else {
        entry.target.classList.remove("wide");
      }
    }
  });
  
  observer.observe(container);
}

Example: Dynamic responsive components

<!-- HTML -->
<div class="grid">
  <div class="grid-item">
    <div class="component-container">
      <div class="component">
        <h2>Responsive Component</h2>
        <p>This component adapts to its container size, not viewport.</p>
      </div>
    </div>
  </div>
  <!-- More grid items... -->
</div>

Example: Container queries CSS for responsive component

/* Setup grid that changes columns */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1rem;
}

/* Each item is a container */
.component-container {
  container-type: inline-size;
  container-name: component;
  border: 1px solid #ccc;
  border-radius: 8px;
  overflow: hidden;
}

/* Component adapts to its container */
.component {
  padding: 1rem;
}

/* Small container */
@container component (max-width: 349px) {
  .component h2 {
    font-size: 1.2rem;
  }
  
  .component p {
    font-size: 0.9rem;
  }
  
  .component-layout {
    display: block;
  }
}

/* Medium container */
@container component (min-width: 350px) and (max-width: 599px) {
  .component h2 {
    font-size: 1.5rem;
  }
  
  .component p {
    font-size: 1rem;
  }
  
  .component-layout {
    display: flex;
    gap: 1rem;
  }
}

/* Large container */
@container component (min-width: 600px) {
  .component h2 {
    font-size: 2rem;
  }
  
  .component p {
    font-size: 1.1rem;
    line-height: 1.6;
  }
  
  .component-layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2rem;
  }
}

/* Container query units */
.component-title {
  /* cqw: % of container width */
  /* cqh: % of container height */
  /* cqi: % of container inline size */
  /* cqb: % of container block size */
  /* cqmin: min of cqi and cqb */
  /* cqmax: max of cqi and cqb */
  
  font-size: clamp(1rem, 4cqw, 2.5rem);
  margin-bottom: 1cqh;
}
Note: Container Queries allow components to respond to their container size, not viewport. More flexible than media queries for reusable components. Use container-type: inline-size for width-based queries. Container query units (cqw, cqh) relative to container, not viewport.
Warning: Container queries are CSS feature, not JavaScript API. Use CSS.supports() to check support. Avoid circular dependencies (container size depends on children, children depend on container). Performance impact on complex layouts - test on lower-end devices. Fallback to media queries for older browsers.

Modern Web Platform APIs Best Practices

  • Streams API perfect for large files and real-time data processing
  • Use ReadableStream.getReader() to process chunks incrementally
  • Compression Streams reduce bandwidth - measure to ensure benefit
  • Web Locks API prevents race conditions across tabs/workers
  • Use shared locks for reads, exclusive locks for writes
  • Scheduler.postTask prioritizes tasks better than setTimeout
  • Use "user-blocking" sparingly - only for critical interactions
  • Yield with scheduler.yield() to prevent blocking main thread
  • View Transitions API smooths SPA navigation and state changes
  • Always provide fallback for View Transitions - limited support
  • Container queries make truly reusable responsive components
  • Check API support with CSS.supports() and feature detection
  • Test all modern APIs on target browsers - support varies

16. Internationalization and Formatting APIs

16.1 Intl Object and Localization Services

Intl Class Purpose Browser Support
Intl.DateTimeFormat Locale-aware date and time formatting. All Browsers
Intl.NumberFormat Locale-aware number formatting (currency, percent, units). All Browsers
Intl.Collator Locale-aware string comparison and sorting. All Browsers
Intl.PluralRules Locale-aware pluralization rules. All Modern Browsers
Intl.RelativeTimeFormat Locale-aware relative time formatting ("2 days ago"). All Modern Browsers
Intl.ListFormat Locale-aware list formatting ("A, B, and C"). All Modern Browsers
Intl.DisplayNames Locale-aware display names for languages, regions, scripts. All Modern Browsers
Intl.Locale Locale identifier parsing and manipulation. All Modern Browsers
Common Method Description
Intl.getCanonicalLocales(locales) Returns canonical locale identifiers.
Intl.supportedValuesOf(key) Returns supported values for key: "calendar", "currency", "timeZone", etc.

Example: Check supported locales and values

// Get canonical locale identifiers
const locales = Intl.getCanonicalLocales(["EN-us", "en-GB", "ja-JP"]);
console.log("Canonical locales:", locales);
// Output: ["en-US", "en-GB", "ja-JP"]

// Check supported currencies
if (Intl.supportedValuesOf) {
  const currencies = Intl.supportedValuesOf("currency");
  console.log("Supported currencies:", currencies.slice(0, 10));
  // Output: ["AED", "AFN", "ALL", "AMD", "ANG", ...]
  
  // Check if specific currency is supported
  const hasEUR = currencies.includes("EUR");
  console.log("EUR supported:", hasEUR);
  
  // Get all time zones
  const timeZones = Intl.supportedValuesOf("timeZone");
  console.log("Time zones count:", timeZones.length);
  console.log("Sample time zones:", timeZones.slice(0, 5));
  // Output: ["Africa/Abidjan", "Africa/Accra", ...]
  
  // Get all calendars
  const calendars = Intl.supportedValuesOf("calendar");
  console.log("Supported calendars:", calendars);
  // Output: ["buddhist", "chinese", "gregory", "hebrew", "indian", ...]
  
  // Get all numbering systems
  const numberingSystems = Intl.supportedValuesOf("numberingSystem");
  console.log("Numbering systems:", numberingSystems.slice(0, 10));
  // Output: ["arab", "arabext", "bali", "beng", ...]
}

// Detect user's locale
const userLocale = navigator.language || navigator.userLanguage;
console.log("User locale:", userLocale);
// Output: "en-US" (or user's system locale)

// Get all preferred locales
const userLocales = navigator.languages;
console.log("User locales:", userLocales);
// Output: ["en-US", "en", "es"]
Note: Intl object provides locale-aware formatting and parsing for dates, numbers, strings, and more. Uses Unicode CLDR data. Always specify locale or use user's locale from navigator.language. All formatters accept locale and options.

16.2 Intl.DateTimeFormat for Date Localization

Option Values Description
dateStyle "full", "long", "medium", "short" Predefined date format style. Cannot combine with individual date/time options.
timeStyle "full", "long", "medium", "short" Predefined time format style.
year "numeric", "2-digit" Year representation.
month "numeric", "2-digit", "long", "short", "narrow" Month representation.
day "numeric", "2-digit" Day representation.
weekday "long", "short", "narrow" Weekday representation.
hour "numeric", "2-digit" Hour representation.
minute "numeric", "2-digit" Minute representation.
second "numeric", "2-digit" Second representation.
timeZone IANA time zone name Time zone: "America/New_York", "Europe/London", "UTC", etc.
hour12 true, false Use 12-hour or 24-hour time.

Example: Format dates in different locales

const date = new Date("2024-03-15T14:30:00");

// Basic formatting with dateStyle and timeStyle
const formatterUS = new Intl.DateTimeFormat("en-US", {
  "dateStyle": "full",
  "timeStyle": "long"
});
console.log("en-US:", formatterUS.format(date));
// Output: "Friday, March 15, 2024 at 2:30:00 PM"

const formatterFR = new Intl.DateTimeFormat("fr-FR", {
  "dateStyle": "full",
  "timeStyle": "long"
});
console.log("fr-FR:", formatterFR.format(date));
// Output: "vendredi 15 mars 2024 à 14:30:00"

const formatterJA = new Intl.DateTimeFormat("ja-JP", {
  "dateStyle": "full",
  "timeStyle": "long"
});
console.log("ja-JP:", formatterJA.format(date));
// Output: "2024年3月15日金曜日 14:30:00"

// Custom formatting
const customFormatter = new Intl.DateTimeFormat("en-US", {
  "year": "numeric",
  "month": "long",
  "day": "numeric",
  "weekday": "long",
  "hour": "numeric",
  "minute": "2-digit",
  "hour12": true
});
console.log("Custom:", customFormatter.format(date));
// Output: "Friday, March 15, 2024, 2:30 PM"

// Different styles
const styles = ["full", "long", "medium", "short"];
styles.forEach(style => {
  const formatter = new Intl.DateTimeFormat("en-US", { "dateStyle": style });
  console.log(`${style}:`, formatter.format(date));
});
// Output:
// full: Friday, March 15, 2024
// long: March 15, 2024
// medium: Mar 15, 2024
// short: 3/15/24

Example: Time zones and formatting parts

const date = new Date("2024-03-15T14:30:00Z");

// Format in different time zones
const timeZones = [
  "America/New_York",
  "Europe/London",
  "Asia/Tokyo",
  "Australia/Sydney"
];

timeZones.forEach(timeZone => {
  const formatter = new Intl.DateTimeFormat("en-US", {
    "timeZone": timeZone,
    "dateStyle": "medium",
    "timeStyle": "long",
    "timeZoneName": "short"
  });
  console.log(`${timeZone}:`, formatter.format(date));
});
// Output:
// America/New_York: Mar 15, 2024, 10:30:00 AM EDT
// Europe/London: Mar 15, 2024, 2:30:00 PM GMT
// Asia/Tokyo: Mar 15, 2024, 11:30:00 PM JST
// Australia/Sydney: Mar 16, 2024, 1:30:00 AM AEDT

// Get formatted parts for custom rendering
const formatter = new Intl.DateTimeFormat("en-US", {
  "year": "numeric",
  "month": "long",
  "day": "numeric",
  "hour": "numeric",
  "minute": "2-digit"
});

const parts = formatter.formatToParts(date);
console.log("Parts:", parts);
// Output: [
//   { type: "month", value: "March" },
//   { type: "literal", value: " " },
//   { type: "day", value: "15" },
//   { type: "literal", value: ", " },
//   { type: "year", value: "2024" },
//   ...
// ]

// Build custom format from parts
const customFormat = parts.map(part => {
  if (part.type === "month") return `<strong>${part.value}</strong>`;
  if (part.type === "day") return `<span class="day">${part.value}</span>`;
  return part.value;
}).join("");
console.log("Custom HTML:", customFormat);

// Format range
const startDate = new Date("2024-03-15");
const endDate = new Date("2024-03-20");
const range = formatter.formatRange(startDate, endDate);
console.log("Range:", range);
// Output: "March 15 – 20, 2024"
Note: Intl.DateTimeFormat provides locale-aware date/time formatting. Use dateStyle/timeStyle for quick formatting or individual options for custom formats. formatToParts() returns parts for custom rendering. formatRange() formats date ranges intelligently.

16.3 Intl.NumberFormat for Number Localization

Option Values Description
style "decimal", "currency", "percent", "unit" Number formatting style.
currency ISO 4217 currency code Currency: "USD", "EUR", "JPY", etc. Required if style is "currency".
currencyDisplay "symbol", "code", "name", "narrowSymbol" How to display currency.
unit Unit identifier Unit: "kilometer", "celsius", "byte", etc. Required if style is "unit".
minimumFractionDigits 0-20 Minimum decimal places.
maximumFractionDigits 0-20 Maximum decimal places.
minimumSignificantDigits 1-21 Minimum significant digits.
maximumSignificantDigits 1-21 Maximum significant digits.
notation "standard", "scientific", "engineering", "compact" Number notation style.
signDisplay "auto", "always", "exceptZero", "never" When to display sign.

Example: Format numbers, currency, and percentages

const number = 1234567.89;

// Basic number formatting
const numberUS = new Intl.NumberFormat("en-US").format(number);
console.log("en-US:", numberUS);
// Output: "1,234,567.89"

const numberDE = new Intl.NumberFormat("de-DE").format(number);
console.log("de-DE:", numberDE);
// Output: "1.234.567,89"

const numberIN = new Intl.NumberFormat("en-IN").format(number);
console.log("en-IN:", numberIN);
// Output: "12,34,567.89" (Indian grouping)

// Currency formatting
const price = 1299.99;

const usd = new Intl.NumberFormat("en-US", {
  "style": "currency",
  "currency": "USD"
}).format(price);
console.log("USD:", usd);
// Output: "$1,299.99"

const eur = new Intl.NumberFormat("de-DE", {
  "style": "currency",
  "currency": "EUR"
}).format(price);
console.log("EUR:", eur);
// Output: "1.299,99 €"

const jpy = new Intl.NumberFormat("ja-JP", {
  "style": "currency",
  "currency": "JPY"
}).format(price);
console.log("JPY:", jpy);
// Output: "¥1,300" (JPY has no decimal places)

// Currency display variations
const currencyDisplays = ["symbol", "code", "name", "narrowSymbol"];
currencyDisplays.forEach(display => {
  const formatted = new Intl.NumberFormat("en-US", {
    "style": "currency",
    "currency": "USD",
    "currencyDisplay": display
  }).format(price);
  console.log(`${display}:`, formatted);
});
// Output:
// symbol: $1,299.99
// code: USD 1,299.99
// name: 1,299.99 US dollars
// narrowSymbol: $1,299.99

// Percentage
const percent = 0.756;
const percentFormatter = new Intl.NumberFormat("en-US", {
  "style": "percent",
  "minimumFractionDigits": 1,
  "maximumFractionDigits": 1
});
console.log("Percent:", percentFormatter.format(percent));
// Output: "75.6%"

Example: Units, compact notation, and advanced formatting

// Unit formatting
const distance = 1234.5;
const distanceKm = new Intl.NumberFormat("en-US", {
  "style": "unit",
  "unit": "kilometer",
  "unitDisplay": "long"
}).format(distance);
console.log("Distance:", distanceKm);
// Output: "1,234.5 kilometers"

const temperature = 23.5;
const tempCelsius = new Intl.NumberFormat("en-US", {
  "style": "unit",
  "unit": "celsius",
  "unitDisplay": "short"
}).format(temperature);
console.log("Temperature:", tempCelsius);
// Output: "23.5°C"

// File size with bytes
const fileSize = 1234567890;
const fileSizeGB = new Intl.NumberFormat("en-US", {
  "style": "unit",
  "unit": "gigabyte",
  "unitDisplay": "narrow"
}).format(fileSize / 1e9);
console.log("File size:", fileSizeGB);
// Output: "1.23GB"

// Compact notation
const bigNumber = 1234567890;

const compact = new Intl.NumberFormat("en-US", {
  "notation": "compact",
  "compactDisplay": "short"
}).format(bigNumber);
console.log("Compact:", compact);
// Output: "1.2B"

const compactLong = new Intl.NumberFormat("en-US", {
  "notation": "compact",
  "compactDisplay": "long"
}).format(bigNumber);
console.log("Compact long:", compactLong);
// Output: "1.2 billion"

// Scientific notation
const scientific = new Intl.NumberFormat("en-US", {
  "notation": "scientific",
  "maximumFractionDigits": 2
}).format(bigNumber);
console.log("Scientific:", scientific);
// Output: "1.23E9"

// Engineering notation
const engineering = new Intl.NumberFormat("en-US", {
  "notation": "engineering"
}).format(bigNumber);
console.log("Engineering:", engineering);
// Output: "1.235E9"

// Sign display
const positiveNumber = 42;
const negativeNumber = -42;

const signAlways = new Intl.NumberFormat("en-US", {
  "signDisplay": "always"
});
console.log("Always:", signAlways.format(positiveNumber));
// Output: "+42"

const signExceptZero = new Intl.NumberFormat("en-US", {
  "signDisplay": "exceptZero"
});
console.log("ExceptZero 0:", signExceptZero.format(0));
// Output: "0"
console.log("ExceptZero +:", signExceptZero.format(positiveNumber));
// Output: "+42"
Note: Intl.NumberFormat handles numbers, currency, percentages, and units. Use style option to choose format type. Compact notation great for large numbers. formatToParts() available for custom rendering. Automatically handles locale-specific grouping and decimals.

16.4 Intl.Collator for String Comparison

Option Values Description
usage "sort", "search" Purpose: sorting or searching. Default: "sort".
sensitivity "base", "accent", "case", "variant" Comparison sensitivity level.
ignorePunctuation true, false Ignore punctuation in comparison.
numeric true, false Use numeric collation (e.g., "2" < "10").
caseFirst "upper", "lower", "false" Sort uppercase or lowercase first.

Example: Locale-aware string sorting

// Array to sort
const words = ["réservé", "Premier", "Communiqué", "café", "adieu"];

// Default JavaScript sort (incorrect for locales)
const defaultSort = [...words].sort();
console.log("Default sort:", defaultSort);
// Output: ["Communiqué", "Premier", "adieu", "café", "réservé"]

// Locale-aware sort
const collator = new Intl.Collator("fr-FR");
const localeSort = [...words].sort(collator.compare);
console.log("Locale sort:", localeSort);
// Output: ["adieu", "café", "Communiqué", "Premier", "réservé"]

// Case-insensitive sort
const caseInsensitive = new Intl.Collator("en-US", {
  "sensitivity": "base"
});
const names = ["Alice", "bob", "Charlie", "alice", "BOB"];
const sortedNames = [...names].sort(caseInsensitive.compare);
console.log("Case-insensitive:", sortedNames);
// Output: ["Alice", "alice", "bob", "BOB", "Charlie"]

// Numeric collation
const files = ["file1.txt", "file10.txt", "file2.txt", "file20.txt"];

const regularSort = [...files].sort();
console.log("Regular sort:", regularSort);
// Output: ["file1.txt", "file10.txt", "file2.txt", "file20.txt"] (wrong)

const numericCollator = new Intl.Collator("en-US", { "numeric": true });
const numericSort = [...files].sort(numericCollator.compare);
console.log("Numeric sort:", numericSort);
// Output: ["file1.txt", "file2.txt", "file10.txt", "file20.txt"] (correct)

Example: Search with different sensitivities

// Sensitivity levels
const searchTerm = "resume";
const documents = ["Resume", "résumé", "RESUME", "resumé"];

// Base: ignore case and accents
const baseCollator = new Intl.Collator("en-US", {
  "usage": "search",
  "sensitivity": "base"
});
const baseMatches = documents.filter(doc =>
  baseCollator.compare(doc, searchTerm) === 0
);
console.log("Base matches:", baseMatches);
// Output: ["Resume", "résumé", "RESUME", "resumé"]

// Accent: case-insensitive, accent-sensitive
const accentCollator = new Intl.Collator("en-US", {
  "usage": "search",
  "sensitivity": "accent"
});
const accentMatches = documents.filter(doc =>
  accentCollator.compare(doc, searchTerm) === 0
);
console.log("Accent matches:", accentMatches);
// Output: ["Resume", "RESUME"]

// Case: case-sensitive, accent-insensitive
const caseCollator = new Intl.Collator("en-US", {
  "usage": "search",
  "sensitivity": "case"
});
const caseMatches = documents.filter(doc =>
  caseCollator.compare(doc, searchTerm) === 0
);
console.log("Case matches:", caseMatches);
// Output: ["resume", "resumé"]

// Variant: exact match
const variantCollator = new Intl.Collator("en-US", {
  "usage": "search",
  "sensitivity": "variant"
});
const variantMatches = documents.filter(doc =>
  variantCollator.compare(doc, searchTerm) === 0
);
console.log("Variant matches:", variantMatches);
// Output: ["resume"]

// Ignore punctuation
const punctuationCollator = new Intl.Collator("en-US", {
  "ignorePunctuation": true
});
console.log(punctuationCollator.compare("co-op", "coop"));
// Output: 0 (equal)
Note: Intl.Collator provides locale-aware string comparison for sorting and searching. Use compare method with Array.sort(). Set numeric: true for natural sorting of numbers in strings. Different sensitivity levels for search scenarios.

16.5 Intl.PluralRules for Pluralization Logic

Method/Property Description
pluralRules.select(number) Returns plural category: "zero", "one", "two", "few", "many", "other".
pluralRules.resolvedOptions() Returns resolved options object.
Option Values Description
type "cardinal", "ordinal" Type: cardinal (1, 2, 3) or ordinal (1st, 2nd, 3rd).

Example: Pluralization rules for different locales

// English pluralization (simple: one vs other)
const enPluralRules = new Intl.PluralRules("en-US");

console.log("English:");
[0, 1, 2, 5].forEach(n => {
  console.log(`${n}: ${enPluralRules.select(n)}`);
});
// Output:
// 0: other
// 1: one
// 2: other
// 5: other

// Russian pluralization (complex: one, few, many, other)
const ruPluralRules = new Intl.PluralRules("ru-RU");

console.log("\nRussian:");
[0, 1, 2, 3, 5, 21, 22, 25, 100].forEach(n => {
  console.log(`${n}: ${ruPluralRules.select(n)}`);
});
// Output:
// 0: many
// 1: one
// 2: few
// 3: few
// 5: many
// 21: one
// 22: few
// 25: many
// 100: many

// Arabic pluralization (has zero, one, two, few, many, other)
const arPluralRules = new Intl.PluralRules("ar-EG");

console.log("\nArabic:");
[0, 1, 2, 3, 11, 100].forEach(n => {
  console.log(`${n}: ${arPluralRules.select(n)}`);
});
// Output:
// 0: zero
// 1: one
// 2: two
// 3: few
// 11: many
// 100: other

Example: Build pluralized messages

// Pluralization helper
function pluralize(locale, count, translations) {
  const pluralRules = new Intl.PluralRules(locale);
  const category = pluralRules.select(count);
  return translations[category] || translations.other;
}

// English
const enMessages = {
  "one": "You have 1 message",
  "other": "You have {count} messages"
};

[0, 1, 5].forEach(count => {
  const message = pluralize("en-US", count, enMessages)
    .replace("{count}", count);
  console.log(message);
});
// Output:
// You have 0 messages
// You have 1 message
// You have 5 messages

// Russian
const ruMessages = {
  "one": "{count} сообщение",
  "few": "{count} сообщения",
  "many": "{count} сообщений",
  "other": "{count} сообщений"
};

[1, 2, 5, 21, 22, 25].forEach(count => {
  const message = pluralize("ru-RU", count, ruMessages)
    .replace("{count}", count);
  console.log(message);
});
// Output:
// 1 сообщение
// 2 сообщения
// 5 сообщений
// 21 сообщение
// 22 сообщения
// 25 сообщений

// Ordinal numbers (1st, 2nd, 3rd)
const ordinalRules = new Intl.PluralRules("en-US", { "type": "ordinal" });

const ordinalSuffixes = {
  "one": "st",
  "two": "nd",
  "few": "rd",
  "other": "th"
};

[1, 2, 3, 4, 11, 21, 22, 23, 101, 102].forEach(n => {
  const category = ordinalRules.select(n);
  const suffix = ordinalSuffixes[category];
  console.log(`${n}${suffix}`);
});
// Output: 1st, 2nd, 3rd, 4th, 11th, 21st, 22nd, 23rd, 101st, 102nd
Note: Intl.PluralRules provides locale-specific plural categories. Different languages have different plural rules (English has 2, Russian has 4, Arabic has 6). Use select() to get category, then map to appropriate message. Use "ordinal" type for 1st, 2nd, 3rd formatting.

16.6 Intl.Locale for Language Tag Parsing

Property Description
locale.language Language subtag (e.g., "en", "fr", "zh").
locale.region Region subtag (e.g., "US", "GB", "CN").
locale.script Script subtag (e.g., "Latn", "Cyrl", "Hans").
locale.baseName Base locale without extensions (e.g., "en-US").
locale.calendar Calendar system (e.g., "gregory", "buddhist", "chinese").
locale.numberingSystem Numbering system (e.g., "latn", "arab", "hanidec").
locale.hourCycle Hour cycle: "h11", "h12", "h23", "h24".
locale.caseFirst Case ordering: "upper", "lower", "false".
locale.numeric Numeric collation boolean.

Example: Parse and manipulate locale identifiers

// Parse locale identifier
const locale1 = new Intl.Locale("en-US");
console.log("Language:", locale1.language);  // "en"
console.log("Region:", locale1.region);      // "US"
console.log("BaseName:", locale1.baseName);  // "en-US"

// Complex locale with extensions
const locale2 = new Intl.Locale("zh-Hans-CN-u-ca-chinese-nu-hanidec");
console.log("\nComplex locale:");
console.log("Language:", locale2.language);          // "zh"
console.log("Script:", locale2.script);              // "Hans"
console.log("Region:", locale2.region);              // "CN"
console.log("Calendar:", locale2.calendar);          // "chinese"
console.log("Numbering:", locale2.numberingSystem);  // "hanidec"
console.log("BaseName:", locale2.baseName);          // "zh-Hans-CN"
console.log("Full:", locale2.toString());            // Full identifier

// Create locale with options
const locale3 = new Intl.Locale("en", {
  "region": "GB",
  "calendar": "gregory",
  "hourCycle": "h23",
  "numberingSystem": "latn"
});
console.log("\nConstructed locale:", locale3.toString());
// Output: "en-GB-u-ca-gregory-hc-h23-nu-latn"

// Get maximized locale (add likely subtags)
const simpleLocale = new Intl.Locale("en");
if (simpleLocale.maximize) {
  const maximized = simpleLocale.maximize();
  console.log("\nMaximized:");
  console.log("Original:", simpleLocale.toString());  // "en"
  console.log("Maximized:", maximized.toString());    // "en-Latn-US"
}

// Get minimized locale (remove redundant subtags)
const complexLocale = new Intl.Locale("en-Latn-US");
if (complexLocale.minimize) {
  const minimized = complexLocale.minimize();
  console.log("\nMinimized:");
  console.log("Original:", complexLocale.toString());  // "en-Latn-US"
  console.log("Minimized:", minimized.toString());     // "en"
}

Example: Locale-specific calendar and numbering systems

// Use locale with different calendar
const gregorianLocale = new Intl.Locale("en-US", {
  "calendar": "gregory"
});

const buddhistLocale = new Intl.Locale("th-TH", {
  "calendar": "buddhist"
});

const date = new Date("2024-03-15");

const gregorianFormatter = new Intl.DateTimeFormat(gregorianLocale, {
  "year": "numeric",
  "month": "long",
  "day": "numeric"
});
console.log("Gregorian:", gregorianFormatter.format(date));
// Output: "March 15, 2024"

const buddhistFormatter = new Intl.DateTimeFormat(buddhistLocale, {
  "year": "numeric",
  "month": "long",
  "day": "numeric"
});
console.log("Buddhist:", buddhistFormatter.format(date));
// Output: "15 มีนาคม 2567" (year 2567 in Buddhist calendar)

// Different numbering systems
const arabicLocale = new Intl.Locale("ar-EG", {
  "numberingSystem": "arab"
});

const westernArabicLocale = new Intl.Locale("ar-EG", {
  "numberingSystem": "latn"
});

const number = 12345;

const arabicFormatter = new Intl.NumberFormat(arabicLocale);
console.log("Arabic digits:", arabicFormatter.format(number));
// Output: "١٢٬٣٤٥"

const latinFormatter = new Intl.NumberFormat(westernArabicLocale);
console.log("Latin digits:", latinFormatter.format(number));
// Output: "12,345"

// Get locale info
console.log("\nLocale info:");
console.log("Calendars:", Intl.supportedValuesOf("calendar"));
console.log("Numbering systems:", Intl.supportedValuesOf("numberingSystem"));
Note: Intl.Locale parses and manipulates BCP 47 language tags. Use to extract language, region, script, and extensions. maximize() adds likely subtags, minimize() removes redundant ones. Can specify calendar, numbering system, hour cycle in locale.
Warning: Locale strings must be valid BCP 47 tags - invalid tags throw errors. Not all calendar/numbering systems supported in all browsers. Use Intl.supportedValuesOf() to check available values. maximize() and minimize() have limited browser support - check before using.

Internationalization and Formatting Best Practices

  • Always use Intl APIs for locale-sensitive formatting - don't build your own
  • Get user's locale from navigator.language or navigator.languages
  • Use Intl.supportedValuesOf() to check available currencies, time zones, etc.
  • DateTimeFormat: Use dateStyle/timeStyle for quick formatting
  • NumberFormat: Always specify currency with style: "currency"
  • Use compact notation for large numbers in UI (1.2B instead of 1,234,567,890)
  • Collator: Enable numeric: true for natural sorting of filenames
  • PluralRules: Build locale-aware pluralization - languages have different rules
  • Cache Intl formatters - creating formatters is expensive
  • Use formatToParts() when you need fine-grained control over rendering
  • Test with multiple locales - especially RTL languages (ar, he)
  • Consider time zones - use IANA time zone identifiers
  • Provide fallback locale if user's locale not supported

17. Progressive Web App (PWA) APIs

17.1 Web App Manifest API for App Installation

Manifest Property Description Example
name Full name of the app displayed during installation. "My Awesome PWA"
short_name Short name for home screen (limit 12 chars). "MyPWA"
start_url URL to open when app is launched. "/app?source=pwa"
display Display mode: "standalone", "fullscreen", "minimal-ui", "browser". "standalone"
background_color Background color during splash screen. "#ffffff"
theme_color Theme color for browser UI. "#4285f4"
icons Array of icon objects with src, sizes, type, purpose. [{src: "/icon-192.png", sizes: "192x192"}]
orientation Preferred orientation: "portrait", "landscape", "any". "portrait"
scope Navigation scope - URLs outside this scope open in browser. "/app/"
description Description of the app. "A powerful task manager"

Example: Complete Web App Manifest

{
  "name": "Task Manager Pro",
  "short_name": "TaskPro",
  "description": "Professional task management for teams",
  "start_url": "/app?source=pwa",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2196f3",
  "orientation": "portrait-primary",
  "scope": "/app/",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/maskable-icon.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/desktop-1.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/screenshots/mobile-1.png",
      "sizes": "750x1334",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ],
  "categories": ["productivity", "business"],
  "lang": "en-US",
  "dir": "ltr"
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Task Manager Pro</title>
  
  <!-- Link to Web App Manifest -->
  <link rel="manifest" href="/manifest.json">
  
  <!-- Theme color for browser chrome -->
  <meta name="theme-color" content="#2196f3">
  
  <!-- Apple-specific tags -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="default">
  <meta name="apple-mobile-web-app-title" content="TaskPro">
  <link rel="apple-touch-icon" href="/icons/icon-192x192.png">
</head>
<body>
  <h1>Task Manager Pro</h1>
  <script>
    // Check if manifest is linked
    if ('manifest' in document.createElement('link')) {
      console.log("Web App Manifest supported");
    }
    
    // Get manifest programmatically (limited support)
    if ('getManifest' in document) {
      document.getManifest().then(manifest => {
        console.log("Manifest:", manifest);
      });
    }
  </script>
</body>
</html>
Note: Web App Manifest is a JSON file defining PWA metadata. Link with <link rel="manifest">. Icons should include multiple sizes (72px to 512px). Use "purpose": "maskable" for adaptive icons. Screenshots shown in install prompts on some platforms.

17.2 App Installation Prompts and beforeinstallprompt

Event/Method Description
beforeinstallprompt Event fired when browser is ready to show install prompt.
event.prompt() Shows the install prompt (must be called from user gesture).
event.userChoice Promise resolving to user's choice: "accepted" or "dismissed".
appinstalled Event fired when app is successfully installed.
Install Criteria (Chrome/Edge) Required
HTTPS ✓ Site must be served over HTTPS (or localhost)
Web App Manifest ✓ Valid manifest with name, icons, start_url, display
Service Worker ✓ Registered service worker with fetch event handler
User Engagement ✓ User has visited site at least once

Example: Custom install button with beforeinstallprompt

// Store the install prompt event
let deferredPrompt;
const installButton = document.getElementById("install-button");

// Hide install button initially
installButton.style.display = "none";

// Listen for beforeinstallprompt event
window.addEventListener("beforeinstallprompt", (event) => {
  console.log("beforeinstallprompt fired");
  
  // Prevent the default browser install prompt
  event.preventDefault();
  
  // Store the event for later use
  deferredPrompt = event;
  
  // Show custom install button
  installButton.style.display = "block";
  
  // Track that prompt is available
  analytics.track("install_prompt_available");
});

// Handle custom install button click
installButton.addEventListener("click", async () => {
  if (!deferredPrompt) {
    console.log("Install prompt not available");
    return;
  }
  
  // Hide the install button
  installButton.style.display = "none";
  
  // Show the install prompt
  deferredPrompt.prompt();
  
  // Wait for user's choice
  const choiceResult = await deferredPrompt.userChoice;
  
  console.log(`User choice: ${choiceResult.outcome}`);
  
  if (choiceResult.outcome === "accepted") {
    console.log("User accepted the install prompt");
    analytics.track("install_accepted");
  } else {
    console.log("User dismissed the install prompt");
    analytics.track("install_dismissed");
    
    // Show button again after dismissal (optional)
    // installButton.style.display = "block";
  }
  
  // Clear the deferred prompt
  deferredPrompt = null;
});

// Listen for successful installation
window.addEventListener("appinstalled", (event) => {
  console.log("PWA installed successfully");
  
  // Hide install button
  installButton.style.display = "none";
  
  // Clear deferred prompt
  deferredPrompt = null;
  
  // Track installation
  analytics.track("app_installed");
  
  // Show thank you message
  showNotification("Thanks for installing our app!");
});

Example: Check if already installed and detection

// Check if app is already installed (display mode detection)
function isInstalled() {
  // Check if running in standalone mode
  if (window.matchMedia("(display-mode: standalone)").matches) {
    return true;
  }
  
  // Check Safari-specific property
  if (window.navigator.standalone === true) {
    return true;
  }
  
  return false;
}

// Use on page load
if (isInstalled()) {
  console.log("App is installed and running in standalone mode");
  
  // Hide install button permanently
  document.getElementById("install-button").style.display = "none";
  
  // Show app-specific features
  showInstalledUserFeatures();
} else {
  console.log("App is running in browser");
  
  // Show install promotion
  showInstallPromotion();
}

// Listen for display mode changes
const displayModeMediaQuery = window.matchMedia("(display-mode: standalone)");

displayModeMediaQuery.addEventListener("change", (event) => {
  if (event.matches) {
    console.log("Switched to standalone mode");
  } else {
    console.log("Switched to browser mode");
  }
});

// Smart install prompt timing
class InstallPromptManager {
  constructor() {
    this.promptShown = localStorage.getItem("install_prompt_shown") === "true";
    this.installDismissed = localStorage.getItem("install_dismissed") === "true";
    this.visitCount = parseInt(localStorage.getItem("visit_count") || "0");
  }
  
  incrementVisit() {
    this.visitCount++;
    localStorage.setItem("visit_count", this.visitCount.toString());
  }
  
  shouldShowPrompt() {
    // Don't show if already shown or dismissed
    if (this.promptShown || this.installDismissed) {
      return false;
    }
    
    // Show after 3 visits
    if (this.visitCount < 3) {
      return false;
    }
    
    // Add more custom logic (e.g., time on site, engagement)
    return true;
  }
  
  markPromptShown() {
    this.promptShown = true;
    localStorage.setItem("install_prompt_shown", "true");
  }
  
  markDismissed() {
    this.installDismissed = true;
    localStorage.setItem("install_dismissed", "true");
  }
}

// Usage
const promptManager = new InstallPromptManager();
promptManager.incrementVisit();

window.addEventListener("beforeinstallprompt", (event) => {
  event.preventDefault();
  deferredPrompt = event;
  
  if (promptManager.shouldShowPrompt()) {
    // Show install UI
    showInstallUI();
    promptManager.markPromptShown();
  }
});
Note: beforeinstallprompt allows custom install UX. Call event.preventDefault() to prevent default prompt. Store event and call prompt() from user gesture. Check userChoice promise for outcome. Not available on iOS Safari - use custom install instructions instead.
Warning: beforeinstallprompt only available on Chrome, Edge, and Android browsers. Not supported on iOS/Safari. Install criteria vary by browser. Prompt() can only be called once per event. Must be called from user gesture (click, touch).

17.3 Window Controls Overlay for Desktop PWAs

Feature Description
Window Controls Overlay Allows PWA to use title bar area for custom content on desktop.
navigator.windowControlsOverlay API to interact with window controls overlay.
getTitlebarAreaRect() Returns DOMRect of title bar area.
visible Boolean indicating if overlay is visible.
geometrychange Event fired when title bar geometry changes.

Example: Enable and use Window Controls Overlay

{
  "name": "Desktop PWA",
  "display": "standalone",
  "display_override": ["window-controls-overlay"],
  "theme_color": "#2196f3"
}

Example: Handle title bar area in JavaScript

// Check if Window Controls Overlay is available
if ("windowControlsOverlay" in navigator) {
  const windowControls = navigator.windowControlsOverlay;
  
  console.log("Overlay visible:", windowControls.visible);
  
  if (windowControls.visible) {
    // Get title bar area dimensions
    const titleBarRect = windowControls.getTitlebarAreaRect();
    
    console.log("Title bar area:", {
      x: titleBarRect.x,
      y: titleBarRect.y,
      width: titleBarRect.width,
      height: titleBarRect.height
    });
    
    // Update custom title bar layout
    updateTitleBarLayout(titleBarRect);
  }
  
  // Listen for geometry changes (resize, maximize, etc.)
  windowControls.addEventListener("geometrychange", (event) => {
    console.log("Title bar geometry changed");
    
    const titleBarRect = windowControls.getTitlebarAreaRect();
    const isVisible = windowControls.visible;
    
    console.log("New dimensions:", titleBarRect);
    console.log("Visible:", isVisible);
    
    // Update layout
    updateTitleBarLayout(titleBarRect);
  });
}

// Update title bar layout
function updateTitleBarLayout(rect) {
  const titleBar = document.getElementById("custom-title-bar");
  
  if (titleBar) {
    // Position title bar content avoiding system controls
    titleBar.style.position = "fixed";
    titleBar.style.top = `${rect.y}px`;
    titleBar.style.left = `${rect.x}px`;
    titleBar.style.width = `${rect.width}px`;
    titleBar.style.height = `${rect.height}px`;
  }
}

// CSS for draggable title bar area
const style = document.createElement("style");
style.textContent = `
  #custom-title-bar {
    app-region: drag; /* Makes area draggable */
    display: flex;
    align-items: center;
    padding: 0 16px;
    background: var(--theme-color);
    color: white;
  }
  
  #custom-title-bar button,
  #custom-title-bar a,
  #custom-title-bar input {
    app-region: no-drag; /* Make interactive elements clickable */
  }
`;
document.head.appendChild(style);

Example: CSS for Window Controls Overlay

/* Environment variables for safe areas */
#app-header {
  /* Use title bar area when available */
  padding-top: env(titlebar-area-height, 0px);
  padding-left: env(titlebar-area-x, 0px);
  padding-right: env(titlebar-area-width, 100%);
}

/* Draggable region for custom title bar */
.title-bar {
  -webkit-app-region: drag;
  app-region: drag;
  height: env(titlebar-area-height, 40px);
  display: flex;
  align-items: center;
  background: var(--theme-color);
}

/* Make buttons clickable in drag region */
.title-bar button,
.title-bar a,
.title-bar input,
.title-bar select {
  -webkit-app-region: no-drag;
  app-region: no-drag;
}

/* Adjust layout when overlay is active */
@media (display-mode: window-controls-overlay) {
  body {
    padding-top: 0;
  }
  
  #custom-title-bar {
    display: flex;
  }
}
Note: Window Controls Overlay enables native-like title bars in desktop PWAs. Set "display_override": ["window-controls-overlay"] in manifest. Use app-region: drag CSS to make areas draggable. Set app-region: no-drag on interactive elements. Experimental - currently only in Chromium-based browsers.

17.4 App Shortcuts API for Context Menu Integration

Shortcut Property Description
name Name of the shortcut displayed in menu.
short_name Short version of the name (optional).
description Description of what the shortcut does.
url URL to open when shortcut is activated.
icons Array of icon objects for the shortcut.

Example: Define app shortcuts in manifest

{
  "name": "Task Manager Pro",
  "short_name": "TaskPro",
  "start_url": "/",
  "display": "standalone",
  "shortcuts": [
    {
      "name": "New Task",
      "short_name": "New",
      "description": "Create a new task",
      "url": "/new-task?source=shortcut",
      "icons": [
        {
          "src": "/icons/new-task-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    },
    {
      "name": "My Tasks",
      "short_name": "Tasks",
      "description": "View my tasks",
      "url": "/my-tasks?source=shortcut",
      "icons": [
        {
          "src": "/icons/my-tasks-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    },
    {
      "name": "Calendar",
      "short_name": "Calendar",
      "description": "View calendar",
      "url": "/calendar?source=shortcut",
      "icons": [
        {
          "src": "/icons/calendar-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    },
    {
      "name": "Settings",
      "short_name": "Settings",
      "description": "Open settings",
      "url": "/settings?source=shortcut",
      "icons": [
        {
          "src": "/icons/settings-96x96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    }
  ]
}

Example: Handle shortcut navigation

// Detect if app was opened from shortcut
const urlParams = new URLSearchParams(window.location.search);
const source = urlParams.get("source");

if (source === "shortcut") {
  console.log("App opened from shortcut");
  
  // Track shortcut usage
  const path = window.location.pathname;
  analytics.track("shortcut_used", { path });
  
  // Handle specific shortcut actions
  if (path === "/new-task") {
    // Open new task modal
    openNewTaskModal();
  } else if (path === "/my-tasks") {
    // Navigate to tasks view
    navigateToTasks();
  } else if (path === "/calendar") {
    // Navigate to calendar view
    navigateToCalendar();
  }
}

// Alternative: Use URL hash for shortcut routing
window.addEventListener("load", () => {
  const hash = window.location.hash;
  
  switch (hash) {
    case "#new-task":
      openNewTaskModal();
      break;
    case "#my-tasks":
      navigateToTasks();
      break;
    case "#calendar":
      navigateToCalendar();
      break;
    default:
      // Show default view
      showDashboard();
  }
});
Note: App Shortcuts appear in OS context menus (right-click on app icon, taskbar, etc.). Maximum 4 shortcuts recommended. Icons should be 96x96 minimum. URLs can include query parameters to track source. Supported on Windows, macOS, Android.

17.5 Display Mode Detection and Handling

Display Mode Description
fullscreen Full screen without any browser UI.
standalone Standalone app without browser chrome (recommended for PWAs).
minimal-ui Standalone with minimal browser UI (back/forward buttons).
browser Regular browser tab.
window-controls-overlay Desktop PWA with custom title bar.

Example: Detect and respond to display mode

// Check current display mode
function getDisplayMode() {
  // Check each display mode
  const modes = [
    "fullscreen",
    "standalone",
    "minimal-ui",
    "browser"
  ];
  
  for (const mode of modes) {
    if (window.matchMedia(`(display-mode: ${mode})`).matches) {
      return mode;
    }
  }
  
  return "browser"; // Default
}

// Use on page load
const displayMode = getDisplayMode();
console.log("Display mode:", displayMode);

// Apply mode-specific styles or features
switch (displayMode) {
  case "fullscreen":
    console.log("Running in fullscreen mode");
    hideNavigationControls();
    break;
    
  case "standalone":
    console.log("Running as installed PWA");
    showPWAFeatures();
    hideInstallButton();
    break;
    
  case "minimal-ui":
    console.log("Running with minimal UI");
    adjustLayoutForMinimalUI();
    break;
    
  case "browser":
    console.log("Running in browser");
    showInstallPrompt();
    break;
}

// Listen for display mode changes
const displayModeQueries = {
  fullscreen: window.matchMedia("(display-mode: fullscreen)"),
  standalone: window.matchMedia("(display-mode: standalone)"),
  minimalUi: window.matchMedia("(display-mode: minimal-ui)"),
  browser: window.matchMedia("(display-mode: browser)")
};

Object.keys(displayModeQueries).forEach(mode => {
  displayModeQueries[mode].addEventListener("change", (event) => {
    if (event.matches) {
      console.log(`Display mode changed to: ${mode}`);
      handleDisplayModeChange(mode);
    }
  });
});

function handleDisplayModeChange(mode) {
  // Update UI based on new display mode
  document.body.dataset.displayMode = mode;
  
  // Trigger layout recalculation
  window.dispatchEvent(new Event("displaymodechange"));
}

Example: CSS for different display modes

/* Default browser mode styles */
.app-header {
  display: flex;
  padding: 1rem;
}

/* Standalone mode (installed PWA) */
@media (display-mode: standalone) {
  .app-header {
    padding-top: env(safe-area-inset-top);
    background: var(--theme-color);
  }
  
  .install-button {
    display: none; /* Hide install button when installed */
  }
  
  .pwa-features {
    display: block; /* Show PWA-specific features */
  }
}

/* Fullscreen mode */
@media (display-mode: fullscreen) {
  .app-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;
  }
  
  .navigation-controls {
    display: flex; /* Show custom nav controls */
  }
}

/* Minimal UI mode */
@media (display-mode: minimal-ui) {
  .app-header {
    padding-top: 0.5rem;
  }
}

/* Browser mode */
@media (display-mode: browser) {
  .pwa-features {
    display: none;
  }
  
  .install-promotion {
    display: block;
  }
}

/* Combine with other media queries */
@media (display-mode: standalone) and (max-width: 768px) {
  /* Standalone mobile styles */
  .app-header {
    flex-direction: column;
  }
}
Note: Display mode indicates how PWA is being viewed. Use matchMedia("(display-mode: MODE)") to detect mode. Apply mode-specific features and styles. Common pattern: hide install button in standalone mode, show custom navigation in fullscreen.

17.6 Install Events and App Lifecycle Management

Event Description
beforeinstallprompt Fired when browser wants to show install prompt.
appinstalled Fired when PWA is successfully installed.
DOMContentLoaded Fired when initial HTML is loaded and parsed.
load Fired when all resources are loaded.
visibilitychange Fired when page visibility changes (app backgrounded/foregrounded).
pagehide / pageshow Fired when page is hidden/shown (mobile app switching).

Example: Complete PWA lifecycle management

class PWALifecycleManager {
  constructor() {
    this.isInstalled = this.checkInstalled();
    this.installPrompt = null;
    
    this.init();
  }
  
  init() {
    // Installation events
    window.addEventListener("beforeinstallprompt", this.handleBeforeInstall.bind(this));
    window.addEventListener("appinstalled", this.handleAppInstalled.bind(this));
    
    // Lifecycle events
    document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this));
    window.addEventListener("pageshow", this.handlePageShow.bind(this));
    window.addEventListener("pagehide", this.handlePageHide.bind(this));
    
    // Network events
    window.addEventListener("online", this.handleOnline.bind(this));
    window.addEventListener("offline", this.handleOffline.bind(this));
    
    // Focus events
    window.addEventListener("focus", this.handleFocus.bind(this));
    window.addEventListener("blur", this.handleBlur.bind(this));
  }
  
  checkInstalled() {
    // Check if running in standalone mode
    return window.matchMedia("(display-mode: standalone)").matches ||
           window.navigator.standalone === true;
  }
  
  handleBeforeInstall(event) {
    console.log("Install prompt available");
    event.preventDefault();
    this.installPrompt = event;
    
    // Show custom install UI
    this.showInstallUI();
    
    // Track
    this.trackEvent("install_prompt_shown");
  }
  
  async showInstallPrompt() {
    if (!this.installPrompt) {
      console.log("No install prompt available");
      return;
    }
    
    // Show prompt
    this.installPrompt.prompt();
    
    // Wait for user choice
    const result = await this.installPrompt.userChoice;
    
    if (result.outcome === "accepted") {
      this.trackEvent("install_accepted");
    } else {
      this.trackEvent("install_dismissed");
    }
    
    this.installPrompt = null;
  }
  
  handleAppInstalled(event) {
    console.log("App installed successfully");
    this.isInstalled = true;
    
    // Hide install UI
    this.hideInstallUI();
    
    // Track installation
    this.trackEvent("app_installed");
    
    // Show welcome message
    this.showWelcomeMessage();
  }
  
  handleVisibilityChange() {
    if (document.hidden) {
      console.log("App went to background");
      this.onBackground();
    } else {
      console.log("App came to foreground");
      this.onForeground();
    }
  }
  
  handlePageShow(event) {
    console.log("Page shown");
    
    // Check if restored from cache
    if (event.persisted) {
      console.log("Page restored from bfcache");
      this.onPageRestore();
    }
  }
  
  handlePageHide(event) {
    console.log("Page hidden");
    this.saveState();
  }
  
  handleOnline() {
    console.log("Connection restored");
    this.onOnline();
    this.showNotification("You're back online");
  }
  
  handleOffline() {
    console.log("Connection lost");
    this.onOffline();
    this.showNotification("You're offline. Some features may be limited.");
  }
  
  handleFocus() {
    console.log("App focused");
    this.checkForUpdates();
  }
  
  handleBlur() {
    console.log("App lost focus");
  }
  
  // Lifecycle hooks
  onBackground() {
    // Pause non-critical operations
    this.pauseTimers();
    this.trackEvent("app_backgrounded");
  }
  
  onForeground() {
    // Resume operations
    this.resumeTimers();
    this.refreshData();
    this.trackEvent("app_foregrounded");
  }
  
  onPageRestore() {
    // Refresh stale data
    this.refreshData();
  }
  
  onOnline() {
    // Sync pending changes
    this.syncPendingData();
  }
  
  onOffline() {
    // Switch to offline mode
    this.enableOfflineMode();
  }
  
  saveState() {
    // Save current state
    const state = {
      timestamp: Date.now(),
      route: window.location.pathname,
      scrollPosition: window.scrollY
    };
    
    localStorage.setItem("app_state", JSON.stringify(state));
  }
  
  restoreState() {
    const savedState = localStorage.getItem("app_state");
    if (savedState) {
      const state = JSON.parse(savedState);
      // Restore scroll position, etc.
      window.scrollTo(0, state.scrollPosition);
    }
  }
  
  trackEvent(eventName, data = {}) {
    if (window.analytics) {
      window.analytics.track(eventName, {
        ...data,
        isInstalled: this.isInstalled,
        displayMode: this.getDisplayMode()
      });
    }
  }
  
  getDisplayMode() {
    const modes = ["fullscreen", "standalone", "minimal-ui", "browser"];
    for (const mode of modes) {
      if (window.matchMedia(`(display-mode: ${mode})`).matches) {
        return mode;
      }
    }
    return "browser";
  }
}

// Initialize
const pwaLifecycle = new PWALifecycleManager();

// Usage
document.getElementById("install-button").addEventListener("click", () => {
  pwaLifecycle.showInstallPrompt();
});

Example: Update notification for PWA

// Service worker update detection
let newWorker;

navigator.serviceWorker.register("/sw.js").then(registration => {
  // Check for updates periodically
  setInterval(() => {
    registration.update();
  }, 60 * 60 * 1000); // Check every hour
  
  // Listen for updates
  registration.addEventListener("updatefound", () => {
    newWorker = registration.installing;
    
    newWorker.addEventListener("statechange", () => {
      if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
        // New version available
        showUpdateNotification();
      }
    });
  });
});

// Show update notification
function showUpdateNotification() {
  const notification = document.createElement("div");
  notification.className = "update-notification";
  notification.innerHTML = `
    <p>A new version is available!</p>
    <button onclick="updateApp()">Update Now</button>
    <button onclick="dismissUpdate()">Later</button>
  `;
  document.body.appendChild(notification);
}

// Apply update
function updateApp() {
  if (newWorker) {
    newWorker.postMessage({ type: "SKIP_WAITING" });
  }
  
  // Reload page when new worker takes control
  navigator.serviceWorker.addEventListener("controllerchange", () => {
    window.location.reload();
  });
}

// In service worker (sw.js)
self.addEventListener("message", (event) => {
  if (event.data.type === "SKIP_WAITING") {
    self.skipWaiting();
  }
});
Note: PWA lifecycle includes install, background, foreground, and network events. Use visibilitychange to detect background/foreground. Handle online/offline events for connectivity. Save state on pagehide, restore on pageshow. Check for updates on focus.
Warning: iOS Safari has limited PWA lifecycle events. beforeinstallprompt not available on iOS. Use visibilitychange instead of pagehide/pageshow on some browsers. Test lifecycle thoroughly on target platforms.

Progressive Web App APIs Best Practices

  • Web App Manifest: Include all required fields (name, icons, start_url, display)
  • Provide multiple icon sizes (72px to 512px) and maskable icons for Android
  • Use HTTPS - required for PWA features and service workers
  • Implement custom install flow with beforeinstallprompt for better UX
  • Don't show install prompt immediately - wait for user engagement
  • Track install metrics: prompt shown, accepted, dismissed, installed
  • Detect display mode and adjust UI accordingly (hide install button when installed)
  • Window Controls Overlay: Use for native-like desktop experience
  • App Shortcuts: Provide quick access to key features (max 4 recommended)
  • Handle lifecycle events: visibility change, online/offline, focus/blur
  • Implement update notification when new version available
  • Save/restore app state on page hide/show for better UX
  • Test on all target platforms - iOS Safari has different PWA behavior

18. Browser Feature Detection APIs

18.1 CSS.supports() for CSS Feature Detection

Method Description
CSS.supports(property, value) Check if browser supports a CSS property-value pair.
CSS.supports(conditionText) Check if browser supports a CSS condition string.
@supports CSS rule Conditional CSS rules based on feature support.

Example: Check CSS property support

// Check individual property-value pairs
const supportsGrid = CSS.supports("display", "grid");
console.log("Grid support:", supportsGrid);
// Output: true (in modern browsers)

const supportsSubgrid = CSS.supports("grid-template-rows", "subgrid");
console.log("Subgrid support:", supportsSubgrid);
// Output: true/false depending on browser

const supportsGap = CSS.supports("gap", "1rem");
console.log("Gap support:", supportsGap);
// Output: true

// Check with condition strings
const supportsFlexGap = CSS.supports("(gap: 1rem) and (display: flex)");
console.log("Flex gap support:", supportsFlexGap);

const supportsContainerQueries = CSS.supports("container-type: inline-size");
console.log("Container queries:", supportsContainerQueries);

const supportsAspectRatio = CSS.supports("aspect-ratio: 16 / 9");
console.log("Aspect ratio:", supportsAspectRatio);

// Check vendor prefixes
const supportsSticky = CSS.supports("position", "sticky") ||
                       CSS.supports("position", "-webkit-sticky");
console.log("Sticky support:", supportsSticky);

// Complex conditions with OR/AND
const supportsModernLayout = CSS.supports(
  "(display: grid) or (display: flex)"
);
console.log("Modern layout:", supportsModernLayout);

// Check custom properties
const supportsCustomProps = CSS.supports("--custom-prop", "value");
console.log("Custom properties:", supportsCustomProps);
// Output: true (in modern browsers)

Example: Feature detection with fallbacks

// Grid with flexbox fallback
function applyLayout() {
  const container = document.querySelector(".container");
  
  if (CSS.supports("display", "grid")) {
    container.classList.add("grid-layout");
    console.log("Using CSS Grid");
  } else if (CSS.supports("display", "flex")) {
    container.classList.add("flex-layout");
    console.log("Using Flexbox fallback");
  } else {
    container.classList.add("float-layout");
    console.log("Using float fallback");
  }
}

// Check multiple features
const features = {
  grid: CSS.supports("display", "grid"),
  subgrid: CSS.supports("grid-template-rows", "subgrid"),
  containerQueries: CSS.supports("container-type", "inline-size"),
  aspectRatio: CSS.supports("aspect-ratio", "1"),
  gap: CSS.supports("gap", "1rem"),
  backdropFilter: CSS.supports("backdrop-filter", "blur(10px)"),
  scrollSnap: CSS.supports("scroll-snap-type", "y mandatory"),
  logicalProps: CSS.supports("margin-inline-start", "1rem")
};

console.log("Browser features:", features);

// Apply feature classes to html element
const html = document.documentElement;
Object.keys(features).forEach(feature => {
  if (features[feature]) {
    html.classList.add(`supports-${feature}`);
  } else {
    html.classList.add(`no-${feature}`);
  }
});

// Check color functions
const supportsOklch = CSS.supports("color", "oklch(0.5 0.2 180)");
const supportsP3 = CSS.supports("color", "color(display-p3 1 0 0)");

console.log("Modern color support:", {
  oklch: supportsOklch,
  displayP3: supportsP3
});

Example: CSS @supports in stylesheets

/* Basic @supports */
@supports (display: grid) {
  .container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 1rem;
  }
}

/* Fallback for older browsers */
@supports not (display: grid) {
  .container {
    display: flex;
    flex-wrap: wrap;
    margin: -0.5rem;
  }
  
  .container > * {
    margin: 0.5rem;
    flex: 1 1 250px;
  }
}

/* Complex conditions */
@supports (display: grid) and (gap: 1rem) {
  .modern-grid {
    display: grid;
    gap: 1rem;
  }
}

/* OR conditions */
@supports (position: sticky) or (position: -webkit-sticky) {
  .sticky-header {
    position: sticky;
    position: -webkit-sticky;
    top: 0;
  }
}

/* Container queries */
@supports (container-type: inline-size) {
  .card-container {
    container-type: inline-size;
  }
  
  @container (min-width: 400px) {
    .card {
      display: flex;
    }
  }
}

/* Modern color spaces */
@supports (color: oklch(0 0 0)) {
  .modern-colors {
    background: oklch(0.7 0.15 180);
  }
}

/* Backdrop filter with fallback */
.modal-backdrop {
  background: rgba(0, 0, 0, 0.5);
}

@supports (backdrop-filter: blur(10px)) {
  .modal-backdrop {
    background: rgba(0, 0, 0, 0.3);
    backdrop-filter: blur(10px);
  }
}
Note: CSS.supports() enables runtime CSS feature detection. Returns boolean indicating browser support. Use two-argument form for property-value pairs or single argument for condition strings. Combine with @supports in CSS for progressive enhancement.

18.2 Navigator.userAgent and Feature Sniffing

Property Description
navigator.userAgent User agent string identifying browser and OS.
navigator.vendor Browser vendor (e.g., "Google Inc.", "Apple Computer, Inc.").
navigator.platform Platform string (deprecated, use userAgentData).
navigator.userAgentData Structured user agent data (modern, limited browser support).
navigator.userAgentData.brands Array of browser brand information.
navigator.userAgentData.mobile Boolean indicating mobile device.
navigator.userAgentData.platform Operating system platform.
// Get user agent string
const ua = navigator.userAgent;
console.log("User Agent:", ua);
// Example output:
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..."

// Detect browser (not recommended - use feature detection instead)
const isChrome = ua.includes("Chrome") && !ua.includes("Edg");
const isFirefox = ua.includes("Firefox");
const isSafari = ua.includes("Safari") && !ua.includes("Chrome");
const isEdge = ua.includes("Edg");

console.log("Browser detection:", {
  chrome: isChrome,
  firefox: isFirefox,
  safari: isSafari,
  edge: isEdge
});

// Detect OS (unreliable)
const isMac = ua.includes("Mac OS X");
const isWindows = ua.includes("Windows");
const isLinux = ua.includes("Linux");
const isIOS = /iPad|iPhone|iPod/.test(ua);
const isAndroid = ua.includes("Android");

console.log("OS detection:", {
  mac: isMac,
  windows: isWindows,
  linux: isLinux,
  iOS: isIOS,
  android: isAndroid
});

// Detect mobile (unreliable)
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
console.log("Is mobile:", isMobile);

// Better: Use feature detection instead
const hasTouchScreen = "ontouchstart" in window || navigator.maxTouchPoints > 0;
const isLikelyMobile = hasTouchScreen && window.innerWidth < 768;

console.log("Feature-based mobile detection:", isLikelyMobile);

Example: Modern User-Agent Client Hints API

// Check if User-Agent Client Hints available
if (navigator.userAgentData) {
  console.log("User-Agent Client Hints supported");
  
  // Basic info (available immediately)
  const { brands, mobile, platform } = navigator.userAgentData;
  
  console.log("Brands:", brands);
  // Output: [{brand: "Chromium", version: "120"}, {brand: "Google Chrome", version: "120"}]
  
  console.log("Is mobile:", mobile);
  // Output: false
  
  console.log("Platform:", platform);
  // Output: "macOS"
  
  // Get high-entropy values (requires permission)
  navigator.userAgentData.getHighEntropyValues([
    "platformVersion",
    "architecture",
    "model",
    "uaFullVersion",
    "fullVersionList"
  ]).then(values => {
    console.log("High entropy values:", values);
    // Output: {
    //   platformVersion: "13.0.0",
    //   architecture: "arm",
    //   model: "",
    //   uaFullVersion: "120.0.6099.109",
    //   fullVersionList: [...]
    // }
  });
  
} else {
  console.log("User-Agent Client Hints not supported");
  console.log("Fallback to user agent string:", navigator.userAgent);
}

// Better approach: Feature detection
function getBrowserCapabilities() {
  return {
    // Feature detection instead of UA sniffing
    supportsWebP: document.createElement("canvas")
      .toDataURL("image/webp").indexOf("data:image/webp") === 0,
    supportsWebGL: !!document.createElement("canvas").getContext("webgl"),
    supportsServiceWorker: "serviceWorker" in navigator,
    supportsWebRTC: "RTCPeerConnection" in window,
    supportsWebAssembly: typeof WebAssembly === "object",
    supportsES6Modules: "noModule" in document.createElement("script"),
    maxTouchPoints: navigator.maxTouchPoints || 0,
    deviceMemory: navigator.deviceMemory || "unknown",
    hardwareConcurrency: navigator.hardwareConcurrency || "unknown",
    connection: navigator.connection?.effectiveType || "unknown"
  };
}

console.log("Browser capabilities:", getBrowserCapabilities());
Note: Avoid user agent sniffing - use feature detection instead. User agent strings are unreliable and easily spoofed. Prefer navigator.userAgentData (Client Hints) for modern browsers. Always detect features, not browsers.
Warning: User agent sniffing is an anti-pattern. UA strings can be spoofed, change frequently, and don't reflect actual capabilities. Use feature detection with CSS.supports(), checking for API existence, or try/catch. Only use UA as last resort for critical bugs in specific browsers.

18.3 MediaCapabilities API for Media Format Support

Method Description
navigator.mediaCapabilities.decodingInfo(config) Check if browser can decode media configuration efficiently.
navigator.mediaCapabilities.encodingInfo(config) Check if browser can encode media configuration efficiently.
Configuration Properties Description
type "file" or "media-source" for decoding; "record" or "transmission" for encoding.
video Video configuration: contentType, width, height, bitrate, framerate.
audio Audio configuration: contentType, channels, bitrate, samplerate.

Example: Check video format support

// Check if browser can play specific video format
async function checkVideoSupport() {
  // H.264 configuration
  const h264Config = {
    type: "file",
    video: {
      contentType: "video/mp4; codecs=avc1.42E01E",
      width: 1920,
      height: 1080,
      bitrate: 5000000, // 5 Mbps
      framerate: 30
    }
  };
  
  // VP9 configuration
  const vp9Config = {
    type: "file",
    video: {
      contentType: "video/webm; codecs=vp9",
      width: 1920,
      height: 1080,
      bitrate: 5000000,
      framerate: 30
    }
  };
  
  // AV1 configuration
  const av1Config = {
    type: "file",
    video: {
      contentType: "video/mp4; codecs=av01.0.05M.08",
      width: 1920,
      height: 1080,
      bitrate: 3000000,
      framerate: 30
    }
  };
  
  try {
    const h264Info = await navigator.mediaCapabilities.decodingInfo(h264Config);
    const vp9Info = await navigator.mediaCapabilities.decodingInfo(vp9Config);
    const av1Info = await navigator.mediaCapabilities.decodingInfo(av1Config);
    
    console.log("H.264 support:", {
      supported: h264Info.supported,
      smooth: h264Info.smooth,
      powerEfficient: h264Info.powerEfficient
    });
    
    console.log("VP9 support:", {
      supported: vp9Info.supported,
      smooth: vp9Info.smooth,
      powerEfficient: vp9Info.powerEfficient
    });
    
    console.log("AV1 support:", {
      supported: av1Info.supported,
      smooth: av1Info.smooth,
      powerEfficient: av1Info.powerEfficient
    });
    
    // Choose best format
    if (av1Info.supported && av1Info.smooth && av1Info.powerEfficient) {
      return "av1";
    } else if (vp9Info.supported && vp9Info.smooth) {
      return "vp9";
    } else {
      return "h264";
    }
    
  } catch (error) {
    console.error("MediaCapabilities not supported:", error);
    // Fallback to canPlayType
    return fallbackFormatDetection();
  }
}

// Legacy fallback using canPlayType
function fallbackFormatDetection() {
  const video = document.createElement("video");
  
  if (video.canPlayType('video/mp4; codecs="avc1.42E01E"')) {
    return "h264";
  } else if (video.canPlayType('video/webm; codecs="vp9"')) {
    return "vp9";
  } else {
    return "unknown";
  }
}

// Usage
checkVideoSupport().then(format => {
  console.log("Selected video format:", format);
  loadVideoWithFormat(format);
});

Example: Check audio format and select best quality

// Check audio codec support
async function checkAudioSupport() {
  const formats = [
    {
      name: "opus",
      config: {
        type: "file",
        audio: {
          contentType: "audio/webm; codecs=opus",
          channels: 2,
          bitrate: 128000,
          samplerate: 48000
        }
      }
    },
    {
      name: "aac",
      config: {
        type: "file",
        audio: {
          contentType: "audio/mp4; codecs=mp4a.40.2",
          channels: 2,
          bitrate: 128000,
          samplerate: 44100
        }
      }
    },
    {
      name: "vorbis",
      config: {
        type: "file",
        audio: {
          contentType: "audio/ogg; codecs=vorbis",
          channels: 2,
          bitrate: 128000,
          samplerate: 44100
        }
      }
    }
  ];
  
  const results = {};
  
  for (const format of formats) {
    try {
      const info = await navigator.mediaCapabilities.decodingInfo(format.config);
      results[format.name] = info;
      console.log(`${format.name}:`, info);
    } catch (error) {
      console.error(`Failed to check ${format.name}:`, error);
      results[format.name] = { supported: false };
    }
  }
  
  return results;
}

// Adaptive media source selection
async function selectMediaSource(videoSources) {
  const capabilities = await Promise.all(
    videoSources.map(async (source) => {
      const info = await navigator.mediaCapabilities.decodingInfo({
        type: "media-source",
        video: {
          contentType: source.mimeType,
          width: source.width,
          height: source.height,
          bitrate: source.bitrate,
          framerate: source.framerate
        }
      });
      
      return { ...source, ...info };
    })
  );
  
  // Filter to supported, smooth, and power-efficient
  const ideal = capabilities.filter(c => 
    c.supported && c.smooth && c.powerEfficient
  );
  
  if (ideal.length > 0) {
    // Pick highest quality from ideal
    return ideal.reduce((best, current) => 
      current.bitrate > best.bitrate ? current : best
    );
  }
  
  // Fallback to any supported and smooth
  const acceptable = capabilities.filter(c => c.supported && c.smooth);
  
  if (acceptable.length > 0) {
    return acceptable[0];
  }
  
  // Last resort: any supported
  const anySupported = capabilities.find(c => c.supported);
  return anySupported || capabilities[0];
}

// Example usage
const sources = [
  {
    url: "video-4k-av1.mp4",
    mimeType: "video/mp4; codecs=av01.0.05M.08",
    width: 3840,
    height: 2160,
    bitrate: 10000000,
    framerate: 30
  },
  {
    url: "video-1080p-h264.mp4",
    mimeType: "video/mp4; codecs=avc1.42E01E",
    width: 1920,
    height: 1080,
    bitrate: 5000000,
    framerate: 30
  },
  {
    url: "video-720p-h264.mp4",
    mimeType: "video/mp4; codecs=avc1.42E01E",
    width: 1280,
    height: 720,
    bitrate: 2500000,
    framerate: 30
  }
];

selectMediaSource(sources).then(selected => {
  console.log("Selected source:", selected.url);
  videoElement.src = selected.url;
});
Note: MediaCapabilities API provides detailed media format support info. Returns supported, smooth, and powerEfficient booleans. Use to select optimal video/audio format based on device capabilities. More accurate than canPlayType().

18.4 Feature Policy and Permissions Policy APIs

API Description
document.featurePolicy Feature Policy API (deprecated, use Permissions Policy).
document.featurePolicy.allowsFeature(feature) Check if feature is allowed in current context.
Permissions-Policy HTTP header Control which features can be used (modern approach).
allow attribute on iframe Grant specific permissions to embedded content.

Example: Check feature policy

// Check if Feature Policy API is available
if (document.featurePolicy) {
  console.log("Feature Policy supported");
  
  // Check specific features
  const features = [
    "camera",
    "microphone",
    "geolocation",
    "payment",
    "autoplay",
    "fullscreen",
    "picture-in-picture",
    "accelerometer",
    "gyroscope",
    "magnetometer"
  ];
  
  features.forEach(feature => {
    const allowed = document.featurePolicy.allowsFeature(feature);
    console.log(`${feature}:`, allowed ? "allowed" : "blocked");
  });
  
  // Check with origin
  const allowedForOrigin = document.featurePolicy.allowsFeature(
    "geolocation",
    "https://example.com"
  );
  console.log("Geolocation for example.com:", allowedForOrigin);
  
  // Get all allowed features
  const allowedFeatures = document.featurePolicy.allowedFeatures();
  console.log("Allowed features:", allowedFeatures);
  
  // Get features list
  const allFeatures = document.featurePolicy.features();
  console.log("All features:", allFeatures);
  
} else {
  console.log("Feature Policy not supported");
}

// Modern approach: Check permissions before using
async function requestCameraWithPolicyCheck() {
  // First check if allowed by policy
  if (document.featurePolicy && !document.featurePolicy.allowsFeature("camera")) {
    console.error("Camera blocked by feature policy");
    return null;
  }
  
  // Then request permission
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
    return stream;
  } catch (error) {
    console.error("Camera access denied:", error);
    return null;
  }
}

Example: Set Permissions Policy in HTTP headers and HTML

# HTTP Response Header (modern, recommended)
Permissions-Policy: 
  camera=(self "https://trusted.com"),
  microphone=(self),
  geolocation=(),
  payment=(self),
  autoplay=*,
  fullscreen=*,
  picture-in-picture=*

# Legacy Feature-Policy header (deprecated)
Feature-Policy: 
  camera 'self' https://trusted.com;
  microphone 'self';
  geolocation 'none';
  payment 'self'

Example: Use allow attribute on iframes

<!-- Grant specific permissions to iframe -->
<iframe
  src="https://example.com/embed"
  allow="camera; microphone; fullscreen; payment"
></iframe>

<!-- Allow all permissions (not recommended) -->
<iframe
  src="https://example.com/embed"
  allow="camera *; microphone *; geolocation *"
></iframe>

<!-- Explicitly block features -->
<iframe
  src="https://example.com/embed"
  allow="fullscreen"
  sandbox="allow-scripts allow-same-origin"
></iframe>

<!-- Combined with sandbox -->
<iframe
  src="https://example.com/payment"
  allow="payment"
  sandbox="allow-scripts allow-forms allow-same-origin"
></iframe>
Note: Permissions Policy (formerly Feature Policy) controls access to powerful features. Set via HTTP header or iframe allow attribute. Use document.featurePolicy.allowsFeature() to check before requesting permissions. Helps prevent unauthorized feature access.
Warning: Feature Policy API is deprecated - use Permissions Policy instead. Check browser support for specific policies. Some features like geolocation require both policy allowance AND user permission. Always handle graceful degradation.

18.5 Navigator Properties and Capability Detection

Property Description
navigator.onLine Boolean indicating network connectivity.
navigator.language Preferred language (e.g., "en-US").
navigator.languages Array of preferred languages.
navigator.hardwareConcurrency Number of logical processor cores.
navigator.deviceMemory Approximate device RAM in GB.
navigator.connection Network Information API object.
navigator.maxTouchPoints Maximum number of simultaneous touch points.
navigator.cookieEnabled Boolean indicating if cookies are enabled.
navigator.pdfViewerEnabled Boolean indicating if built-in PDF viewer available.

Example: Detect device capabilities

// Comprehensive capability detection
function getDeviceCapabilities() {
  const capabilities = {
    // Network
    online: navigator.onLine,
    connection: navigator.connection ? {
      effectiveType: navigator.connection.effectiveType,
      downlink: navigator.connection.downlink,
      rtt: navigator.connection.rtt,
      saveData: navigator.connection.saveData
    } : null,
    
    // Language
    language: navigator.language,
    languages: navigator.languages,
    
    // Hardware
    hardwareConcurrency: navigator.hardwareConcurrency,
    deviceMemory: navigator.deviceMemory,
    maxTouchPoints: navigator.maxTouchPoints,
    
    // Features
    cookieEnabled: navigator.cookieEnabled,
    pdfViewerEnabled: navigator.pdfViewerEnabled || false,
    
    // Screen
    screenSize: {
      width: window.screen.width,
      height: window.screen.height,
      availWidth: window.screen.availWidth,
      availHeight: window.screen.availHeight,
      colorDepth: window.screen.colorDepth,
      pixelDepth: window.screen.pixelDepth
    },
    
    // Viewport
    viewport: {
      width: window.innerWidth,
      height: window.innerHeight,
      devicePixelRatio: window.devicePixelRatio
    },
    
    // Storage
    storage: "storage" in navigator && "estimate" in navigator.storage,
    
    // Media
    mediaDevices: "mediaDevices" in navigator,
    
    // APIs
    serviceWorker: "serviceWorker" in navigator,
    geolocation: "geolocation" in navigator,
    bluetooth: "bluetooth" in navigator,
    usb: "usb" in navigator,
    webGL: !!document.createElement("canvas").getContext("webgl"),
    webGL2: !!document.createElement("canvas").getContext("webgl2"),
    webGPU: "gpu" in navigator,
    
    // Input
    pointerEvents: "PointerEvent" in window,
    touchEvents: "ontouchstart" in window
  };
  
  return capabilities;
}

// Use capabilities for adaptive loading
const caps = getDeviceCapabilities();
console.log("Device capabilities:", caps);

// Adapt based on capabilities
if (caps.deviceMemory && caps.deviceMemory < 4) {
  console.log("Low memory device - loading lightweight version");
  loadLightweightAssets();
}

if (caps.connection?.saveData) {
  console.log("Data saver mode active");
  disableAutoplayVideos();
  loadCompressedImages();
}

if (caps.hardwareConcurrency && caps.hardwareConcurrency < 4) {
  console.log("Limited CPU - reducing animations");
  reducedMotion();
}

// Monitor network changes
if (navigator.connection) {
  navigator.connection.addEventListener("change", () => {
    console.log("Connection changed:", {
      type: navigator.connection.effectiveType,
      downlink: navigator.connection.downlink,
      rtt: navigator.connection.rtt
    });
    
    adaptToConnection(navigator.connection);
  });
}

// Monitor online/offline
window.addEventListener("online", () => {
  console.log("Back online");
  syncOfflineData();
});

window.addEventListener("offline", () => {
  console.log("Gone offline");
  enableOfflineMode();
});

Example: Feature detection utilities

// Comprehensive feature detection
const Features = {
  // Storage APIs
  localStorage: (() => {
    try {
      const test = "__test__";
      localStorage.setItem(test, test);
      localStorage.removeItem(test);
      return true;
    } catch (e) {
      return false;
    }
  })(),
  
  indexedDB: "indexedDB" in window,
  
  // Worker APIs
  webWorker: "Worker" in window,
  serviceWorker: "serviceWorker" in navigator,
  sharedWorker: "SharedWorker" in window,
  
  // Modern APIs
  fetch: "fetch" in window,
  promise: "Promise" in window,
  intersectionObserver: "IntersectionObserver" in window,
  resizeObserver: "ResizeObserver" in window,
  mutationObserver: "MutationObserver" in window,
  
  // Media APIs
  webRTC: "RTCPeerConnection" in window,
  mediaRecorder: "MediaRecorder" in window,
  webAudio: "AudioContext" in window || "webkitAudioContext" in window,
  
  // Graphics
  canvas: (() => {
    const canvas = document.createElement("canvas");
    return !!(canvas.getContext && canvas.getContext("2d"));
  })(),
  
  webGL: (() => {
    const canvas = document.createElement("canvas");
    return !!(
      canvas.getContext("webgl") ||
      canvas.getContext("experimental-webgl")
    );
  })(),
  
  // Advanced features
  webAssembly: typeof WebAssembly === "object",
  webGL2: (() => {
    const canvas = document.createElement("canvas");
    return !!canvas.getContext("webgl2");
  })(),
  
  // Input
  touchEvents: "ontouchstart" in window,
  pointerEvents: "PointerEvent" in window,
  
  // Performance
  performanceObserver: "PerformanceObserver" in window,
  
  // Sensors
  deviceOrientation: "DeviceOrientationEvent" in window,
  deviceMotion: "DeviceMotionEvent" in window,
  
  // Permissions
  permissions: "permissions" in navigator,
  
  // Payment
  paymentRequest: "PaymentRequest" in window,
  
  // Credentials
  credentials: "credentials" in navigator,
  webAuthn: "credentials" in navigator && "create" in navigator.credentials,
  
  // Clipboard
  clipboardAPI: "clipboard" in navigator,
  
  // Share
  webShare: "share" in navigator
};

console.log("Feature support:", Features);

// Add feature classes to HTML
const html = document.documentElement;
Object.keys(Features).forEach(feature => {
  html.classList.add(Features[feature] ? feature : `no-${feature}`);
});

// Export for use in app
export default Features;
Note: Navigator properties provide device and browser capability info. Use hardwareConcurrency and deviceMemory for performance optimization. Check connection for adaptive loading. Feature detection more reliable than UA sniffing.

18.6 Modernizr Integration Patterns and Polyfills

Concept Description
Modernizr Feature detection library that adds CSS classes and JavaScript object.
Polyfill Code that implements missing browser features.
Progressive Enhancement Build base experience, then add features for capable browsers.
Graceful Degradation Build full experience, provide fallbacks for older browsers.

Example: Modernizr usage pattern

// After including Modernizr script
// Check features via JavaScript
if (Modernizr.webp) {
  console.log("WebP supported");
  loadWebPImages();
} else {
  console.log("WebP not supported");
  loadJPEGImages();
}

// Check multiple features
if (Modernizr.flexbox && Modernizr.cssgrid) {
  console.log("Modern layout supported");
  useModernLayout();
} else {
  console.log("Fallback to float layout");
  useFloatLayout();
}

// Load polyfills conditionally
if (!Modernizr.promises) {
  // Load Promise polyfill
  loadScript("https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js");
}

if (!Modernizr.fetch) {
  // Load fetch polyfill
  loadScript("https://cdn.jsdelivr.net/npm/whatwg-fetch@3/dist/fetch.umd.js");
}

// Helper to load scripts
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

Example: CSS with Modernizr classes

/* Modernizr adds classes to html element */

/* Use CSS Grid when available */
.cssgrid .container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}

/* Fallback to flexbox */
.no-cssgrid .container {
  display: flex;
  flex-wrap: wrap;
  margin: -0.5rem;
}

.no-cssgrid .container > * {
  margin: 0.5rem;
  flex: 1 1 250px;
}

/* WebP images */
.webp .hero {
  background-image: url('hero.webp');
}

.no-webp .hero {
  background-image: url('hero.jpg');
}

/* Object-fit support */
.objectfit img {
  width: 100%;
  height: 300px;
  object-fit: cover;
}

.no-objectfit img {
  width: 100%;
  height: auto;
}

/* Custom properties (CSS variables) */
.customproperties {
  --primary-color: #007bff;
  color: var(--primary-color);
}

.no-customproperties {
  color: #007bff;
}

/* Backdrop filter */
.backdropfilter .modal-backdrop {
  backdrop-filter: blur(10px);
  background: rgba(0, 0, 0, 0.3);
}

.no-backdropfilter .modal-backdrop {
  background: rgba(0, 0, 0, 0.7);
}

Example: Manual polyfill loading strategy

// Polyfill loader with feature detection
async function loadPolyfills() {
  const polyfills = [];
  
  // Check and load required polyfills
  if (!window.Promise) {
    polyfills.push(import("promise-polyfill"));
  }
  
  if (!window.fetch) {
    polyfills.push(import("whatwg-fetch"));
  }
  
  if (!window.IntersectionObserver) {
    polyfills.push(import("intersection-observer"));
  }
  
  if (!window.ResizeObserver) {
    polyfills.push(import("resize-observer-polyfill"));
  }
  
  if (!("remove" in Element.prototype)) {
    Element.prototype.remove = function() {
      if (this.parentNode) {
        this.parentNode.removeChild(this);
      }
    };
  }
  
  if (!("prepend" in Element.prototype)) {
    Element.prototype.prepend = function(...nodes) {
      const fragment = document.createDocumentFragment();
      nodes.forEach(node => {
        fragment.appendChild(
          node instanceof Node ? node : document.createTextNode(String(node))
        );
      });
      this.insertBefore(fragment, this.firstChild);
    };
  }
  
  // Wait for all polyfills to load
  await Promise.all(polyfills);
  
  console.log(`Loaded ${polyfills.length} polyfills`);
}

// Initialize app after polyfills
loadPolyfills().then(() => {
  console.log("Polyfills loaded, initializing app");
  initializeApp();
});

// Alternative: Polyfill.io service
// <script src="https://polyfill.io/v3/polyfill.min.js?features=default,fetch,IntersectionObserver"></script>

// Progressive enhancement example
function enhanceWithModernFeatures() {
  // Base functionality works everywhere
  const items = document.querySelectorAll(".item");
  
  // Enhance with Intersection Observer if available
  if ("IntersectionObserver" in window) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          observer.unobserve(entry.target);
        }
      });
    });
    
    items.forEach(item => observer.observe(item));
  } else {
    // Fallback: show all items immediately
    items.forEach(item => item.classList.add("visible"));
  }
}
Note: Modernizr adds feature detection classes to HTML element. Use for progressive enhancement in CSS and JavaScript. Load polyfills only when needed. Consider using dynamic imports or Polyfill.io service for automatic detection.
Warning: Don't over-polyfill - adds bloat and slows page load. Only polyfill features you actually use. Test polyfills thoroughly. Some features can't be polyfilled (e.g., CSS Grid in IE). Consider if supporting old browsers is worth the cost.

Browser Feature Detection Best Practices

  • Always use feature detection, never browser detection (user agent sniffing)
  • CSS.supports() is the best way to detect CSS feature support
  • Check for API existence before using: if ("fetch" in window)
  • Use MediaCapabilities API for optimal media format selection
  • Combine multiple detection methods for robust feature checking
  • Leverage navigator properties (deviceMemory, hardwareConcurrency) for adaptive experiences
  • Use Permissions Policy to control feature access in iframes
  • Load polyfills conditionally - only when features are missing
  • Prefer progressive enhancement over graceful degradation
  • Add feature classes to HTML element for CSS targeting
  • Test feature detection code in multiple browsers
  • Consider build-time feature detection with Modernizr custom builds
  • Monitor connection quality and adapt content loading accordingly

19. Experimental and Emerging APIs

19.1 Web GPU API for Graphics Computing

WebGPU Concept Description
GPU Adapter Represents physical GPU device. Access via navigator.gpu.requestAdapter().
GPU Device Logical connection to GPU for command submission. Request from adapter.
Shader Module Compiled WGSL (WebGPU Shading Language) code for GPU execution.
Pipeline Complete GPU program including shaders, state, and resource bindings.
Command Encoder Records GPU commands to submit to queue.
Buffer GPU memory for vertex data, uniforms, or compute results.
Texture Multi-dimensional data (images) for rendering or compute.

Example: Initialize WebGPU

// Check WebGPU support
if (!navigator.gpu) {
  console.error("WebGPU not supported");
  showWebGLFallback();
} else {
  console.log("WebGPU supported");
  initWebGPU();
}

async function initWebGPU() {
  // Request GPU adapter
  const adapter = await navigator.gpu.requestAdapter();
  
  if (!adapter) {
    console.error("Failed to get GPU adapter");
    return;
  }
  
  console.log("Adapter info:", {
    vendor: adapter.info?.vendor || "unknown",
    architecture: adapter.info?.architecture || "unknown",
    features: Array.from(adapter.features)
  });
  
  // Request device
  const device = await adapter.requestDevice();
  
  console.log("Device limits:", device.limits);
  console.log("Device features:", Array.from(device.features));
  
  // Handle device loss
  device.lost.then((info) => {
    console.error("Device lost:", info.message, info.reason);
    
    if (info.reason === "destroyed") {
      console.log("Device intentionally destroyed");
    } else {
      console.error("Device lost unexpectedly, reinitializing...");
      initWebGPU();
    }
  });
  
  return { adapter, device };
}

// Example: Create render pipeline
async function createRenderPipeline(device, canvas) {
  // Configure canvas context
  const context = canvas.getContext("webgpu");
  const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
  
  context.configure({
    device: device,
    format: canvasFormat,
    alphaMode: "premultiplied"
  });
  
  // Vertex shader (WGSL)
  const vertexShaderCode = `
    @vertex
    fn main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
      var pos = array<vec2<f32>, 3>(
        vec2<f32>(0.0, 0.5),
        vec2<f32>(-0.5, -0.5),
        vec2<f32>(0.5, -0.5)
      );
      return vec4<f32>(pos[vertexIndex], 0.0, 1.0);
    }
  `;
  
  // Fragment shader (WGSL)
  const fragmentShaderCode = `
    @fragment
    fn main() -> @location(0) vec4<f32> {
      return vec4<f32>(1.0, 0.0, 0.0, 1.0); // Red color
    }
  `;
  
  // Create shader modules
  const vertexModule = device.createShaderModule({
    label: "Vertex Shader",
    code: vertexShaderCode
  });
  
  const fragmentModule = device.createShaderModule({
    label: "Fragment Shader",
    code: fragmentShaderCode
  });
  
  // Create render pipeline
  const pipeline = device.createRenderPipeline({
    label: "Basic Pipeline",
    layout: "auto",
    vertex: {
      module: vertexModule,
      entryPoint: "main"
    },
    fragment: {
      module: fragmentModule,
      entryPoint: "main",
      targets: [{
        format: canvasFormat
      }]
    },
    primitive: {
      topology: "triangle-list"
    }
  });
  
  return { context, pipeline, canvasFormat };
}

Example: WebGPU compute shader

// Compute shader for parallel processing
async function runComputeShader(device) {
  // Compute shader code (matrix multiplication example)
  const computeShaderCode = `
    @group(0) @binding(0) var<storage, read> inputA: array<f32>;
    @group(0) @binding(1) var<storage, read> inputB: array<f32>;
    @group(0) @binding(2) var<storage, read_write> output: array<f32>;
    
    @compute @workgroup_size(64)
    fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
      let index = global_id.x;
      output[index] = inputA[index] + inputB[index];
    }
  `;
  
  // Create compute pipeline
  const shaderModule = device.createShaderModule({
    code: computeShaderCode
  });
  
  const computePipeline = device.createComputePipeline({
    layout: "auto",
    compute: {
      module: shaderModule,
      entryPoint: "main"
    }
  });
  
  // Create buffers
  const dataSize = 1024;
  const bufferSize = dataSize * Float32Array.BYTES_PER_ELEMENT;
  
  const inputABuffer = device.createBuffer({
    size: bufferSize,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
  });
  
  const inputBBuffer = device.createBuffer({
    size: bufferSize,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
  });
  
  const outputBuffer = device.createBuffer({
    size: bufferSize,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
  });
  
  const stagingBuffer = device.createBuffer({
    size: bufferSize,
    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
  });
  
  // Write input data
  const inputDataA = new Float32Array(dataSize).fill(1.0);
  const inputDataB = new Float32Array(dataSize).fill(2.0);
  
  device.queue.writeBuffer(inputABuffer, 0, inputDataA);
  device.queue.writeBuffer(inputBBuffer, 0, inputDataB);
  
  // Create bind group
  const bindGroup = device.createBindGroup({
    layout: computePipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: { buffer: inputABuffer } },
      { binding: 1, resource: { buffer: inputBBuffer } },
      { binding: 2, resource: { buffer: outputBuffer } }
    ]
  });
  
  // Encode and submit commands
  const commandEncoder = device.createCommandEncoder();
  const passEncoder = commandEncoder.beginComputePass();
  
  passEncoder.setPipeline(computePipeline);
  passEncoder.setBindGroup(0, bindGroup);
  passEncoder.dispatchWorkgroups(Math.ceil(dataSize / 64));
  passEncoder.end();
  
  // Copy output to staging buffer
  commandEncoder.copyBufferToBuffer(
    outputBuffer, 0,
    stagingBuffer, 0,
    bufferSize
  );
  
  device.queue.submit([commandEncoder.finish()]);
  
  // Read results
  await stagingBuffer.mapAsync(GPUMapMode.READ);
  const resultData = new Float32Array(stagingBuffer.getMappedRange());
  console.log("Compute result (first 10):", resultData.slice(0, 10));
  
  stagingBuffer.unmap();
}
Note: WebGPU is next-gen GPU API for web, replacing WebGL. Provides compute shaders, better performance, modern GPU features. Uses WGSL shader language. Experimental - currently in Chrome, Edge. Check navigator.gpu before use.
Warning: WebGPU is experimental and not widely supported yet. API may change. Always provide WebGL fallback. Requires HTTPS. Shaders use WGSL, not GLSL. GPU device can be lost - handle device.lost promise.

19.2 Web Assembly (WASM) Integration APIs

WebAssembly API Description
WebAssembly.compile() Compile WASM bytes to module asynchronously.
WebAssembly.instantiate() Compile and instantiate WASM module in one step.
WebAssembly.Module Compiled WASM module (stateless, can be cached).
WebAssembly.Instance Instantiated module with memory and exported functions.
WebAssembly.Memory Resizable ArrayBuffer for WASM linear memory.
WebAssembly.Table Resizable typed array of references (functions, objects).
WebAssembly.Global Global variable accessible to WASM and JavaScript.

Example: Load and use WebAssembly module

// Fetch and instantiate WASM module
async function loadWasm() {
  try {
    // Fetch WASM file
    const response = await fetch("module.wasm");
    const bytes = await response.arrayBuffer();
    
    // Instantiate with imports
    const imports = {
      env: {
        // Import JavaScript functions to WASM
        consoleLog: (arg) => console.log("WASM says:", arg),
        randomNumber: () => Math.random(),
        // Import memory (optional)
        memory: new WebAssembly.Memory({ initial: 256, maximum: 512 })
      }
    };
    
    const { instance, module } = await WebAssembly.instantiate(bytes, imports);
    
    console.log("WASM module loaded");
    console.log("Exports:", Object.keys(instance.exports));
    
    return instance;
    
  } catch (error) {
    console.error("Failed to load WASM:", error);
    throw error;
  }
}

// Use WASM module
async function useWasm() {
  const wasm = await loadWasm();
  
  // Call exported functions
  if (wasm.exports.add) {
    const result = wasm.exports.add(10, 32);
    console.log("10 + 32 =", result);
  }
  
  if (wasm.exports.fibonacci) {
    const fib10 = wasm.exports.fibonacci(10);
    console.log("Fibonacci(10) =", fib10);
  }
  
  // Access memory
  if (wasm.exports.memory) {
    const memory = new Uint8Array(wasm.exports.memory.buffer);
    console.log("Memory size:", memory.length);
  }
}

// Streaming compilation (faster for large modules)
async function loadWasmStreaming() {
  try {
    const response = await fetch("module.wasm");
    const { instance } = await WebAssembly.instantiateStreaming(response);
    
    console.log("WASM loaded via streaming");
    return instance;
    
  } catch (error) {
    console.error("Streaming failed, falling back:", error);
    // Fallback to regular loading
    return loadWasm();
  }
}

Example: WASM with shared memory and threads

// Create shared memory for multi-threaded WASM
const sharedMemory = new WebAssembly.Memory({
  initial: 1,
  maximum: 10,
  shared: true  // Enable sharing between workers
});

// Main thread: Load WASM with shared memory
async function loadThreadedWasm() {
  const response = await fetch("threaded.wasm");
  
  const { instance } = await WebAssembly.instantiateStreaming(response, {
    env: {
      memory: sharedMemory
    },
    wasi_snapshot_preview1: {
      // WASI imports if needed
    }
  });
  
  return instance;
}

// Worker thread code (worker.js)
/*
self.onmessage = async (event) => {
  const { memory, wasmUrl } = event.data;
  
  const response = await fetch(wasmUrl);
  const { instance } = await WebAssembly.instantiateStreaming(response, {
    env: { memory }
  });
  
  // Run compute-intensive work
  const result = instance.exports.processData();
  
  self.postMessage({ result });
};
*/

// Use from main thread
async function runParallelWasm() {
  const wasmInstance = await loadThreadedWasm();
  
  // Spawn workers
  const workers = [];
  const numWorkers = navigator.hardwareConcurrency || 4;
  
  for (let i = 0; i < numWorkers; i++) {
    const worker = new Worker("worker.js");
    
    worker.postMessage({
      memory: sharedMemory,
      wasmUrl: "threaded.wasm"
    });
    
    workers.push(worker);
  }
  
  // Coordinate work via shared memory and Atomics
  const sharedArray = new Int32Array(sharedMemory.buffer);
  
  // Signal workers to start
  Atomics.store(sharedArray, 0, 1);
  Atomics.notify(sharedArray, 0);
  
  console.log("Workers started");
}

// WASM Binary Interface example
class WasmBinaryInterface {
  constructor(instance) {
    this.instance = instance;
    this.memory = new DataView(instance.exports.memory.buffer);
  }
  
  // Read string from WASM memory
  readString(ptr, length) {
    const bytes = new Uint8Array(this.instance.exports.memory.buffer, ptr, length);
    return new TextDecoder().decode(bytes);
  }
  
  // Write string to WASM memory
  writeString(str, ptr) {
    const bytes = new TextEncoder().encode(str);
    const memory = new Uint8Array(this.instance.exports.memory.buffer);
    memory.set(bytes, ptr);
    return bytes.length;
  }
  
  // Read array from memory
  readArray(ptr, length, type = Float32Array) {
    return new type(this.instance.exports.memory.buffer, ptr, length);
  }
}
Note: WebAssembly enables near-native performance for compute-intensive tasks. Use for: video/audio processing, games, simulations, cryptography. Import/export functions between JS and WASM. Use instantiateStreaming() for faster loading. Supports multithreading with SharedArrayBuffer.

19.3 Web Neural Network API (WebNN)

WebNN Concept Description
ML Context Hardware-accelerated context for neural network operations.
Graph Builder Builds ML computation graph with layers and operations.
Operand Tensor (multi-dimensional array) in the computation graph.
Operation Neural network operation: conv2d, matmul, relu, softmax, etc.
Graph Compiled computation graph ready for execution.
Backend Hardware backend: CPU, GPU, or dedicated ML accelerator.

Example: WebNN basic usage

// Check WebNN support
if (!("ml" in navigator)) {
  console.error("WebNN not supported");
} else {
  console.log("WebNN supported");
  runInference();
}

async function runInference() {
  // Create ML context
  const context = await navigator.ml.createContext();
  
  console.log("ML Context created");
  
  // Build a simple neural network graph
  const builder = new MLGraphBuilder(context);
  
  // Input tensor: [batch, height, width, channels]
  const input = builder.input("input", {
    type: "float32",
    dimensions: [1, 224, 224, 3]
  });
  
  // Convolutional layer
  const conv1Weights = builder.constant({
    type: "float32",
    dimensions: [64, 3, 3, 3]
  }, new Float32Array(64 * 3 * 3 * 3));
  
  const conv1 = builder.conv2d(input, conv1Weights, {
    padding: [1, 1, 1, 1],
    strides: [1, 1],
    activation: builder.relu()
  });
  
  // Max pooling
  const pool1 = builder.maxPool2d(conv1, {
    windowDimensions: [2, 2],
    strides: [2, 2]
  });
  
  // Fully connected layer
  const fc1Weights = builder.constant({
    type: "float32",
    dimensions: [1000, 112 * 112 * 64]
  }, new Float32Array(1000 * 112 * 112 * 64));
  
  const flatten = builder.reshape(pool1, [1, -1]);
  const fc1 = builder.matmul(flatten, fc1Weights);
  
  // Softmax output
  const output = builder.softmax(fc1);
  
  // Build graph
  const graph = await builder.build({ output });
  
  console.log("Graph built successfully");
  
  // Prepare input data
  const inputData = new Float32Array(1 * 224 * 224 * 3);
  // ... fill with actual image data
  
  // Execute graph
  const inputs = { input: inputData };
  const outputs = await context.compute(graph, inputs);
  
  console.log("Inference complete");
  console.log("Output shape:", outputs.output.shape);
  console.log("Top prediction:", getTopPrediction(outputs.output));
}

function getTopPrediction(tensor) {
  const data = tensor.data;
  let maxIndex = 0;
  let maxValue = data[0];
  
  for (let i = 1; i < data.length; i++) {
    if (data[i] > maxValue) {
      maxValue = data[i];
      maxIndex = i;
    }
  }
  
  return { class: maxIndex, confidence: maxValue };
}

Example: Load and run ONNX model with WebNN

// Using ONNX Runtime Web with WebNN backend
async function runONNXModel() {
  // Load ONNX Runtime
  const ort = window.ort;
  
  // Check WebNN availability
  if (ort.env.wasm.numThreads) {
    console.log("ONNX Runtime loaded");
  }
  
  try {
    // Create session with WebNN backend
    const session = await ort.InferenceSession.create("model.onnx", {
      executionProviders: ["webnn", "wasm"]
    });
    
    console.log("Model loaded");
    console.log("Input names:", session.inputNames);
    console.log("Output names:", session.outputNames);
    
    // Prepare input tensor
    const inputData = Float32Array.from({ length: 224 * 224 * 3 }, () => Math.random());
    const inputTensor = new ort.Tensor("float32", inputData, [1, 3, 224, 224]);
    
    // Run inference
    const feeds = { [session.inputNames[0]]: inputTensor };
    const results = await session.run(feeds);
    
    console.log("Inference complete");
    
    // Get output
    const output = results[session.outputNames[0]];
    console.log("Output shape:", output.dims);
    console.log("Output data (first 10):", output.data.slice(0, 10));
    
    return output;
    
  } catch (error) {
    console.error("ONNX inference failed:", error);
  }
}

// Image classification with preprocessing
async function classifyImage(imageElement) {
  // Preprocess image
  const canvas = document.createElement("canvas");
  canvas.width = 224;
  canvas.height = 224;
  const ctx = canvas.getContext("2d");
  
  ctx.drawImage(imageElement, 0, 0, 224, 224);
  const imageData = ctx.getImageData(0, 0, 224, 224);
  
  // Convert to tensor (normalize to [0, 1])
  const inputData = new Float32Array(3 * 224 * 224);
  for (let i = 0; i < imageData.data.length / 4; i++) {
    inputData[i] = imageData.data[i * 4] / 255.0;           // R
    inputData[224 * 224 + i] = imageData.data[i * 4 + 1] / 255.0;  // G
    inputData[224 * 224 * 2 + i] = imageData.data[i * 4 + 2] / 255.0; // B
  }
  
  // Run model
  const ort = window.ort;
  const session = await ort.InferenceSession.create("mobilenet.onnx", {
    executionProviders: ["webnn"]
  });
  
  const tensor = new ort.Tensor("float32", inputData, [1, 3, 224, 224]);
  const results = await session.run({ input: tensor });
  
  return results.output.data;
}
Note: WebNN provides hardware-accelerated machine learning inference. Uses GPU, NPU, or dedicated ML accelerators. Supports common operations: conv2d, matmul, relu, softmax. Can load ONNX models. Experimental - limited browser support.
Warning: WebNN is highly experimental. Currently only in some Chromium builds behind flags. API subject to change. For production, use TensorFlow.js or ONNX Runtime Web with WASM backend. Always check "ml" in navigator and provide fallback.

19.4 Web Transport API for Low-latency Communication

WebTransport Feature Description
WebTransport Connection Low-latency, bidirectional communication over HTTP/3 (QUIC protocol).
Datagrams Unreliable, unordered messages (like UDP) for real-time data.
Streams Reliable, ordered streams (like TCP) with multiplexing.
Unidirectional Streams One-way data flow from client or server.
Bidirectional Streams Two-way data flow, independent read/write.
Connection Migration Maintains connection across network changes (WiFi to cellular).

Example: WebTransport connection and streams

// Check WebTransport support
if (!("WebTransport" in window)) {
  console.error("WebTransport not supported");
} else {
  console.log("WebTransport supported");
  connectWebTransport();
}

async function connectWebTransport() {
  try {
    // Connect to server (requires HTTPS and HTTP/3)
    const url = "https://example.com:4433/webtransport";
    const transport = new WebTransport(url);
    
    // Wait for connection
    await transport.ready;
    console.log("WebTransport connected");
    
    // Handle incoming bidirectional streams
    handleIncomingStreams(transport);
    
    // Handle incoming unidirectional streams
    handleIncomingUnidirectionalStreams(transport);
    
    // Send data via bidirectional stream
    await sendViaBidirectionalStream(transport);
    
    // Send datagrams
    sendDatagrams(transport);
    
    // Handle connection close
    transport.closed.then(() => {
      console.log("Connection closed gracefully");
    }).catch((error) => {
      console.error("Connection closed with error:", error);
    });
    
  } catch (error) {
    console.error("WebTransport connection failed:", error);
  }
}

// Bidirectional streams (reliable, ordered)
async function sendViaBidirectionalStream(transport) {
  // Create outgoing bidirectional stream
  const stream = await transport.createBidirectionalStream();
  
  // Get writer and reader
  const writer = stream.writable.getWriter();
  const reader = stream.readable.getReader();
  
  // Send data
  const encoder = new TextEncoder();
  await writer.write(encoder.encode("Hello from client!"));
  await writer.close();
  
  console.log("Sent via bidirectional stream");
  
  // Read response
  const { value, done } = await reader.read();
  if (!done) {
    const decoder = new TextDecoder();
    console.log("Received:", decoder.decode(value));
  }
  
  reader.releaseLock();
}

// Handle incoming bidirectional streams
async function handleIncomingStreams(transport) {
  const reader = transport.incomingBidirectionalStreams.getReader();
  
  while (true) {
    const { value: stream, done } = await reader.read();
    if (done) break;
    
    console.log("Incoming bidirectional stream");
    
    // Handle stream in background
    handleStream(stream);
  }
}

async function handleStream(stream) {
  const reader = stream.readable.getReader();
  const writer = stream.writable.getWriter();
  
  try {
    const { value, done } = await reader.read();
    if (!done) {
      const decoder = new TextDecoder();
      const message = decoder.decode(value);
      console.log("Stream received:", message);
      
      // Send response
      const encoder = new TextEncoder();
      await writer.write(encoder.encode(`Echo: ${message}`));
    }
  } finally {
    reader.releaseLock();
    await writer.close();
  }
}

// Unidirectional streams
async function handleIncomingUnidirectionalStreams(transport) {
  const reader = transport.incomingUnidirectionalStreams.getReader();
  
  while (true) {
    const { value: stream, done } = await reader.read();
    if (done) break;
    
    console.log("Incoming unidirectional stream");
    
    const streamReader = stream.getReader();
    const { value, done: streamDone } = await streamReader.read();
    
    if (!streamDone) {
      const decoder = new TextDecoder();
      console.log("Unidirectional data:", decoder.decode(value));
    }
  }
}

Example: WebTransport datagrams for real-time data

// Datagrams (unreliable, unordered - best for real-time)
async function sendDatagrams(transport) {
  const writer = transport.datagrams.writable.getWriter();
  const encoder = new TextEncoder();
  
  // Send game state updates
  setInterval(async () => {
    const gameState = {
      position: { x: Math.random(), y: Math.random() },
      timestamp: Date.now()
    };
    
    const data = encoder.encode(JSON.stringify(gameState));
    
    try {
      await writer.write(data);
    } catch (error) {
      console.error("Failed to send datagram:", error);
    }
  }, 16); // ~60 FPS
}

// Receive datagrams
async function receiveDatagrams(transport) {
  const reader = transport.datagrams.readable.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    try {
      const { value, done } = await reader.read();
      if (done) break;
      
      const message = decoder.decode(value);
      const data = JSON.parse(message);
      
      // Update game state
      updateGameState(data);
      
    } catch (error) {
      console.error("Datagram read error:", error);
    }
  }
}

// Real-time multiplayer game example
class WebTransportGameClient {
  constructor(serverUrl) {
    this.serverUrl = serverUrl;
    this.transport = null;
    this.datagramWriter = null;
  }
  
  async connect() {
    this.transport = new WebTransport(this.serverUrl);
    await this.transport.ready;
    
    console.log("Game client connected");
    
    this.datagramWriter = this.transport.datagrams.writable.getWriter();
    
    // Start receiving updates
    this.receiveUpdates();
  }
  
  async sendPlayerAction(action) {
    if (!this.datagramWriter) return;
    
    const data = new TextEncoder().encode(JSON.stringify({
      type: "action",
      action: action,
      timestamp: performance.now()
    }));
    
    try {
      await this.datagramWriter.write(data);
    } catch (error) {
      console.error("Failed to send action:", error);
    }
  }
  
  async receiveUpdates() {
    const reader = this.transport.datagrams.readable.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      try {
        const { value, done } = await reader.read();
        if (done) break;
        
        const update = JSON.parse(decoder.decode(value));
        this.handleGameUpdate(update);
        
      } catch (error) {
        console.error("Failed to receive update:", error);
        break;
      }
    }
  }
  
  handleGameUpdate(update) {
    // Process game state update
    console.log("Game update:", update);
  }
  
  async disconnect() {
    if (this.transport) {
      await this.transport.close();
    }
  }
}
Note: WebTransport offers lower latency than WebSocket using HTTP/3 (QUIC). Provides both reliable streams and unreliable datagrams. Ideal for: gaming, video conferencing, live streaming. Supports connection migration (network switching). Experimental - Chrome, Edge support.
Warning: WebTransport requires HTTP/3 server support and HTTPS. Experimental - limited browser support. Not available on iOS Safari. Server setup more complex than WebSocket. Consider WebSocket for broader compatibility.

19.5 WebCodecs API for Audio/Video Encoding

WebCodecs Component Description
VideoEncoder Encode video frames to compressed format (H.264, VP9, AV1).
VideoDecoder Decode compressed video to raw frames.
AudioEncoder Encode audio data to compressed format (Opus, AAC).
AudioDecoder Decode compressed audio to PCM samples.
VideoFrame Represents a single video frame with pixel data.
EncodedVideoChunk Encoded video data (can be keyframe or delta frame).
AudioData Raw audio samples in various formats.

Example: Encode video frames

// Check WebCodecs support
if (!("VideoEncoder" in window)) {
  console.error("WebCodecs not supported");
} else {
  console.log("WebCodecs supported");
}

// Initialize video encoder
const encodedChunks = [];

const encoder = new VideoEncoder({
  output: (chunk, metadata) => {
    // Receive encoded chunk
    console.log("Encoded chunk:", {
      type: chunk.type,  // "key" or "delta"
      timestamp: chunk.timestamp,
      byteLength: chunk.byteLength
    });
    
    encodedChunks.push(chunk);
    
    // If key frame, save decoder config
    if (metadata?.decoderConfig) {
      console.log("Decoder config:", metadata.decoderConfig);
    }
  },
  error: (error) => {
    console.error("Encoder error:", error);
  }
});

// Configure encoder
encoder.configure({
  codec: "vp09.00.10.08", // VP9
  width: 1920,
  height: 1080,
  bitrate: 2_000_000,  // 2 Mbps
  framerate: 30,
  latencyMode: "realtime" // or "quality"
});

// Encode frames from canvas
async function encodeCanvasFrames(canvas) {
  const fps = 30;
  const frameDuration = 1_000_000 / fps; // microseconds
  let frameCount = 0;
  
  const captureFrame = () => {
    // Create VideoFrame from canvas
    const frame = new VideoFrame(canvas, {
      timestamp: frameCount * frameDuration
    });
    
    // Encode frame
    encoder.encode(frame, { keyFrame: frameCount % 30 === 0 });
    
    // Close frame to free resources
    frame.close();
    
    frameCount++;
    
    if (frameCount < 300) { // Encode 10 seconds
      requestAnimationFrame(captureFrame);
    } else {
      // Finish encoding
      encoder.flush().then(() => {
        console.log("Encoding complete");
        encoder.close();
      });
    }
  };
  
  captureFrame();
}

// Encode from MediaStream (camera/screen)
async function encodeMediaStream() {
  const stream = await navigator.mediaDevices.getUserMedia({ 
    video: { width: 1280, height: 720 } 
  });
  
  const track = stream.getVideoTracks()[0];
  const processor = new MediaStreamTrackProcessor({ track });
  const reader = processor.readable.getReader();
  
  while (true) {
    const { value: frame, done } = await reader.read();
    if (done) break;
    
    // Encode frame
    encoder.encode(frame);
    frame.close();
  }
}

Example: Decode and display video

// Video decoder
const decoder = new VideoDecoder({
  output: (frame) => {
    // Display decoded frame
    console.log("Decoded frame:", {
      format: frame.format,
      codedWidth: frame.codedWidth,
      codedHeight: frame.codedHeight,
      timestamp: frame.timestamp
    });
    
    // Draw to canvas
    displayFrame(frame);
    
    // Close frame
    frame.close();
  },
  error: (error) => {
    console.error("Decoder error:", error);
  }
});

// Configure decoder
decoder.configure({
  codec: "vp09.00.10.08",
  codedWidth: 1920,
  codedHeight: 1080
});

// Decode encoded chunks
function decodeVideo(encodedChunks) {
  encodedChunks.forEach(chunk => {
    decoder.decode(chunk);
  });
  
  decoder.flush().then(() => {
    console.log("Decoding complete");
  });
}

// Display frame on canvas
function displayFrame(frame) {
  const canvas = document.getElementById("output-canvas");
  canvas.width = frame.displayWidth;
  canvas.height = frame.displayHeight;
  
  const ctx = canvas.getContext("2d");
  ctx.drawImage(frame, 0, 0);
}

// Audio encoding example
const audioEncoder = new AudioEncoder({
  output: (chunk, metadata) => {
    console.log("Encoded audio chunk:", chunk.byteLength);
  },
  error: (error) => {
    console.error("Audio encoder error:", error);
  }
});

audioEncoder.configure({
  codec: "opus",
  sampleRate: 48000,
  numberOfChannels: 2,
  bitrate: 128000
});

// Encode audio from microphone
async function encodeAudio() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const track = stream.getAudioTracks()[0];
  
  const processor = new MediaStreamTrackProcessor({ track });
  const reader = processor.readable.getReader();
  
  while (true) {
    const { value: audioData, done } = await reader.read();
    if (done) break;
    
    audioEncoder.encode(audioData);
    audioData.close();
  }
}
Note: WebCodecs provides low-level access to media codecs. Encode/decode video and audio programmatically. Useful for: video editing, conferencing, streaming, transcoding. More control than MediaRecorder API. Supports H.264, VP8, VP9, AV1 for video; Opus, AAC for audio.
Warning: WebCodecs is low-level - you handle frame-by-frame encoding/decoding. Must manage memory by calling frame.close(). Not all codecs supported in all browsers. Check codec support with VideoEncoder.isConfigSupported(). Chrome, Edge support - not in Firefox/Safari yet.

19.6 Origin Trial APIs and Feature Flags

Concept Description
Origin Trial Test experimental features in production with token from Chrome.
Feature Flag Enable experimental features locally via browser flags.
Origin Trial Token Signed token that enables feature for specific origin and duration.
chrome://flags Browser page to enable experimental features for testing.
Meta Tag Registration Activate origin trial via <meta> tag in HTML.
HTTP Header Registration Activate origin trial via Origin-Trial HTTP response header.

Example: Register for Origin Trial

<!-- Method 1: Meta tag -->
<meta http-equiv="origin-trial" 
      content="YOUR_ORIGIN_TRIAL_TOKEN_HERE">

<!-- Example with actual token format -->
<meta http-equiv="origin-trial"
      content="A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8S9t0U1v2W3x4Y5z6A7b8C9d0E1f2G3h4I5j6K7l8M9n0O1p2Q3r4S5t6U7v8W9x0Y1z2A3b4C5d6E7f8G9h0I1j2K3l4M5n6O7p8Q9r0S1t2U3v4W5x6Y7z8A9b0C1d2E3f4==">

<script>
  // Check if experimental feature is available
  if ("experimentalFeature" in window) {
    console.log("Experimental feature enabled via origin trial");
    useExperimentalFeature();
  } else {
    console.log("Experimental feature not available");
    useFallback();
  }
</script>

Example: HTTP header registration and feature detection

# HTTP Response Header
Origin-Trial: YOUR_ORIGIN_TRIAL_TOKEN_HERE

# Multiple tokens (comma-separated)
Origin-Trial: TOKEN_1, TOKEN_2

Example: Programmatic feature detection and flags

// Check for experimental features
const experimentalFeatures = {
  // Web GPU
  webGPU: "gpu" in navigator,
  
  // WebNN
  webNN: "ml" in navigator,
  
  // Web Transport
  webTransport: "WebTransport" in window,
  
  // WebCodecs
  webCodecs: "VideoEncoder" in window,
  
  // File System Access
  fileSystemAccess: "showOpenFilePicker" in window,
  
  // Web Bluetooth
  webBluetooth: "bluetooth" in navigator,
  
  // Web USB
  webUSB: "usb" in navigator,
  
  // Web Serial
  webSerial: "serial" in navigator,
  
  // Web HID
  webHID: "hid" in navigator,
  
  // Compute Pressure
  computePressure: "ComputePressureObserver" in window,
  
  // Eyedropper API
  eyeDropper: "EyeDropper" in window,
  
  // Window Controls Overlay
  windowControlsOverlay: "windowControlsOverlay" in navigator
};

console.log("Experimental features:", experimentalFeatures);

// Progressive enhancement with feature detection
function useExperimentalFeatures() {
  // WebGPU if available, else WebGL
  if (experimentalFeatures.webGPU) {
    initWebGPU();
  } else {
    initWebGL();
  }
  
  // Web Transport if available, else WebSocket
  if (experimentalFeatures.webTransport) {
    connectWebTransport();
  } else {
    connectWebSocket();
  }
}

// Origin Trial status check
async function checkOriginTrialStatus() {
  // Some APIs expose trial status
  if (document.featurePolicy) {
    const features = document.featurePolicy.features();
    console.log("Available features:", features);
  }
  
  // Check specific trial features
  try {
    // Attempt to use feature
    const feature = await tryExperimentalFeature();
    console.log("Origin trial active for feature");
    return true;
  } catch (error) {
    console.log("Origin trial not active:", error.message);
    return false;
  }
}

// Get Origin Trial token programmatically
function getOriginTrialToken() {
  // For your actual implementation, get token from:
  // https://developer.chrome.com/origintrials/
  
  // Sign up for trial
  // 1. Go to Chrome Origin Trials
  // 2. Select feature to trial
  // 3. Enter your origin (https://example.com)
  // 4. Receive token
  // 5. Add to meta tag or HTTP header
  
  return "YOUR_TOKEN_HERE";
}

// Enable feature flags for local development
console.log(`
To enable experimental features locally:
1. Open chrome://flags
2. Search for feature name
3. Enable and restart browser

Common flags:
- #enable-experimental-web-platform-features
- #enable-webgpu-developer-features
- #enable-experimental-webassembly-features
`);
Note: Origin Trials let you test experimental features in production. Register at Chrome Origin Trials site, get token for your domain. Token enables feature for all users for trial duration (usually 6 months). Use for gathering feedback before feature ships.
Warning: Origin Trial features may change or be removed. Don't rely on them for critical functionality. Always provide fallback. Trial tokens expire - monitor trial status. Features may become available without trial when they ship. Test thoroughly - experimental APIs can have bugs.

Experimental and Emerging APIs Best Practices

  • Always check feature availability before use: if ("feature" in window)
  • Provide fallbacks for unsupported browsers - progressive enhancement
  • WebGPU: Use for compute-heavy graphics, provide WebGL fallback
  • WebAssembly: Great for performance-critical code, use instantiateStreaming()
  • WebNN: Hardware ML acceleration, fallback to TensorFlow.js WASM backend
  • WebTransport: Lower latency than WebSocket, fallback to WebSocket
  • WebCodecs: Low-level media control, requires manual memory management
  • Register for Origin Trials to test features in production safely
  • Monitor experimental API specifications - they can change
  • Use feature flags locally (chrome://flags) for development
  • Document which experimental features your app uses
  • Have migration plan when experimental features become stable
  • Test on multiple browsers - experimental features may not be cross-browser

20. API Integration and Development Patterns

20.1 API Polyfills and Feature Detection Patterns

Pattern Description
Feature Detection First Always check if API exists before using: if ("fetch" in window).
Conditional Polyfill Loading Load polyfills only when needed to reduce bundle size.
Progressive Enhancement Build base functionality, enhance for capable browsers.
Polyfill Service Use CDN service (polyfill.io) for automatic polyfill delivery.
Core-js Integration Use core-js for comprehensive ES6+ and Web API polyfills.
Babel Transformation Transform modern syntax to compatible code at build time.

Example: Comprehensive feature detection

// Feature detection utility
const FeatureDetector = {
  // Check API support
  supports(feature) {
    const features = {
      // Network APIs
      fetch: "fetch" in window,
      websocket: "WebSocket" in window,
      
      // Storage APIs
      localStorage: (() => {
        try {
          const test = "__test__";
          localStorage.setItem(test, test);
          localStorage.removeItem(test);
          return true;
        } catch (e) {
          return false;
        }
      })(),
      indexedDB: "indexedDB" in window,
      
      // Modern JavaScript
      promise: "Promise" in window,
      async: (() => {
        try {
          eval("(async () => {})");
          return true;
        } catch (e) {
          return false;
        }
      })(),
      modules: "noModule" in document.createElement("script"),
      
      // DOM APIs
      intersectionObserver: "IntersectionObserver" in window,
      resizeObserver: "ResizeObserver" in window,
      mutationObserver: "MutationObserver" in window,
      
      // Service Worker
      serviceWorker: "serviceWorker" in navigator,
      
      // Media APIs
      mediaRecorder: "MediaRecorder" in window,
      webRTC: "RTCPeerConnection" in window,
      
      // Graphics
      canvas: (() => {
        const canvas = document.createElement("canvas");
        return !!(canvas.getContext && canvas.getContext("2d"));
      })(),
      webgl: (() => {
        const canvas = document.createElement("canvas");
        return !!(
          canvas.getContext("webgl") ||
          canvas.getContext("experimental-webgl")
        );
      })(),
      
      // Advanced features
      webAssembly: typeof WebAssembly === "object",
      webWorker: typeof Worker !== "undefined",
      sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined"
    };
    
    return features[feature] !== undefined ? features[feature] : false;
  },
  
  // Get all unsupported features
  getUnsupportedFeatures(requiredFeatures) {
    return requiredFeatures.filter(feature => !this.supports(feature));
  },
  
  // Check browser capabilities
  getBrowserCapabilities() {
    return {
      // Hardware
      cores: navigator.hardwareConcurrency || 1,
      memory: navigator.deviceMemory || "unknown",
      
      // Network
      online: navigator.onLine,
      connection: navigator.connection?.effectiveType || "unknown",
      
      // Screen
      pixelRatio: window.devicePixelRatio || 1,
      touch: "ontouchstart" in window || navigator.maxTouchPoints > 0
    };
  }
};

// Usage
const requiredFeatures = ["fetch", "promise", "localStorage"];
const unsupported = FeatureDetector.getUnsupportedFeatures(requiredFeatures);

if (unsupported.length > 0) {
  console.warn("Missing features:", unsupported);
  loadPolyfills(unsupported);
} else {
  console.log("All features supported");
  initApp();
}

Example: Conditional polyfill loading

// Dynamic polyfill loader
async function loadPolyfills() {
  const polyfillsNeeded = [];
  
  // Check and queue polyfills
  if (!window.fetch) {
    polyfillsNeeded.push(
      import("whatwg-fetch")
    );
  }
  
  if (!window.Promise) {
    polyfillsNeeded.push(
      import("promise-polyfill")
    );
  }
  
  if (!window.IntersectionObserver) {
    polyfillsNeeded.push(
      import("intersection-observer")
    );
  }
  
  if (!window.ResizeObserver) {
    polyfillsNeeded.push(
      import("resize-observer-polyfill")
    );
  }
  
  if (!Element.prototype.closest) {
    Element.prototype.closest = function(selector) {
      let el = this;
      while (el) {
        if (el.matches(selector)) return el;
        el = el.parentElement;
      }
      return null;
    };
  }
  
  if (!Array.prototype.includes) {
    Array.prototype.includes = function(searchElement, fromIndex) {
      return this.indexOf(searchElement, fromIndex) !== -1;
    };
  }
  
  // Load all polyfills in parallel
  if (polyfillsNeeded.length > 0) {
    console.log(`Loading ${polyfillsNeeded.length} polyfills...`);
    await Promise.all(polyfillsNeeded);
    console.log("Polyfills loaded");
  }
}

// Initialize app after polyfills
loadPolyfills().then(() => {
  console.log("Starting application");
  initApp();
});

// Alternative: Use Polyfill.io CDN
/*
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,IntersectionObserver"></script>
*/

// Alternative: Conditional script loading
function loadPolyfillScript(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

async function loadLegacyPolyfills() {
  if (!window.Promise) {
    await loadPolyfillScript("https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js");
  }
  
  if (!window.fetch) {
    await loadPolyfillScript("https://cdn.jsdelivr.net/npm/whatwg-fetch@3/dist/fetch.umd.js");
  }
}
Note: Always use feature detection, never browser sniffing. Load polyfills conditionally to reduce bundle size. Use dynamic imports for code splitting. Consider using Polyfill.io CDN for automatic polyfill delivery based on user agent.

20.2 Error Handling and Graceful Degradation

Strategy Description
Try-Catch Blocks Wrap API calls in try-catch for synchronous and async/await code.
Promise Error Handling Use .catch() or try-catch with async/await for promise rejections.
Global Error Handler Use window.onerror and window.onunhandledrejection for uncaught errors.
Fallback Strategies Provide alternative implementations when APIs unavailable.
User Feedback Show meaningful error messages to users, log details for developers.
Retry Logic Implement exponential backoff for transient failures.

Example: Comprehensive error handling

// Error handling utility
class ErrorHandler {
  constructor() {
    this.setupGlobalHandlers();
  }
  
  setupGlobalHandlers() {
    // Handle uncaught errors
    window.addEventListener("error", (event) => {
      console.error("Uncaught error:", {
        message: event.message,
        filename: event.filename,
        line: event.lineno,
        column: event.colno,
        error: event.error
      });
      
      this.logError(event.error);
      this.showUserError("An unexpected error occurred");
      
      // Prevent default error handling
      // event.preventDefault();
    });
    
    // Handle unhandled promise rejections
    window.addEventListener("unhandledrejection", (event) => {
      console.error("Unhandled promise rejection:", event.reason);
      
      this.logError(event.reason);
      this.showUserError("An operation failed");
      
      // Prevent default
      // event.preventDefault();
    });
  }
  
  // Wrap API calls with error handling
  async safeApiCall(apiFunction, fallback = null) {
    try {
      return await apiFunction();
    } catch (error) {
      console.error("API call failed:", error);
      this.logError(error);
      
      if (fallback) {
        console.log("Using fallback");
        return fallback();
      }
      
      throw error;
    }
  }
  
  // Retry with exponential backoff
  async retry(fn, options = {}) {
    const {
      maxAttempts = 3,
      initialDelay = 1000,
      maxDelay = 10000,
      backoffMultiplier = 2
    } = options;
    
    let lastError;
    let delay = initialDelay;
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error;
        
        if (attempt === maxAttempts) {
          console.error(`Failed after ${maxAttempts} attempts:`, error);
          break;
        }
        
        console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
        await this.sleep(delay);
        
        // Exponential backoff
        delay = Math.min(delay * backoffMultiplier, maxDelay);
      }
    }
    
    throw lastError;
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  logError(error) {
    // Send to error tracking service
    if (window.errorTracker) {
      window.errorTracker.log(error);
    }
    
    // Log to analytics
    if (window.analytics) {
      window.analytics.track("error", {
        message: error.message,
        stack: error.stack,
        timestamp: Date.now()
      });
    }
  }
  
  showUserError(message) {
    // Show user-friendly error message
    const errorDiv = document.getElementById("error-message");
    if (errorDiv) {
      errorDiv.textContent = message;
      errorDiv.style.display = "block";
      
      setTimeout(() => {
        errorDiv.style.display = "none";
      }, 5000);
    }
  }
}

// Usage
const errorHandler = new ErrorHandler();

// Safe API call with fallback
async function loadData() {
  return errorHandler.safeApiCall(
    async () => {
      const response = await fetch("/api/data");
      if (!response.ok) throw new Error("Network error");
      return response.json();
    },
    () => {
      // Fallback: use cached data
      return getCachedData();
    }
  );
}

// Retry failed operations
async function uploadFile(file) {
  return errorHandler.retry(
    async () => {
      const formData = new FormData();
      formData.append("file", file);
      
      const response = await fetch("/upload", {
        method: "POST",
        body: formData
      });
      
      if (!response.ok) throw new Error("Upload failed");
      return response.json();
    },
    { maxAttempts: 3, initialDelay: 1000 }
  );
}

Example: API-specific graceful degradation

// Graceful degradation for various APIs

// 1. Fetch with XMLHttpRequest fallback
async function httpRequest(url, options = {}) {
  if (window.fetch) {
    try {
      const response = await fetch(url, options);
      return await response.json();
    } catch (error) {
      console.error("Fetch failed:", error);
      throw error;
    }
  } else {
    // Fallback to XMLHttpRequest
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open(options.method || "GET", url);
      
      xhr.onload = () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error(`HTTP ${xhr.status}`));
        }
      };
      
      xhr.onerror = () => reject(new Error("Network error"));
      xhr.send(options.body);
    });
  }
}

// 2. IntersectionObserver with scroll fallback
function observeVisibility(element, callback) {
  if ("IntersectionObserver" in window) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        callback(entry.isIntersecting);
      });
    });
    
    observer.observe(element);
    return () => observer.disconnect();
  } else {
    // Fallback: use scroll events
    const checkVisibility = () => {
      const rect = element.getBoundingClientRect();
      const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
      callback(isVisible);
    };
    
    window.addEventListener("scroll", checkVisibility);
    checkVisibility(); // Check initially
    
    return () => window.removeEventListener("scroll", checkVisibility);
  }
}

// 3. localStorage with cookie fallback
const storage = {
  set(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      // Fallback to cookies
      document.cookie = `${key}=${encodeURIComponent(JSON.stringify(value))}`;
    }
  },
  
  get(key) {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : null;
    } catch (error) {
      // Fallback to cookies
      const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
      return match ? JSON.parse(decodeURIComponent(match[1])) : null;
    }
  }
};

// 4. WebSocket with long-polling fallback
class RealTimeConnection {
  constructor(url) {
    this.url = url;
    this.useWebSocket = "WebSocket" in window;
  }
  
  connect(onMessage) {
    if (this.useWebSocket) {
      this.ws = new WebSocket(this.url);
      
      this.ws.onmessage = (event) => {
        onMessage(JSON.parse(event.data));
      };
      
      this.ws.onerror = () => {
        console.warn("WebSocket failed, falling back to polling");
        this.startPolling(onMessage);
      };
    } else {
      this.startPolling(onMessage);
    }
  }
  
  startPolling(onMessage) {
    const poll = async () => {
      try {
        const response = await fetch(`${this.url}/poll`);
        const data = await response.json();
        onMessage(data);
      } catch (error) {
        console.error("Polling failed:", error);
      }
      
      setTimeout(poll, 2000); // Poll every 2 seconds
    };
    
    poll();
  }
}
Note: Always handle errors at multiple levels: API level, function level, and global level. Provide fallbacks for missing APIs. Use retry logic for transient failures. Show user-friendly messages while logging detailed errors for debugging.

20.3 Performance Optimization for API Usage

Technique Description
Debouncing Delay API calls until user stops action (e.g., typing).
Throttling Limit API call frequency (e.g., max once per 100ms).
Request Caching Cache API responses to avoid redundant requests.
Request Deduplication Prevent duplicate simultaneous requests for same resource.
Lazy Loading Load resources only when needed, not upfront.
Request Batching Combine multiple API calls into single request.
Web Workers Offload heavy processing to background thread.

Example: Debouncing and throttling

// Debounce - wait for inactivity
function debounce(func, delay) {
  let timeoutId;
  
  return function(...args) {
    clearTimeout(timeoutId);
    
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// Throttle - limit frequency
function throttle(func, limit) {
  let inThrottle;
  
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// Usage: Debounced search
const searchAPI = debounce(async (query) => {
  console.log("Searching for:", query);
  const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
  displayResults(results);
}, 300);

document.getElementById("search-input").addEventListener("input", (e) => {
  searchAPI(e.target.value);
});

// Usage: Throttled scroll handler
const handleScroll = throttle(() => {
  console.log("Scroll position:", window.scrollY);
  updateScrollIndicator();
}, 100);

window.addEventListener("scroll", handleScroll);

Example: Request caching and deduplication

// Request cache with TTL
class RequestCache {
  constructor(ttl = 5 * 60 * 1000) { // 5 minutes default
    this.cache = new Map();
    this.pendingRequests = new Map();
    this.ttl = ttl;
  }
  
  async get(url, fetcher) {
    // Check cache
    const cached = this.cache.get(url);
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      console.log("Cache hit:", url);
      return cached.data;
    }
    
    // Deduplicate simultaneous requests
    if (this.pendingRequests.has(url)) {
      console.log("Request already pending:", url);
      return this.pendingRequests.get(url);
    }
    
    // Make new request
    console.log("Cache miss, fetching:", url);
    const promise = fetcher().then(data => {
      this.cache.set(url, {
        data,
        timestamp: Date.now()
      });
      this.pendingRequests.delete(url);
      return data;
    }).catch(error => {
      this.pendingRequests.delete(url);
      throw error;
    });
    
    this.pendingRequests.set(url, promise);
    return promise;
  }
  
  clear() {
    this.cache.clear();
    this.pendingRequests.clear();
  }
  
  invalidate(url) {
    this.cache.delete(url);
  }
}

// Usage
const cache = new RequestCache();

async function getUserData(userId) {
  return cache.get(`/api/users/${userId}`, async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
}

// Multiple calls will only make one request
Promise.all([
  getUserData(1),
  getUserData(1),
  getUserData(1)
]).then(([user1, user2, user3]) => {
  console.log("All resolved to same user:", user1 === user2);
});

Example: Request batching and Web Workers

// Request batching
class RequestBatcher {
  constructor(batchInterval = 50) {
    this.queue = [];
    this.timer = null;
    this.batchInterval = batchInterval;
  }
  
  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({ request, resolve, reject });
      
      if (!this.timer) {
        this.timer = setTimeout(() => {
          this.flush();
        }, this.batchInterval);
      }
    });
  }
  
  async flush() {
    if (this.queue.length === 0) return;
    
    const batch = this.queue.splice(0);
    this.timer = null;
    
    try {
      // Send batched request
      const requests = batch.map(b => b.request);
      const response = await fetch("/api/batch", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ requests })
      });
      
      const results = await response.json();
      
      // Resolve individual promises
      batch.forEach((item, index) => {
        item.resolve(results[index]);
      });
    } catch (error) {
      // Reject all promises
      batch.forEach(item => {
        item.reject(error);
      });
    }
  }
}

// Usage
const batcher = new RequestBatcher();

async function fetchUser(id) {
  return batcher.add({ type: "user", id });
}

// These will be batched into single request
Promise.all([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
]).then(users => {
  console.log("Users:", users);
});

// Web Worker for heavy processing
const workerCode = `
  self.onmessage = function(event) {
    const { data, operation } = event.data;
    
    let result;
    
    switch (operation) {
      case "processImage":
        result = processImage(data);
        break;
      case "parseData":
        result = parseData(data);
        break;
    }
    
    self.postMessage({ result });
  };
  
  function processImage(imageData) {
    // Heavy image processing
    return imageData;
  }
  
  function parseData(data) {
    // Heavy data parsing
    return data;
  }
`;

// Create worker
const blob = new Blob([workerCode], { type: "application/javascript" });
const workerUrl = URL.createObjectURL(blob);
const worker = new Worker(workerUrl);

// Use worker
worker.postMessage({
  operation: "processImage",
  data: imageData
});

worker.onmessage = (event) => {
  console.log("Worker result:", event.data.result);
};
Note: Performance optimization is critical for good UX. Debounce user input, throttle scroll/resize handlers. Cache API responses with appropriate TTL. Deduplicate simultaneous requests. Use Web Workers for CPU-intensive tasks to keep main thread responsive.

20.4 Cross-Browser Compatibility Strategies

Strategy Description
Feature Detection Check API availability before use, provide fallbacks.
Vendor Prefixes Try standard API first, then vendor-prefixed versions.
Autoprefixer Use build tools to add vendor prefixes automatically.
Browserslist Define target browsers for transpilation and polyfills.
Babel Transpile modern JavaScript to compatible versions.
Can I Use Check browser support data before using APIs.

Example: Cross-browser API access

// Vendor prefix handling
function getVendorPrefixed(object, property) {
  const prefixes = ["webkit", "moz", "ms", "o"];
  
  // Try standard property first
  if (property in object) {
    return object[property];
  }
  
  // Try capitalized standard property
  const capitalized = property.charAt(0).toUpperCase() + property.slice(1);
  if (capitalized in object) {
    return object[capitalized];
  }
  
  // Try vendor prefixes
  for (const prefix of prefixes) {
    const prefixedProperty = prefix + capitalized;
    if (prefixedProperty in object) {
      return object[prefixedProperty];
    }
  }
  
  return null;
}

// Usage examples

// 1. RequestAnimationFrame
const requestAnimationFrame =
  window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function(callback) {
    return setTimeout(callback, 1000 / 60);
  };

// 2. AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = AudioContext ? new AudioContext() : null;

// 3. IndexedDB
const indexedDB =
  window.indexedDB ||
  window.mozIndexedDB ||
  window.webkitIndexedDB ||
  window.msIndexedDB;

// 4. FullScreen API
const fullscreenAPI = {
  requestFullscreen: getVendorPrefixed(document.documentElement, "requestFullscreen"),
  exitFullscreen: getVendorPrefixed(document, "exitFullscreen"),
  fullscreenElement: () =>
    document.fullscreenElement ||
    document.webkitFullscreenElement ||
    document.mozFullScreenElement ||
    document.msFullscreenElement
};

async function enterFullscreen(element) {
  const method =
    element.requestFullscreen ||
    element.webkitRequestFullscreen ||
    element.mozRequestFullScreen ||
    element.msRequestFullscreen;
  
  if (method) {
    await method.call(element);
  }
}

// 5. Clipboard API
const clipboard = {
  async writeText(text) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      await navigator.clipboard.writeText(text);
    } else {
      // Fallback using execCommand
      const textarea = document.createElement("textarea");
      textarea.value = text;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.select();
      document.execCommand("copy");
      document.body.removeChild(textarea);
    }
  }
};

Example: Browser-specific workarounds

// Browser detection utilities (use sparingly!)
const BrowserDetect = {
  isChrome() {
    return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
  },
  
  isFirefox() {
    return /Firefox/.test(navigator.userAgent);
  },
  
  isSafari() {
    return /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor);
  },
  
  isEdge() {
    return /Edg/.test(navigator.userAgent);
  },
  
  isIOS() {
    return /iPad|iPhone|iPod/.test(navigator.userAgent);
  },
  
  isMobile() {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  }
};

// Safari-specific workaround for Date parsing
function parseDate(dateString) {
  // Safari doesn't support YYYY-MM-DD HH:mm:ss format
  if (BrowserDetect.isSafari()) {
    // Convert to ISO format
    dateString = dateString.replace(" ", "T");
  }
  return new Date(dateString);
}

// iOS Safari workaround for video autoplay
function setupVideoAutoplay(video) {
  if (BrowserDetect.isIOS()) {
    // iOS requires user interaction
    video.muted = true;
    video.playsInline = true;
    
    // Try autoplay on user interaction
    document.addEventListener("touchstart", () => {
      video.play().catch(e => console.log("Autoplay prevented:", e));
    }, { once: true });
  } else {
    video.autoplay = true;
  }
}

// Cross-browser event normalization
function normalizeEvent(event) {
  return {
    target: event.target || event.srcElement,
    which: event.which || event.keyCode,
    preventDefault: () => {
      if (event.preventDefault) {
        event.preventDefault();
      } else {
        event.returnValue = false;
      }
    },
    stopPropagation: () => {
      if (event.stopPropagation) {
        event.stopPropagation();
      } else {
        event.cancelBubble = true;
      }
    }
  };
}
Note: Prefer feature detection over browser detection. Use vendor prefixes for CSS and JavaScript APIs when needed. Leverage build tools (Babel, Autoprefixer) for automatic compatibility. Define target browsers with Browserslist. Test on actual devices, not just browser DevTools.

20.5 API Mocking and Testing Patterns

Pattern Description
Mock Service Worker Intercept network requests at Service Worker level for testing.
Jest Mocking Mock APIs using Jest's mocking functions.
Stub Objects Create fake API implementations for testing.
Dependency Injection Pass API dependencies to make testing easier.
Test Doubles Use spies, stubs, mocks, and fakes appropriately.
Fixture Data Maintain realistic test data for API responses.

Example: API mocking strategies

// 1. Simple fetch mock
const mockFetch = (response) => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve(response)
    })
  );
};

// Usage in tests
test("fetches user data", async () => {
  mockFetch({ id: 1, name: "John" });
  
  const data = await fetchUser(1);
  
  expect(data.name).toBe("John");
  expect(fetch).toHaveBeenCalledWith("/api/users/1");
});

// 2. Mock Service Worker (MSW)
/*
import { rest } from "msw";
import { setupServer } from "msw/node";

const server = setupServer(
  rest.get("/api/users/:id", (req, res, ctx) => {
    const { id } = req.params;
    return res(
      ctx.json({ id, name: "John Doe" })
    );
  }),
  
  rest.post("/api/users", (req, res, ctx) => {
    return res(
      ctx.status(201),
      ctx.json({ id: 123, ...req.body })
    );
  })
);

// Enable mocking before all tests
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
*/

// 3. Dependency injection for testability
class UserService {
  constructor(httpClient = fetch) {
    this.httpClient = httpClient;
  }
  
  async getUser(id) {
    const response = await this.httpClient(`/api/users/${id}`);
    return response.json();
  }
}

// Test with mock client
test("UserService.getUser", async () => {
  const mockClient = jest.fn().mockResolvedValue({
    json: () => Promise.resolve({ id: 1, name: "John" })
  });
  
  const service = new UserService(mockClient);
  const user = await service.getUser(1);
  
  expect(user.name).toBe("John");
  expect(mockClient).toHaveBeenCalledWith("/api/users/1");
});

// 4. LocalStorage mock
const mockLocalStorage = (() => {
  let store = {};
  
  return {
    getItem: jest.fn((key) => store[key] || null),
    setItem: jest.fn((key, value) => {
      store[key] = value.toString();
    }),
    removeItem: jest.fn((key) => {
      delete store[key];
    }),
    clear: jest.fn(() => {
      store = {};
    })
  };
})();

global.localStorage = mockLocalStorage;

// 5. IntersectionObserver mock
global.IntersectionObserver = class IntersectionObserver {
  constructor(callback) {
    this.callback = callback;
  }
  
  observe(element) {
    // Simulate element being visible
    this.callback([{
      target: element,
      isIntersecting: true,
      intersectionRatio: 1
    }]);
  }
  
  unobserve() {}
  disconnect() {}
};

Example: Integration testing with real APIs

// Test helpers for API testing
class APITestHelpers {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async waitForCondition(conditionFn, timeout = 5000) {
    const startTime = Date.now();
    
    while (Date.now() - startTime < timeout) {
      if (await conditionFn()) {
        return true;
      }
      await this.sleep(100);
    }
    
    throw new Error("Timeout waiting for condition");
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  async setupTestData() {
    // Create test data in database
    await fetch(`${this.baseURL}/test/setup`, {
      method: "POST"
    });
  }
  
  async cleanupTestData() {
    // Clean up test data
    await fetch(`${this.baseURL}/test/cleanup`, {
      method: "POST"
    });
  }
}

// Integration test example
describe("User API Integration", () => {
  const helpers = new APITestHelpers("http://localhost:3000");
  
  beforeEach(async () => {
    await helpers.setupTestData();
  });
  
  afterEach(async () => {
    await helpers.cleanupTestData();
  });
  
  test("creates and retrieves user", async () => {
    // Create user
    const createResponse = await fetch("http://localhost:3000/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: "Test User",
        email: "test@example.com"
      })
    });
    
    expect(createResponse.ok).toBe(true);
    const createdUser = await createResponse.json();
    expect(createdUser.id).toBeDefined();
    
    // Retrieve user
    const getResponse = await fetch(
      `http://localhost:3000/api/users/${createdUser.id}`
    );
    
    expect(getResponse.ok).toBe(true);
    const retrievedUser = await getResponse.json();
    expect(retrievedUser.name).toBe("Test User");
  });
  
  test("handles network errors gracefully", async () => {
    // Test with invalid URL
    const response = await fetch("http://localhost:3000/api/invalid");
    expect(response.ok).toBe(false);
    expect(response.status).toBe(404);
  });
});
Note: Mock APIs for unit tests, use real APIs for integration tests. Mock Service Worker provides realistic network mocking. Use dependency injection to make code testable. Mock browser APIs (localStorage, IntersectionObserver) in test environment.

20.6 Progressive Enhancement with Modern APIs

Principle Description
Core Functionality First Ensure basic features work without modern APIs.
Enhancement Layers Add features progressively based on capability detection.
Resilient Foundation Build on HTML/CSS, enhance with JavaScript.
Feature Queries Use CSS @supports and JavaScript feature detection.
Adaptive Loading Adjust features based on device capabilities and network.
Graceful Fallbacks Provide alternative experiences when APIs unavailable.

Example: Progressive enhancement framework

// Progressive enhancement manager
class ProgressiveEnhancement {
  constructor() {
    this.features = this.detectFeatures();
    this.applyEnhancements();
  }
  
  detectFeatures() {
    return {
      // Core APIs
      fetch: "fetch" in window,
      promises: "Promise" in window,
      
      // Storage
      localStorage: this.testLocalStorage(),
      indexedDB: "indexedDB" in window,
      
      // Observers
      intersectionObserver: "IntersectionObserver" in window,
      resizeObserver: "ResizeObserver" in window,
      
      // Modern features
      serviceWorker: "serviceWorker" in navigator,
      webGL: this.testWebGL(),
      webAssembly: typeof WebAssembly === "object",
      
      // Device capabilities
      touch: "ontouchstart" in window,
      deviceMemory: navigator.deviceMemory,
      connection: navigator.connection?.effectiveType
    };
  }
  
  testLocalStorage() {
    try {
      const test = "__test__";
      localStorage.setItem(test, test);
      localStorage.removeItem(test);
      return true;
    } catch (e) {
      return false;
    }
  }
  
  testWebGL() {
    const canvas = document.createElement("canvas");
    return !!(
      canvas.getContext("webgl") ||
      canvas.getContext("experimental-webgl")
    );
  }
  
  applyEnhancements() {
    const html = document.documentElement;
    
    // Add feature classes
    Object.keys(this.features).forEach(feature => {
      if (this.features[feature]) {
        html.classList.add(`has-${feature}`);
      } else {
        html.classList.add(`no-${feature}`);
      }
    });
    
    // Apply enhancements based on features
    if (this.features.intersectionObserver) {
      this.enableLazyLoading();
    }
    
    if (this.features.serviceWorker) {
      this.registerServiceWorker();
    }
    
    if (this.features.webGL) {
      this.enable3DGraphics();
    }
    
    // Adaptive loading based on device
    if (this.features.connection === "slow-2g" || this.features.connection === "2g") {
      this.enableDataSaver();
    }
    
    if (this.features.deviceMemory && this.features.deviceMemory < 4) {
      this.reduceFeaturesForLowMemory();
    }
  }
  
  enableLazyLoading() {
    const images = document.querySelectorAll("img[data-src]");
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          observer.unobserve(img);
        }
      });
    });
    
    images.forEach(img => observer.observe(img));
  }
  
  async registerServiceWorker() {
    try {
      await navigator.serviceWorker.register("/sw.js");
      console.log("Service Worker registered");
    } catch (error) {
      console.error("SW registration failed:", error);
    }
  }
  
  enable3DGraphics() {
    console.log("3D graphics available");
    // Initialize WebGL-based features
  }
  
  enableDataSaver() {
    console.log("Data saver mode enabled");
    // Reduce image quality, disable autoplay, etc.
  }
  
  reduceFeaturesForLowMemory() {
    console.log("Reducing features for low memory device");
    // Simplify UI, reduce animations, etc.
  }
}

// Initialize on page load
document.addEventListener("DOMContentLoaded", () => {
  new ProgressiveEnhancement();
});

Example: Adaptive component loading

// Adaptive component loader
class AdaptiveLoader {
  constructor() {
    this.capabilities = this.assessCapabilities();
  }
  
  assessCapabilities() {
    return {
      tier: this.getDeviceTier(),
      network: this.getNetworkQuality(),
      features: this.getSupportedFeatures()
    };
  }
  
  getDeviceTier() {
    const memory = navigator.deviceMemory || 4;
    const cores = navigator.hardwareConcurrency || 2;
    
    if (memory >= 8 && cores >= 8) return "high";
    if (memory >= 4 && cores >= 4) return "medium";
    return "low";
  }
  
  getNetworkQuality() {
    const connection = navigator.connection;
    if (!connection) return "unknown";
    
    const effectiveType = connection.effectiveType;
    if (effectiveType === "4g") return "fast";
    if (effectiveType === "3g") return "medium";
    return "slow";
  }
  
  getSupportedFeatures() {
    return {
      webGL: !!document.createElement("canvas").getContext("webgl"),
      webAssembly: typeof WebAssembly === "object",
      workers: typeof Worker !== "undefined"
    };
  }
  
  async loadComponent(componentName) {
    const { tier, network, features } = this.capabilities;
    
    // Load appropriate version based on capabilities
    if (tier === "high" && network === "fast") {
      // Load full-featured version
      return import(`./components/${componentName}/full.js`);
    } else if (tier === "medium" || network === "medium") {
      // Load standard version
      return import(`./components/${componentName}/standard.js`);
    } else {
      // Load lite version
      return import(`./components/${componentName}/lite.js`);
    }
  }
  
  async loadMedia(mediaUrl) {
    const { tier, network } = this.capabilities;
    
    // Select appropriate media quality
    let quality;
    
    if (tier === "high" && network === "fast") {
      quality = "high";
    } else if (network === "slow" || tier === "low") {
      quality = "low";
    } else {
      quality = "medium";
    }
    
    return `${mediaUrl}?quality=${quality}`;
  }
}

// Usage
const loader = new AdaptiveLoader();

// Load component based on device capabilities
loader.loadComponent("VideoPlayer").then(({ default: VideoPlayer }) => {
  const player = new VideoPlayer();
  player.init();
});

// Load appropriate media quality
const videoUrl = await loader.loadMedia("/videos/intro.mp4");
videoElement.src = videoUrl;
Note: Progressive enhancement ensures everyone gets working experience, with better browsers getting enhanced features. Build resilient foundation with HTML/CSS. Enhance with JavaScript based on feature detection. Adapt to device capabilities and network conditions.
Warning: Don't assume all users have modern browsers or fast connections. Test on low-end devices and slow networks. Don't break core functionality for users without cutting-edge features. Monitor real-user metrics to understand actual device/network distribution.

API Integration and Development Best Practices

  • Always use feature detection, never assume API availability
  • Load polyfills conditionally to minimize bundle size
  • Implement comprehensive error handling at all levels
  • Provide meaningful fallbacks when APIs unavailable
  • Debounce user input and throttle high-frequency events
  • Cache API responses with appropriate TTL to reduce network calls
  • Deduplicate simultaneous requests for same resource
  • Use Web Workers for CPU-intensive operations
  • Test across multiple browsers and devices
  • Mock APIs for unit tests, use real APIs for integration tests
  • Practice progressive enhancement - build on solid foundation
  • Adapt features based on device capabilities and network quality
  • Monitor performance and error rates in production