🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Proposal: Bulk mutation methods for working with ranges of nodes #1369

Open

Description

@justinfagnani

What problem are you trying to solve?

It's very common in modern framework code to have to work with ranges of nodes as a unit, where a range is a sequence of sibling nodes. The operations include:

  • Clear a range of nodes
  • Move a range of nodes
  • Extract a range of nodes into a fragment

Two of the APIs are available on Range as deleteContents(), extractContents(), but using Range, even StaticRange, is slower than implementing these operations in JS. The move operation doesn't exist in any form, since moveBefore() is brand new.

While the JS code for implementing these operations is relatively simple, native methods would still reduce the amount of required utility code needed in some libraries and frameworks. Higher-level methods would also help reduce the number of wrapper JS objects created during traversal, and the total number of calls like .remove() and .nextSibling. The operations are often on the very hottest paths of rendering code, so any perf improvement is beneficial.

What solutions exist today?

Today frameworks implement these using traversal and individual node removal and insertion.

How would you solve it?

Add these methods to ParentNode:

/**
 * Removes the nodes between startNode and endNode exclusively.
 * - startNode and endNode must be children of the receiver, or nullish.
 * - If startNode is nullish, then start removing nodes at the first child of the receiver.
 * - If endNode is nullsih, then remove nodes until the last child of the receiver.
 */
deleteRange(startNode?: ChildNode, endNode?: ChildNode): void;

/**
 * Moves the nodes between startNode and endNode exclusively to a new DocumentFragment.
 */
extractRange(startNode?: ChildNode, endNode?: ChildNode): DocumentFragment;

/**
 * Atomically moves the nodes between startNode and endNode exclusively to the receiver, inserted
 * before refNode or the end of the container.
 */
moveRangeBefore(fromParent: ParentNode, startNode?: ChildNode, endNode?: ChildNode, refNode?: ChildNode): void

/**
 * Moves the nodes between startNode and endNode exclusively to the receiver, inserted
 * before refNode or the end of the container.
 * 
 * This is the same as moveRangeBefore() without the atomic operation guarantees and without
 * throwing in cases where moveRangeBefore() throws (the same as in moveBefore()).
 */
insertRangeBefore(fromParent: ParentNode, startNode?: ChildNode, endNode?: ChildNode, refNode?: ChildNode): void

Anything else?

This is an alternative to #736 that is smaller in scope. It helps with the bulk mutation operations, but doesn't deal with DOM serialization and how start and end nodes are represented.

Activity

  1. robbiespeed commented on Apr 15, 2025

    @robbiespeed

    I like the general idea of adding more powerful low level DOM methods, although it doesn't really cover the same use cases as #736 even putting aside serialisation / DOM representation aspects.

    A few notes on the API:

    Since move operations error between non-connected and connected areas (Or was that changed?) a insertRangeBefore would also be necessary.

    Having the operations inclusive of start and end would allow simplifying. fromParent in moveRangeBefore would be redundant since it can be inferred from startNode or endNode, the parents of which would need to be checked anyway to ensure they match. extractRange wouldn't be necessary, since one could create a DocumentFragment, then place the nodes inside using fragment.insertRangeBefore.

  2. justinfagnani commented on Apr 16, 2025

    @justinfagnani
    Author

    @robbiespeed yes, this would address a more narrow set of use cases than #736, namely mutating contiguous ranges of nodes. It's a simpler proposal because it doesn't introduce a new DOM interface, construct new novel tree structures, or affect parsing at all with special handling for certain comments. Those things could certainly still be added - these proposals aren't mutually exclusive.

    insertRangeBefore() is a good idea. You can accomplish it with extractRange() and insertBefore() with the resulting DocumentFragment, but insertRangeBefore() would bypass creating DocumentFragment instance.

    The reason I went with the operations working exclusive of the start and end nodes is to make it easy to work with the contents of ranges bracketed by marker nodes, which is how a lot of library code works. Clearing a dynamic expression would look something like

    container.deleteRange(expr.start, expr.end);

    which would preserve the marker nodes.

    If the operation worked on start and end inclusive, then you would have to write something like:

    if (expr.start.nextSibling !== expr.end) {
      container.deleteRange(expr.start.nextSibling, expr.end.previousSibling);
    }

    Sometimes you want to move a range and its marker nodes, so with exclusive ranges you need write:

    const fragment = container.extractRange(expr.start.previousSibling, expr.end.nextSibling);

    But this is safe because if either previous and next siblings are null, that means the marker is the first or last child respectively, and extracting to the beginning or end of the child list is correct.

  3. smaug---- commented on Aug 25, 2025

    @smaug----
    Contributor

    Two of the APIs are available on Range as deleteContents(), extractContents(), but using Range, even StaticRange, is slower than implementing these operations in JS.

    Curious, do you have some examples showing the slowness?

  4. justinfagnani commented on Aug 25, 2025

    @justinfagnani
    Author

    @smaug---- not off hand, but I'll try to write a benchmark soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions