2

When looking at the childNodes of a parentNode the data structure appears to be an array like object.

Instead of iterating through each childNode and removing it - is it possible to just remove the array-like object?

I'm thinking of performance here and would like to avoid iterating through the children and removing them one by one.

In my case, I'm creating child Li elements on a parent Ul:

  _createLi(address) {
    const parentUl = document.querySelector("ul#parent");
    const li = document.createElement("li");
    li.appendChild(document.createTextNode(address));
    parentUl.appendChild(li);
  }

I can remove them like this:

  _removeLi(node) {
    while (!node.lastChild) node.removeChild(!node.lastChild);
  }

This works but it iterates through each node. How do you remove all childNodes by removing the object like array that contains them, and is it possible in O(1)?

zero_cool
  • 3,960
  • 5
  • 39
  • 54
  • How about finding the parent of the list and then just removing the list from its parent? – Olafant Mar 05 '19 at 18:20
  • 3
    "I'm thinking of performance here" This is what is called premature optimization unless you have measured this piece of code to be a bottleneck :) – Yury Tarabanko Mar 05 '19 at 18:20
  • 1
    You could compare you current approach to say `_removeLi(node) { node.innerHTML = ''; }` but I'm not sure this would be faster. – Yury Tarabanko Mar 05 '19 at 18:22
  • 1
    I don't think it's possible in O(1). Even with `innerHTML`, it's most likely not O(1) under the hood. O(N) shouldn't ever be a problem if you avoid N DOM reflows. Try removing your list from the document, remove the items and then add back the list into the document. – plalx Mar 05 '19 at 18:35
  • @yuri NEVER use node.innerHTML = '''. The nodes appear removed but internally they still exist and add overhead to the browser. – zero_cool Mar 05 '19 at 18:48
  • This is not premature optimization BTW - the list I'm working on can get very long - and the UI can appear frozen / laggy. – zero_cool Mar 05 '19 at 18:49
  • @zero_cool "NEVER use node.innerHTML = ''' The nodes appear removed but internally they still exist and add overhead to the browser." Hmmm, where did you get this from? This would be a major memory issue in any browser. – Yury Tarabanko Mar 05 '19 at 21:39
  • I mean I know there are security considerations if you don't control html string. But in case of an empty string this is not a problem ;) – Yury Tarabanko Mar 05 '19 at 21:42

2 Answers2

2

I really doubt children can be removed in O(1), even with node.innerHTML = '' as the underlying implementation may very well be a O(N) operation.

What you should consider to improve performance is to minimize the number of DOM reflows.

  1. You could try replacing the element with a clone.

const list = document.querySelector('ul');
const listClone = list.cloneNode(false);
list.parentNode.replaceChild(listClone, list);
<ul>
  <li>First</li>
  <li>Last</li>
</ul>
  1. You could try removing the list from the DOM, perform the manipulations and add it back.

withElOutOfFlow(document.querySelector('ul'), el => {
   while(el.lastChild) el.removeChild(el.lastChild);
});

function withElOutOfFlow(el, callback) {
  const parent = el.parentNode;
  
  if (!parent) {
    callback(e);
    return;
  }
  
  const nextSibling = el.nextSibling;
  parent.removeChild(el);
  callback(el);
  
  if (nextSibling) parent.insertBefore(el, nextSibling);
  else parent.appendChild(el);
}
<ul>
  <li>First</li>
  <li>Last</li>
<ul>
plalx
  • 42,889
  • 6
  • 74
  • 90
  • If you remove a parentNode with childnre - would that not be O(1)? – zero_cool Mar 05 '19 at 19:40
  • I assume it would (we would have to know more about the underlying DOM implementation), but that's also not the same as removing only the children, which is what I was referring to. – plalx Mar 05 '19 at 19:46
  • @zero_cool I have no idea why you accepted the above answer. I posted basically the same thing 1 hour before (solution #1), except `insertAdjacentHTML` which is pointless IMO. Removing the original list by appending it to a document fragment also seems quite silly. Just use `list.remove()` and it's out of the flow, document fragments are useful when to append many children in a single DOM reflow. All in all both of my solutions seems much more appropriate and intuitive. Furthermore, the original answer wouldn't even work if the list wasn't the last child (or only child) of it's parent. – plalx Mar 06 '19 at 13:55
  • 1
    All in all, the accepted answer is just a bad implementation of the solution #1 I proposed (replacing the existing UL by a copy). – plalx Mar 06 '19 at 14:04
  • @plalx My solution is nothing like solution#1. The replacement is an empty `
      ` not a copy. *"wouldn't even work if the list wasn't the last child (or only child) of it's parent"* The list can be anywhere as long as it's the first list on the page just like solution 1 (which is the only thing my answer has in common with solution 1). Reread my answer and understand it, instead of blatantly criticizing it. As far as which is faster just count the times the DOM is accessed.
    – zer00ne Mar 07 '19 at 01:18
  • @zer00ne Pehaps you should read your own answer again. `insertAdjacentHTML('beforeend', '
      ')` will insert the list at the end of the parent, so it is not guaranteed to be in the same position as the original list if the original list wasn't the latest children of it's parent. Furthermore, appending an element to a document fragment to perform a removal is just misleading, use `el.remove()`. Finally, the goal of the OP was to clear it's original list, so the new list MUST be a copy. With your solution he would have to use `originalList.outerHTML` to copy it.
      – plalx Mar 07 '19 at 12:01
    • I haven't tested, but I'm quite sure `el.cloneNode(false)` is faster than making the engine parse a new HTML string. `insertAdjacentHTML` is a replacement for `el.innerHTML += moreHtml`, but you've used it to replace `el.appendChild(newEl)` which is not justified IMO. [Performance seems quite bad a s well](https://jsperf.com/insertadjacenthtml-perf/3). – plalx Mar 07 '19 at 12:08
    • Concerning `.createDocumentFragment()` [The key difference is that because the document fragment isn't part of the active document tree structure, **changes made to the fragment don't affect the document, cause reflow, or incur any performance impact that can occur when changes are made.**](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) – zer00ne Mar 07 '19 at 15:23
    • *"..will insert the list at the end of the parent, so it is not guaranteed to be in the same position as the original list if the original list wasn't the latest children of it's parent. "* I didn't think of that you're right, so I updated my answer with 2 examples that are faster than yours. – zer00ne Mar 07 '19 at 15:39
    • @zer00ne When you remove an element from the DOM it's not part of the document flow anymore. There's no point appending the original element to a document fragment just for having the removal side effects. If you whish to reuse the original element, mutate it outside of the DOM flow and then put it back you could use my solution #2. The range solution is interesting, but your tests aren't conclusive at all as you don't have enough data. Just tried in Edge and the solution is 8% slower than my first solution. – plalx Mar 07 '19 at 15:44
    • What data should be added besides the browsers that don't matter (ie Not Chrome or Firefox)? – zer00ne Mar 07 '19 at 15:48
    • @zer00ne I like the range solution by the way, I was not aware of that so I've learned something new and it could very well be the most effective way of removing all children while keeping the original element. Could you also add the solution #2 to your test case, since it's the only equivalent solution (other are doing copies). Good find! – plalx Mar 07 '19 at 15:56
    • Thanks @plalx I stumbled upon the `.deleteContents()` and thought I found something original then I found [this answer](https://stackoverflow.com/a/22966637/2813224) which has both of our solutions. Here's the updated [Perf](https://jsperf.com/remove-all-list-items/3) – zer00ne Mar 08 '19 at 02:24
    • @plalx correction the latest revision of [Perf](https://jsperf.com/remove-all-list-items/4) – zer00ne Mar 08 '19 at 02:34
    • @zer00ne The fn definition of withElOutOfFlow should be in the setup phase though. – plalx Mar 08 '19 at 03:56
    2

    Update

    Faster than .cloneNode() and .replaceChild()

    Range API

    5% faster than ALL examples
    (including accepted answer)

    The following example is from the Range API:

    const rng = document.createRange();
    rng.selectNodeContents(document.querySelector('ul'));
    rng.deleteContents();
    

    the Range interface deals with fragments of a Document that comprise of Nodes and Text. Although it appears slow, it's actually fast and 100% compatible with all browsers.

    Range Methods

    .createRange()
    .selectNodeContents()
    .deleteContents()


    .replaceWith() and .createElement()

    15% slower than All examples
    This combo is faster than most examples and less verbose (with the exception of the Range demo being the fastest.)

     document.querySelector('ul').replaceWith(document.createElement('ul'));
    

    That's 2 DOM interactions: Find the list and replace it with an empty list. See Demo 1. If you want to support IE11 (ATM 2.26% global share) , then don't use it.


    Demo 1

    Range API

    const rng = document.createRange();
    rng.selectNodeContents(document.querySelector('ul'));
    rng.deleteContents();
    ul {
      min-height: 30px;
      min-width: 30px;
      outline: 1px dashed red;
    }
    <ul>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
    </ul>

    Demo 2

    .replaceWith() and .createElement()

    document.querySelector('ul').replaceWith(document.createElement('ul'));
    ul {
      min-height: 30px;
      min-width: 30px;
      outline: 1px dashed red;
    }
    <ul>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
    </ul>

    .insertAdjacentHTML() & .createDocumentFragment()

    @50% slower than All examples
    The best way to optimize any DOM manipulation is not to have them. Keep DOM access to a minimum -- it's very time consuming. Every time the JS engine looks for an element it will traverse the DOM tree, every time a tag is added or removed, the nodes (element, text, etc.) that are already in the DOM must be recalculated for positioning and dimensions so even if the number of nodes involved are just a few, it can domino into an exceptionally long process for the browser. This is called reflow and a similar problem involving CSS styles is called a repaint.


    The following demo removes all <li> in a <ul> with 4 DOM operations:
    1. References the <ul> -- 1 lookup

    2. Reference parent of <ul> append an empty <ul> -- 1 lookup, 1 addition

    3. Create a documentFragment and append original <ul> to it -- 1 removal

    .insertAdjacentHTML() non-destructively renders htmlString into HTML and it's highly optimized.

    .createDocumentFragment() never touches the DOM and whatever is attached to it is no longer touching the DOM.


    Demo 3

    .insertAdjacentHTML and .createDocumentFragment()

    // Reference the <ul>
    const list = document.querySelector('ul');
    
    // Reference parent of <ul> append an empty <ul>
    list.parentElement.insertAdjacentHTML('beforeend', `<ul></ul>`);
    
    // Create a document fragment and append original <ul> to it
    document.createDocumentFragment().appendChild(list);
    ul {
      min-height: 30px;
      min-width: 30px;
      outline: 1px dashed red;
    }
    <ul>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
      <li>ITEM</li>
    </ul>
    zer00ne
    • 41,936
    • 6
    • 41
    • 68
    • Why do you need Step 3 - if you've already referenced the parent of the ul and appended an empty ul? – zero_cool Mar 05 '19 at 22:59
    • Step 3 is the actual removal of the `
    • `. The whole procedure is non-destructive. You can do DOM free tasks on the original `
        ` without any reflow as long as it's parent is `documentFragment` .
    • – zer00ne Mar 06 '19 at 07:45