linkedom

repository·main·Indexed 24 days ago

https://github.com/webreflection/linkedom

A high-performance, triple-linked list based DOM implementation designed for DOM-less environments like Node.js or Workers. It focuses on linear performance and memory efficiency, making it ideal for Server-Side Rendering (SSR). It provides utilities like parseHTML for simplified parsing, JSDON for JSON serialization, and a choice between standard and cached entry points (linkedom/cached) depending on mutation rates and RAM requirements.

Tokens
5.1K
Snippets
13
Records
31
Agent score
83%

What's inside linkedom

  1. How the triple-linked list data structure works

    main

    LinkeDOM uses a triple-linked list structure where all nodes are linked on both sides. This allows for extremely fast DOM manipulations (like moving thousands of nodes) because operations only require updating a few pointers rather than performing array or memory reallocations.

    Key characteristics:

    • Performance: Moving nodes is an $O(1)$ operation regarding memory/array shifts, making it scale linearly from small to very large documents.
    • Stability: The structure helps avoid "Maximum call stack size exceeded" errors and reduces memory pressure compared to JSDOM.
    • No Caching by default: To avoid the complexity of state invalidation, childNodes and children are computed on demand. If you need cached performance, you must explicitly import linkedom/cached.
  2. Understand LinkeDOM's lack of live collections

    main

    LinkeDOM does not support live collections (e.g., getElementsByTagName, children, childNodes, or attributes do not automatically update when the DOM is mutated). This design choice ensures linear performance and avoids the overhead of state invalidation.

    Warning: Using a trapped reference to a live collection in a loop can cause infinite loops if you are moving nodes. For example:

    // DANGEROUS: This will cause an infinite loop if 'children' is a live collection
    const {children} = element;
    while (children.length) {
      target.appendChild(children[0]);
    }

    Recommended approaches for moving nodes:

    1. Modern approach: target.append(...element.children);
    2. First-child approach: while (element.firstChild) target.appendChild(element.firstChild);
    3. Array conversion: const list = [].slice.call(element.children); while (list.length) target.appendChild(list.shift());
  3. Choose between linkedom and linkedom/cached

    main

    Linkedom provides two entry points that are functionally identical but optimized for different performance profiles:

    Use linkedom/cached when:

    • The document or its elements are rarely changed (low mutation rate).
    • You frequently use the same CSS selectors repeatedly.
    • You can tolerate whole-document cache invalidation upon any change/removal.
    • RAM usage is not a primary concern (cached results are held in NodeList arrays).

    Use linkedom (non-cached) when:

    • You need minimal RAM usage (nothing is retained in memory).
    • You are frequently creating new structures (e.g., via importNode or cloneNode).
    • You need fast DOM manipulation without the overhead of cache invalidation side effects.
  4. How LinkeDOM represents the DOM tree

    main

    LinkeDOM achieves high performance and low memory usage by avoiding array manipulation and retention. Instead of a traditional tree structure, it represents the DOM as a linear, double-linked list of nodes.

    This approach treats the DOM similarly to how a C-style string works: a sequence of characters (nodes) where elements are defined by boundaries rather than nested object hierarchies. This allows for linear crawling of the document and makes most operations simple property updates.

  5. Understand the Node and Element structure in LinkeDOM

    main

    LinkeDOM uses a linked-list based approach to represent the DOM. Nodes are connected via prev and next pointers.

    Key concepts:

    • Node Types: Defined by Node.END (-1), Node.ELEMENT (1), Node.ATTRIBUTE (2), and Node.TEXT (3).
    • Elements and Ends: An Element is automatically paired with an End node. The Element.end property points to this boundary node, which is used to manage the lifecycle and boundaries of the element's content.
    • Attributes: Attributes are linked nodes that exist within the element's structure but are distinct from child nodes.
    • Parentage: The parentNode property is automatically updated when nodes are appended or removed, providing a way to traverse upwards without backward loops.
    class Node {
      static END = -1;
      static ELEMENT = 1;
      static ATTRIBUTE = 2;
      static TEXT = 3;
    
      constructor(type) {
        this.type = type;
        this.prev = null;
        this.next = null;
      }
    }
    
    class Element extends Node {
      constructor(name) {
        super(Node.ELEMENT);
        this.name = name;
        this.end = new End(this);
        setAdjacent(this, this.end);
      }
    }
  6. Initialize Custom Elements when parsing JSON

    main

    When reconstructing a document from JSON, Custom Elements will not be automatically upgraded. To ensure they are initialized correctly, you have two options:

    1. Use document.importNode(nodeOrFragment, true) to import the nodes.
    2. Use JSDON.fromJSON(array, document) to initialize Custom Elements associated with the provided document immediately.
  7. Run tests and check coverage

    main

    To run the test suite, you must first build the project because ESM source code is transpiled into CJS for testing and coverage analysis.

    Workflow:

    1. Run npm run build.
    2. The test suite executes twice: once for the linkedom module and once for the linkedom/cached export.
    3. The project requires 100% code coverage. If a change decreases coverage, it will not be accepted.

    Handling Coverage for Accessors: For certain areas like HTMLClasses accessors that may not be fully augmented, you can bypass coverage requirements by wrapping your code in /* c8 ignore start */ and /* c8 ignore stop */ directives.

    npm run build
  8. Implement manual node manipulation with setBoundaries

    main

    LinkeDOM's internal logic relies on setting boundaries between nodes. If you are working with the underlying primitives, you can use setAdjacent to link two nodes or setBoundaries to insert a node between two existing ones while correctly handling element boundaries.

    const setAdjacent = (before, after) => {
      if (before !== null)
        before.next = after;
      if (after !== null)
        after.prev = before;
    };
    
    const setBoundaries = (before, current, after) => {
      setAdjacent(before, current);
      // skip to the node end if needed
      if (current.type === Node.ELEMENT)
        current = current.end;
      setAdjacent(current, after);
    };
  9. Register new HTML classes via registerHTMLClass

    main

    To ensure the parser creates specialized elements (e.g., HTMLExample) instead of generic HTMLElement instances, you must register the class using registerHTMLClass.

    When registering a class:

    1. Define the class extending HTMLElement.
    2. Call registerHTMLClass(tagName, ClassName).
    3. Export the class at the end of the file.

    If a single class should handle multiple HTML tags (like h1 through h6), pass an array of strings to registerHTMLClass.

    import {registerHTMLClass} from '../shared/register-html-class.js';
    import {HTMLElement} from './html-element.js';
    
    /**
     * @implements globalThis.HTMLHeadingElement
     */
    class HTMLHeadingElement extends HTMLElement {
      constructor(ownerDocument, localName = 'h1') {
        super(ownerDocument, localName);
      }
    }
    
    // registerHTMLClass accepts a string or an array of strings
    registerHTMLClass(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], HTMLHeadingElement);
    
    export {HTMLHeadingElement};
  10. Run linkedom benchmarks locally

    main

    To run the performance benchmarks, clone the repository, install dependencies in both the test and root directories, and execute the benchmark command.

    git clone https://github.com/WebReflection/linkedom.git
    
    cd linkedom/test
    npm i
    
    cd ..
    npm i
    
    npm run benchmark
  11. Iterate over an element's child nodes

    main

    Because attributes are part of the linked list but are not considered child nodes, you must skip them when iterating through an element's children. You can traverse the list using next and end pointers, checking the type of each node.

    const childNodes = element => {
      const nodeList = [];
      let {next, end} = element;
      while (next !== end) {
        if (next.type !== Node.ATTRIBUTE) {
          nodeList.push(next);
          if (next.type === Node.ELEMENT)
            next = next.end;
        }
        next = next.next;
      }
      return nodeList;
    };
  12. Simulate a JSDOM bootstrap

    main

    If you need a facade that behaves like JSDOM (returning a document and window object), you can wrap parseHTML in a function. This avoids global pollution while providing the same developer experience as JSDOM.

    import {parseHTML} from 'linkedom';
    
    function JSDOM(html) { return parseHTML(html); }
    
    const {document, window} = new JSDOM('<h1>Hello LinkeDOM 👋</h1>');