Open-source project
fgnass/domino avatar
fgnass/domino

domino is a server-side DOM that trades proxies for speed

Server-side DOM implementation based on Mozilla's dom.js

791 stars134 forksJavaScriptBSD-2-Clause

At a glance

What is it?
domino is a Node implementation of the DOM built on Mozilla's dom.js, intended for building pages rather than scraping them, with no script execution and no proxy facade. It is what Wikimedia's Parsoid uses, it is BSD licensed, and it is old enough to still support Node versions without ES6 features.
Who is it for?
domino suits a Node service that has to produce or transform HTML without a browser, such as a renderer, an email renderer or a wiki parser, because it implements DOM level 4 with CSS selectors and no proxy overhead. It is the wrong tool for scraping, since it executes no scripts and downloads no external resources, and it is the wrong tool if you need the browser APIs that only a real engine provides.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A DOM in Node, built for building pages

domino's goal, as the README puts it with some emphasis, is to provide a DOM in Node. It is a server-side DOM implementation based on Mozilla's dom.js, written in JavaScript and published on npm as domino under a BSD-2-Clause licence.

The interesting design decision is stated early and explains everything else. In contrast to the original dom.js project, domino was not designed to run untrusted code. Because it does not have to defend against a hostile page, it does not have to hide its internals behind a proxy facade, which the README says makes the code simpler and also more performant, with a link to a benchmark project.

The consequence is visible in the API. Domino currently does not use any harmony or ES6 features like proxies or WeakMaps, and therefore also runs in older Node versions. The implementation classes are reachable directly, for example through domino.impl.Element, instead of being hidden behind proxy traps.

That is a deliberate trade, and it is the right one for the stated purpose. A renderer that only sees HTML it produced itself has no reason to pay for isolation it does not need, and a library that runs on older Node is a library that runs in the older Node versions many server side projects are still on.

Speed over compliance, and what that rules out

There is a section heading in the README that says it plainly: speed over compliance.

Domino is intended for building pages rather than scraping them, and as a consequence it does not execute scripts and does not download external resources. It also does not generally implement properties which have been deprecated in HTML5.

Those three omissions define the tool. A page that depends on JavaScript to render its content will produce an empty shell in domino, and a page whose layout depends on a stylesheet fetched from a CDN will render unstyled. If your requirement is to take a live URL and reproduce what a browser shows, domino is the wrong library, and the README is upfront that it never intended that.

It sticks to DOM level 4, which means attributes do not inherit the Node interface. The one visible consequence the project calls out is that Element.attributes is not a true JavaScript array, because domino does not use proxies. It is an object with a length property and an item(n) accessor method, with a link to issue 27 for the discussion. It does implement direct indexed accessors, element.attributes[i], and the collection is live.

That is a real difference from a browser, and code that does Array.isArray on attributes or calls attributes.forEach will break. The project accepts this as the price of the no-proxy design.

Creating a document, three different ways

Domino supports the DOM level 4 API, so the API documentation can be found on standard reference sites, with the README suggesting MDN's pages for Document and Node as a starting point. The only exception to that is the initial creation of a document, which has its own API.

The simplest form creates a window from an HTML string and a URL:

javascript
var domino = require('domino');
var Element = domino.impl.Element; // etc

var window = domino.createWindow('<h1>Hello world</h1>', 'http://example.com');
var document = window.document;

// alternatively: document = domino.createDocument(htmlString, true)

and the result behaves as you would expect, with querySelector returning the element and instanceof checks working against the exposed class.

For a more standards-compliant route there is DOMImplementation, which gives you a document created the same way a browser would create one:

javascript
var domino = require('domino');
var domimpl = domino.createDOMImplementation();
var doc = domimpl.createHTMLDocument();

One more knob is worth knowing. By default many domino methods are stored in writable properties, to allow polyfills, as browsers do. You can lock the implementation down by setting a global flag before the module is required:

javascript
global.__domino_frozen__ = true; // Must precede any `require('domino')`
var domino = require('domino');

The order matters, since the flag has to be set before the require call happens.

An incremental parser for interleaved work

There is an incremental parser available if you need to interleave parsing with other processing, which is the case when you are streaming a large document rather than receiving it all at once.

The pattern is a write, process and repeat cycle:

javascript
var incrParser = domino.createIncrementalHTMLParser();
incrParser.write('<p>hello<');
incrParser.write('b>&am');
incrParser.process(pauseAfter(1/*ms*/)); // can interleave processing
incrParser.write('p;');

The process method takes a predicate that returns true when the time budget is used up, and the example defines one with Date.now to pause after a given number of milliseconds. The loop at the end drains the remaining work, and incrParser.end() marks the document complete, after which incrParser.document().outerHTML gives you the serialised result.

Splitting the writes at awkward points, in the middle of a tag name and in the middle of an entity, is deliberate. It shows the parser handles partial input, which is the whole point of an incremental parser, and it gives you a feel for how a streaming server would drive it.

The API differs from the browser's, because the browser does not let you feed it bytes by hand, but it maps onto a Node stream transform quite directly.

CSS selectors come from Zest

Domino provides support for querySelector(), querySelectorAll() and matches(), backed by the Zest selector engine, which was written by the author of the tangram build tool.

Zest is a separate project, and the README links to it rather than describing the engine. What matters for a user is that the three selector entry points exist and behave the way the DOM standard describes, so code written against a browser generally works.

The selector support is what makes domino usable for real page work rather than only for text extraction. Building a document and then reaching into it by selector is the pattern most server side rendering code follows, and having querySelectorAll return a list rather than requiring a manual tree walk is the difference between a usable library and a toy.

Because the selector work is delegated to Zest, the selector dialect is Zest's rather than a browser's, and any divergence in an exotic selector would live in that project rather than here.

Linked list children, and how to traverse them fast

The optimization section describes an implementation choice that has a direct effect on your code.

Domino represents the DOM tree structure the way WebKit and other browser based implementations do, as a linked list of children, which is converted to an array based representation if and only if the Node#childNodes accessor is used.

So there is a fast path and a slow path, and the README says which one to stay on. You get the best performance from tree modification code, inserting and removing children, if you avoid Node#childNodes and traverse the tree using Node#firstChild and Node#nextSibling, or Node#lastChild and Node#previousSibling, or querySelector and similar.

The reason is that touching childNodes converts the tree to the array representation, and once it exists you have paid that cost. A renderer that inserts a thousand nodes and then walks them with firstChild and nextSibling never pays that cost, while the same code that reads childNodes once converts the whole subtree.

This is the kind of detail that only shows up in a library with an unusual performance profile, and it is worth internalising before benchmarking anything. It is also a good example of documentation that describes a mechanism rather than asserting a speed claim, which is more useful.

Conformance tests, maintenance and the Parsoid connection

Domino includes tests from the W3C DOM Conformance Suites and tests from the HTML Working Group. They can be run via npm test, or directly through the Mocha command line. The package manifest shows the test script as npm run lint followed by npm run mocha, with lint being jshint over lib and the test files, and the Mocha version pinned at ^12.0.0-rc.1 with should and jQuery as dev dependencies.

A separate script, regen-html5lib-tests, regenerates the html5lib test fixture from the tool in test/tools, which is how the HTML parsing conformance data is kept in sync.

The history is worth knowing because it explains the maintenance arrangement. The majority of the code was originally written by Andreas Gal and David Flanagan as part of dom.js. Felix Gnass extracted the code and turned it into a standalone npm package. The code has been maintained since 2013 by C. Scott Ananian on behalf of the Wikimedia Foundation, which uses it in the Parsoid project, and a large number of improvements have been made, mostly focusing on correctness, performance and, to a lesser extent, completeness.

The repository is not archived and the last push was on 2026-10-18, so the code is still being worked on even though the manifest version reads 2.1.8+git and there are no GitHub releases. The BSD-2-Clause licence covers the package, and the original dom.js copyright notice and disclaimer are in the included LICENSE file.

Editorial conclusion

domino suits a Node service that has to produce or transform HTML without a browser, such as a renderer, an email renderer or a wiki parser, because it implements DOM level 4 with CSS selectors and no proxy overhead. It is the wrong tool for scraping, since it executes no scripts and downloads no external resources, and it is the wrong tool if you need the browser APIs that only a real engine provides. Before adopting it, check that the features your code uses are in DOM level 4, and read the caveats on Element.attributes, which is not a real array because domino avoids proxies.

Frequently asked questions

How do I create a document with domino?

Use domino.createWindow with an HTML string and a URL, then read window.document, or domino.createDocument for the simpler form. For a standards-compliant document you can use domino.createDOMImplementation and then createHTMLDocument, and an incremental parser is available through domino.createIncrementalHTMLParser.

Does domino execute JavaScript or fetch external resources?

No. The README states that domino does not execute scripts and does not download external resources, because it is intended for building pages rather than scraping them. It also does not generally implement properties deprecated in HTML5, and it sticks to DOM level 4.

Why is Element.attributes not a real array in domino?

Because domino does not use proxies, Element.attributes is an object with a length property and an item(n) accessor method rather than a true JavaScript array. It does support direct indexed accessors such as element.attributes[i] and the collection is live, and the project links issue 27 for the full discussion.

Official sources

  1. fgnass/domino on GitHub
  2. Issues
  3. License: BSD-2-Clause
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fgnass-domino.svg)](https://hysenlabs.com/projects/fgnass-domino)

Was this useful?