Web Performance Calendar

The speed geek's favorite time of year
2017 Edition
ABOUT THE AUTHOR

Andrea Giammarchi

Andrea Giammarchi, @WebReflection on Twitter, is a Software Engineer with 18+ of full stack development experience.

Open Source Software enthusiast and contributor since about ever, Andrea is mostly known for his polyfills or crazy libraries idea like hyper(HTML).

NodeJS, Internet of Things, native GNOME JS development, or Linux tools, Andrea Open Source software and experiments can be found wherever there's some JavaScript to play with.

Available since IE9, DOM Ranges have been mostly advocated only as a way to select some text on the document, eventually copy it, or pollute users selections with extra content for social sharing.

These use cases are great and useful … but also half of the story.

What is a Range ?

If we know about the document fragment element, a transparent container that can contain zero, one, or many nodes, we can say that ranges can be used as transparent fragments able to group one or more live nodes together, without removing those nodes during such operation, like a fragment appending any node would do otherwise.

// basic range example: clean all the body !
const range = document.createRange();
// you can do new Range({...}) in modern browsers

// inclusive, first child to last one
range.setStartBefore(document.body.firstChild);
range.setEndAfter(document.body.lastChild);

// drop'em all at once
range.deleteContents();

What else can a Range do ?

Removing many elements at once as seen in the previous example, without going too dirty with node.textContent = "" or node.innerHTML = "" is something DOM users have been looking for long time.

// remember these kind of while loops?
while (node.hasChildNodes()) {
  node.removeChild(node.lastChild);
}
// NO MORE 🎉

Even handier, is the ability to collect into a fragment many nodes at once via range.extractContents(), eventually moving these nodes somewhere else through a single operation.

// append first 3 nodes at the end
const range = document.createRange();

range.setStartBefore(document.body.childNodes[0]);
range.setEndBefore(document.body.childNodes[3]);

// a one liner operation
document.body.appendChild(range.extractContents());

Another handy method is range.cloneContents() which creates a fragment with content cloned from the currently selected live range.

How is performance any better ?

Mutating many nodes at once means less reflows, repaints, and eventually less events consecutively dispatched.

Using ranges can also be as fast, or up to 10X faster, than using any other technique that involves many DOM changes per node.

You can test live your browser on this CodePen demo or see screenshots from various browsers.

Safari / Web

safari/web benchmark


Chrome

chrome benchmark


Firefox

firefox benchmark


Edge

edge benchmark


IE9

ie9 benchrmak


Any limitation ?

As far as I know there is no way to create spare ranges, something ideal to grab, clone, remove, or move as example many selected items.

However, for various list related operations, ranges can already boost up many common tasks, like prioritizing or sorting.

// create abc content
document.body.appendChild(
  ['a', 'b', 'c'].reduce(
    (f, t) => (f.append(t), f),
    document.createDocumentFragment()
  )
);

sortContentDesc(document.body);

function sortContentDesc(container) {

  if (!container.hasChildNodes()) return;

  // grab all content
  const range = new Range;
  range.setStartBefore(container.firstChild);
  range.setEndAfter(container.lastChild);

  // perform sort operation offline and put nodes back
  container.append(
    ...Array.from(range.extractContents().childNodes)
            .sort((a, b) => a.textContent < b.textContent ? 1 : -1)
  );
}

Simple and efficient, you can see the rest of the Range API in MDN.