Repository navigation
Proposal: Bulk mutation methods for working with ranges of nodes #1369
Description
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): voidAnything 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
robbiespeed commentedon Apr 15, 2025
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
insertRangeBeforewould also be necessary.Having the operations inclusive of start and end would allow simplifying.
fromParentinmoveRangeBeforewould be redundant since it can be inferred fromstartNodeorendNode, the parents of which would need to be checked anyway to ensure they match.extractRangewouldn't be necessary, since one could create a DocumentFragment, then place the nodes inside usingfragment.insertRangeBefore.justinfagnani commentedon Apr 16, 2025
@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 withextractRange()andinsertBefore()with the resulting DocumentFragment, butinsertRangeBefore()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
which would preserve the marker nodes.
If the operation worked on start and end inclusive, then you would have to write something like:
Sometimes you want to move a range and its marker nodes, so with exclusive ranges you need write:
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.
smaug---- commentedon Aug 25, 2025
Curious, do you have some examples showing the slowness?
justinfagnani commentedon Aug 25, 2025
@smaug---- not off hand, but I'll try to write a benchmark soon.