Skip to content

WebReflection/linkedom

Repository files navigation

🔗 linkedom

DownloadsBuild StatusCoverage Status

Social Media Photo byJJ YingonUnsplash

This is not a crawler!

LinkeDOM is atriple-linked listbased DOM-like namespace, for DOM-less environments, with the following goals:

  • avoidmaximum callstack/recursion orcrashes,even under heaviest conditions.
  • guaranteelinear performancefrom small to big documents.
  • beclose to thecurrentDOM standard,butnot too close.
import{DOMParser,parseHTML}from'linkedom';

// Standard way: text/html, text/xml, image/svg+xml, etc...
// const document = (new DOMParser).parseFromString(html, 'text/html');

// Simplified way for HTML
const{
// note, these are *not* globals
window,document,customElements,
HTMLElement,
Event,CustomEvent
// other exports..
}=parseHTML(`
<!doctype html>
<html lang= "en" >
<head>
<title>Hello SSR</title>
</head>
<body>
<form>
<input name= "user" >
<button>
Submit
</button>
</form>
</body>
</html>
`);

// builtin extends compatible too 👍
customElements.define('custom-element',classextendsHTMLElement{
connectedCallback(){
console.log('it works 🥳');
}
});

document.body.appendChild(
document.createElement('custom-element')
);

document.toString();
// the SSR ready document

document.querySelectorAll('form, input[name], button');
// the NodeList of elements
// CSS Selector via CSSselect

What's New

  • inv0.11a newlinkedom/workerexport has been added. This works withdeno,Web, and Service Workers, and it's not strictly coupled with NodeJS. Please note, this export does not includecanvasmodule, and theperformanceis retrieved from theglobalThiscontext.

Serializing as JSON

LinkeDOMuses a blazing fastJSDON serializer,and nodes, as well as whole documents, can be retrieved back viaparseJSON(value).

// any node can be serialized
constarray=document.toJSON();

// somewhere else...
import{parseJSON}from'linkedom';

constdocument=parseJSON(array);

Please note thatCustom Elementswon't be upgraded, unless the resulting nodes are imported viadocument.importNode(nodeOrFragment, true).

Alternatively,JSDON.fromJSON(array, document)is able to initialize right awayCustom Elementsassociated with the passeddocument.

Simulating JSDOM Bootstrap

This module is based onDOMParserAPI, hence it creates anewdocumenteach timenew DOMParser().parseFromString(...)is invoked.

As there'sno global pollutionwhatsoever, to retrieve classes and features associated to thedocumentreturned byparseFromString,you need to access itsdefaultViewproperty, which is a special proxy that lets you getpseudo-global-but-not-globalproperties and classes.

Alternatively, you can use theparseHTMLutility which returns a pseudowindowobject with all the public references you need.

// facade to a generic JSDOM bootstrap
import{parseHTML}from'linkedom';
functionJSDOM(html){returnparseHTML(html);}

// now you can do the same as you would with JSDOM
const{document,window}=newJSDOM('<h1>Hello LinkeDOM 👋</h1>');

Data Structure

The triple-linked list data structure is explained below inHow does it work?,theDeep Dive,and thepresentation on Speakeasy JS.

F.A.Q.

Why "not too close"?

LinkeDOMhas zero intention to:

  • implement all thingsJSDOMalready implemented. If you need a library which goal is to be 100% standard compliant, pleaseuse JSDOMbecauseLinkeDOMdoesn't want to be neirly as bloated nor as slow asJSDOMis
  • implement features not interesting forServer Side Rendering.If you need to pretend your NodeJS, Worker, or any other environment, is a browser, pleaseuse JSDOM
  • other points listed, or not, in the followungF.A.Q.s:this project will always prefer the minimal/fast approach over 100% compliant behavior. Again, if you are looking for 100% compliant behavior and you are not willing to have any compromise in the DOM, this isnotthe project you are looking for

That's it, the rule of thumb is: do I want to be able to render anything, and as fast as possible, in a DOM-less env?LinkeDOMis great!

Do I need a 100% spec compliant env that simulate a browser? I rather usecypressorJSDOMthen, asLinkeDOMis not meant to be a replacement for neither projects.

Are live collections supported?

TheTL;DRanswer isno.Live collections are considered legacy, are slower, have side effects, and it's not intention ofLinkeDOMto support these, including:

  • getElementsByTagNamedoes not update when nodes are added or removed
  • getElementsByClassNamedoes not update when nodes are added or removed
  • childNodes,if trapped once, does not update when nodes are added or removed
  • children,if trapped once, does not update when nodes are added or removed
  • attributes,if trapped once, does not update when attributes are added or removed
  • document.all,if trapped once, does not update when attributes are added or removed

If any code you are dealing with does something like this:

const{children}=element;
while(children.length)
target.appendChild(children[0]);

it will cause an infinite loop, as thechildrenreference won't side-effect when nodes are moved.

You can solve this in various ways though:

// the modern approach (suggested)
target.append(...element.children);

// the check for firstElement/Child approach (good enough)
while(element.firstChild)
target.appendChild(element.firstChild);

// the convert to array approach (slow but OK)
constlist=[].slice.call(element.children);
while(list.length)
target.appendChild(list.shift());

// the zero trap approach (inefficient)
while(element.childNodes.length)
target.appendChild(element.childNodes[0]);
Are childNodes and children always same?

Nope,these are discovered each time, so when heavy usage of theselistsis needed, but no mutation is meant, just trap these once and use these like a frozen array.

functioneachChildNode({childNodes},callback){
for(constchildofchildNodes){
callback(child);
if(child.nodeType===child.ELEMENT_NODE)
eachChildNode(child,callback);
}
}

eachChildNode(document,console.log);

How does it work?

All nodes are linked on both sides, and all elements consist of 2 nodes, also linked in between.

Attributes are always at the beginning of an element, while zero or more extra nodes can be found before the end.

A fragment is a special element without boundaries, or parent node.

Node: ← node →
Attr<Node>: ← attr → ↑ ownerElement?
Text<Node>: ← text → ↑ parentNode?
Comment<Node>: ← comment → ↑ parentNode?
Element<Node>: ← start ↔ end → ↑ parentNode?

Fragment<Element>: start ↔ end

Element example:

parentNode? (as shortcut for a linked list of previous nodes)
↑
├────────────────────────────────────────────┐
│ ↓
node? ← start → attr* → text* → comment* → element* → end → node?
↑ │
└────────────────────────────────────────────┘


Fragment example:

┌────────────────────────────────────────────┐
│ ↓
start → attr* → text* → comment* → element* → end
↑ │
└────────────────────────────────────────────┘

If this is not clear, feel free toread more in the deep dive page.

Why is this better?

MovingNnodes from a container, being it either anElementor aFragment,requires the following steps:

  • update the firstleftlink of the moved segment
  • update the lastrightlink of the moved segment
  • connect theleftside, if any, of the moved node at the beginning of the segment, with therightside, if any, of the node at the end of such segment
  • update theparentNodeof the segment to eithernull,or the newparentNode

As result, there are no array operations, and no memory operations, and everything is kept in sync by updating a few properties, so that removing3714sparse<div>elements in a12Mdocument, as example, takes as little as3ms,while appending a whole fragment takes close to0ms.

Trynpm run benchmark:htmlto see it yourself.

This structure also allows programs to avoid issues such as "Maximum call stack size exceeded"(basicHTML),or "JavaScript heap out of memory"crashes(JSDOM),thanks to its reduced usage of memory and zero stacks involved, hence scaling better from small to very big documents.

ArechildNodesandchildrenalways computed?

As everything is awhile(...)loop away, by default this module does not cache anything, specially because caching requires state invalidation for each container, returned queries, and so on. However, you can importlinkedom/cachedinstead, as long as youunderstand its constraints.

Parsing VS Node Types

This module parses, and works, only with the followingnodeType:

  • ELEMENT_NODE
  • ATTRIBUTE_NODE
  • TEXT_NODE
  • COMMENT_NODE
  • DOCUMENT_NODE
  • DOCUMENT_FRAGMENT_NODE
  • DOCUMENT_TYPE_NODE

Everything else, at least for the time being, is consideredYAGNI,and it won't likely ever land in this project, as there's no goal to replicate deprecated features of this aged Web.

Cached VS Not Cached

This module exports bothlinkedomandlinkedom/cached,which are basically the exact same thing, except the cached version outperformslinkedomin these scenarios:

  • the document, or any of its elements, are rarely changed, as opposite of frequently mutated or manipulated
  • the use-case needs many repeatedCSSselectors, over a sporadically mutated "tree"
  • the generic DOM mutation time isnota concern (each, removal or change requires a whole document cache invalidation)
  • theRAMisnota concern (all cached results are held intoNodeListarrays until changes happen)

On the other hand, the basic,non-cached,module, grants the following:

  • minimal amount ofRAMneeded, given any task to perform, as nothing is ever retained onRAM
  • linear fast performance for anyevery-time-newstructure, such as those created viaimportNodeorcloneNode(i.e. template literals based libraries)
  • much faster DOM manipulation, without side effect caused by cache invalidation

Benchmarks

To run the benchmark locally, please follow these commands:

git clone https://github /WebReflection/linkedom.git

cdlinkedom/test
npm i

cd..
npm i

npm run benchmark