dom-expressions: a rendering runtime for libraries without a virtual DOM
A Fine-Grained Runtime for Performant DOM Rendering
At a glance
- What is it?
- DOM Expressions is the machinery under Solid, MobX-JSX, Vuex-JSX and Knockout-JSX: a small runtime that renders plain DOM expressions for reactive libraries built on signals and observables rather than on lifecycle functions and a virtual DOM. Most developers will never import it directly, and the README says so, which is the first thing to understand about the project.
- Who is it for?
- Adopt dom-expressions only if you are building a reactive library or a framework yourself, because the README states that most developers will not use this package directly and that it is meant to help authors write their own reactive libraries. Do not adopt it as a component library or as a replacement for React in an application, since the package ships a runtime and not an application model.
- Can I use it commercially?
- Yes. MIT 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 16 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The layer under Solid and MobX-JSX
DOM Expressions is described as a Rendering Runtime for reactive libraries that do fine grained change detection. Those libraries rely on concepts like Observables and Signals rather than Lifecycle functions and the Virtual DOM.
The reason that matters is what a compiler has to do. In a virtual DOM framework, an update is a diff against a previous tree, and the unit of work is a component. In a fine-grained system, the unit of work is a single expression, and the renderer has to be able to evaluate that expression on its own and write the result straight into the DOM without touching anything else.
Standard JSX transformers are not helpful here, and the README says why: those libraries need to evaluate their expressions in isolation to avoid re-rendering unnecessary parts of the DOM. A transform that produces component calls is the wrong shape for that job.
The package therefore provides a small library to render pure DOM expressions, and it can drive libraries like KnockoutJS or MobX independently of their current render systems. The performance claim attached to that design is that the approach has been proven to be incredibly fast, dominating the highest rankings in the JS Framework Benchmark, which is a claim worth taking with the usual caution but which is at least checkable against a public benchmark.
The example implementations are the map of the ecosystem. Solid, a declarative JavaScript library for building user interfaces, is built on it. mobx-jsx asks how much more performant MobX is without React. vuerx-jsx does the same for Vue with a renderer built on Vue's own reactivity. ko-jsx is Knockout with JSX rendering. And s-jsx is a testbed for trying new techniques in the fine-grained space.
The package tells you not to use it directly
The README is unusually direct about its audience. It is designed to be used with a companion render API, and the render APIs available are a JSX Babel Plugin, Tagged Template Literals, and HyperScript runtime APIs. Then comes the sentence that should stop most readers: most developers will not use this package directly, it is intended to help author your own Reactive Libraries and not to be used directly in projects.
That is worth taking seriously, because the failure mode of a low-level runtime library is being adopted directly by an application team. dom-expressions is not a component library, it has no notion of a component lifecycle, no router, no data fetching layer and no opinion about state. An application built directly on it would be writing a framework, which is exactly the work the package exists to make unnecessary.
The project also describes itself as a work in progress, with the goal being to better understand and generalise the approach to provide non Virtual DOM alternatives to developing web applications. That framing has consequences for stability. The runtime's internals are the contract between a reactive core and the renderer, and the author is candid that the shape is still being explored.
The release tags in the repository stop at v0.23.0 in November 2020, while the build manifest declares version 0.36.0, so the npm package version and the GitHub tags have diverged, and the npm package is the one an application would actually install.
Generating a runtime that a bundler can shake
The runtime generator section explains the mechanism, and it is a neat piece of engineering.
DOM Expressions is designed to let you create a runtime that is tree shakeable. It does that by using babel-plugin-transform-rename-import to rename the import to your reactive core file, so the generic runtime code is rewritten to point at your own reactivity implementation and the unused parts of the generic code become unreachable and can be dropped by the bundler. The README warns to set the babel plugin up and then export from the runtime, and to be sure not to exclude the dom-expressions node_module.
The configuration is small:
{
plugins: [
[
"babel-plugin-transform-rename-import",
{
original: "rxcore",
replacement: "../src/core"
}
]
];
}The reactive core file is the part you write, and the README says it exports an object with the methods the runtime requires. The example is built on s-js:
import S, { root, value, sample } from "s-js";
const currentContext = null;
function memo(fn, equal) {
if (typeof fn !== "function") return fn;
if (!equal) return S(fn);
const s = value(sample(fn));
S(() => s(fn()));
return s;
}
function createComponent(Comp, props) {
return sample(() => Comp(props));
}
export { root, S as effect, memo, createComponent, currentContext };The interface is small on purpose. A library that wants to be wrapped by this runtime has to expose an effect primitive, a memo with a custom equality function, a component creator and a root, and that is the entire surface a reactive core has to implement to work with the renderer.
Three renderers and the trade-off between each
Once a runtime is generated, it is used through one of three companion render APIs, and the README ranks them.
JSX is by far the best way to use the library, through the Babel Plugin JSX DOM Expressions. The reason given is pre-compilation, which lends itself to the best performance since the whole template can be analysed and optimally compiled into the most performant JavaScript. The result is not only the most performant code but the cleanest and the smallest.
Tagged Template Literals are the next best thing, for when pre-compilation is not an option. Lit DOM Expressions provides a similar experience to the JSX, compiling templates at runtime into similar code on first run. The cost is stated plainly: this option is the largest in size and memory usage, although it keeps most of the performance and the syntax from the JSX version.
HyperScript is the third. It is not as performant as the other options, but the library provides a mechanism to expose a HyperScript version, which offers the greatest flexibility when working with existing tooling for HyperScript and enables pure JavaScript DSLs.
The ordering is a genuine engineering trade-off rather than a ranking of quality. Pre-compilation buys speed and size at the cost of a build step, runtime compilation buys convenience at the cost of memory, and HyperScript buys flexibility at the cost of performance. A library author picks the one that fits their users.
The escaping rule, and where it does not apply
The XSS section is short and unusually useful, and it is the part of the README that an application team should read even if they never touch the runtime directly.
dom-expressions automatically escapes inserts and attributes in HTML. There are two exceptions, and the README names both.
The first is the innerHTML property, which bypasses the escaping entirely. The second is noscript, which is outside the purview of the library because those tags and their contents are evaluated even without JavaScript. The advice for that case is to sanitise any strings in attributes, especially when inside noscript tags.
The rule of thumb the README gives is threefold. Avoid injecting HTML into your page as much as possible. Make sure the contents of noscript are properly sanitised. And add a strict Content Security Policy to your application.
That last point is what makes the first two matter. If the library escapes what it controls, and a Content Security Policy restricts what a page may load or execute, then the remaining exposure is the code that deliberately injects HTML, and the policy is the backstop for the cases the library cannot see.
The repository carries a SECURITY.md alongside the code, which is the right place for the disclosure process.
Where dom-expressions is the wrong choice
The first wrong-tool case is an application that wants a UI framework. If you need components, routing, state management, forms and a build pipeline, you want a framework, and Solid is built on this package. Reaching for dom-expressions directly means writing the layer that Solid already provides, in a runtime the README describes as a work in progress.
The second is a project that has standardised on a virtual DOM. Fine-grained rendering wins on benchmarks in a controlled setting, and the benchmark is the reason the approach is interesting, but the trade is a different programming model. The model is signals and observables, and the cost of teaching a team that model is real. A team whose React, Vue or Svelte code is working is not served by a rendering runtime.
The third is a project that needs a stable dependency with a support commitment. The visible releases stop in 2020, the manifest version and the tags have diverged, and the author's stated goal is to generalise the approach. The library is used in production by several projects, so it is not abandoned, but the framing is research-adjacent and the internals can move.
There is a fourth consideration if you do build on it. The runtime expects a specific reactive core interface, and the cost of writing that core, and of keeping it aligned with a runtime that is still changing, is the real price of entry.
MIT, a lerna monorepo and a package built for bundlers
The project is MIT licensed, with the LICENSE file at the root, and the default branch is main, the repository is not archived, and the last push was on 2026-10-07.
The build layout is a pnpm workspace with lerna on top. The root package is private and named dom-expressions-build at version 0.36.0, with pnpm 11.1.1 as the package manager. The scripts show the shape of the release process: the build and test targets run through lerna with a concurrency of one and stream the output, the coverage targets run in parallel, and publish:release builds the packages and then publishes them through lerna. There is an init script that installs and bootstraps the workspace, and a jsx-sync-types script that runs a script inside the dom-expressions package to keep the JSX types in step.
The devDependencies are the toolchain a library like this needs: Babel with the preset-env and preset-typescript plugins, Rollup with the babel, commonjs, json and node-resolve plugins, Jest with jsdom, babel-plugin-tester for testing the Babel transforms, the rename-import plugin that the runtime generator depends on, coveralls for coverage reporting, lerna itself and rimraf.
The packages/ directory holds the published pieces, the Babel plugin for JSX, the tagged template renderer, the HyperScript renderer and the core dom-expressions package. The last push in October 2026 on a project whose visible release history stops in 2020 is a reminder to read the npm version rather than the tags, and to expect the internals to move.
Editorial conclusion
Adopt dom-expressions only if you are building a reactive library or a framework yourself, because the README states that most developers will not use this package directly and that it is meant to help authors write their own reactive libraries. Do not adopt it as a component library or as a replacement for React in an application, since the package ships a runtime and not an application model. Before you build on it, read the XSS section, because the innerHTML and noscript exceptions are real and the library states them plainly, and check the package version on npm rather than the release tags, since the visible GitHub releases stop at v0.23.0 while the manifest declares 0.36.0.
Frequently asked questions
What is DOM Expressions used for?
It is a rendering runtime for reactive libraries that do fine grained change detection, built on Observables and Signals rather than lifecycle functions and the virtual DOM. It renders pure DOM expressions so that expressions can be evaluated in isolation, avoiding re-rendering of unnecessary parts of the DOM.
Should I use dom-expressions directly in an application?
The README says most developers will not use this package directly and that it is intended to help authors write their own reactive libraries. If you want a UI framework, use Solid, which is built on it, or another framework that matches your team's model.
How do you generate a runtime for dom-expressions?
You configure babel-plugin-transform-rename-import to rename the import to your reactive core file, then export from dom-expressions/src/runtime in your runtime, and do not exclude the dom-expressions node module. Your reactive core exports the methods the runtime needs, such as an effect, a memo with custom equality, a component creator and a root.
Which render APIs does dom-expressions provide?
Three. JSX through the Babel Plugin JSX DOM Expressions, which pre-compiles the whole template and is the most performant and smallest. Tagged Template Literals through Lit DOM Expressions, which compile at runtime on first run and keep most of the performance but are the largest in size and memory. And HyperScript through Hyper DOM Expressions, the least performant but the most flexible, working with existing HyperScript tooling and enabling pure JavaScript DSLs.
How does dom-expressions handle XSS?
It automatically escapes inserts and attributes in HTML. The exceptions are HTML inserted through the innerHTML property, which bypasses the escaping, and noscript tags, whose contents are evaluated even without JavaScript. The README advises sanitising strings in attributes, especially inside noscript, avoiding HTML injection, and adding a strict Content Security Policy.
Official sources
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.
[](https://hysenlabs.com/projects/ryansolid-dom-expressions)Was this useful?