JSX can be more efficient by default

Andrea Giammarchi
6 min readOct 27, 2022

--

comparison of frameworks and libraries via https://github.com/krausest/js-framework-benchmark

This post is a follow up for the JSX is inefficient by default … but … one, describing a journey behind AST manipulation, brainstorming use cases and possible caveats, and finally releasing an experimental library that already compete with all others despite its infancy.

… in an ideal world …

I’ve already explained why JSX is inefficient in its current form, and the pattern I’ve used in udomsay helped me shaping what kind of perfect helping tool instead it could be:

const div = (
<div class="any" data-runtime={someValue}>
<p class="dynamic">{someContent}</p>
<p>Some other content</p>
</div>
);

With the better transform, above syntax is translated into:

const token1 = {};
const div = createElement(
'div',
{
__token: token1,
class: 'any',
'data-runtime': interpolation(someValue)
},
createElement(
'p',
{class: 'dynamic'},
interpolation(someContent)
),
createElement(
'p',
null,
'Some other content'
)
);

and that’s already a lot of callbacks, but the only difference from standard JSX is though:

  • it is possible to weakly relate once that outer div JSX template, compared to its internal nodes, thanks to that __token reference. The token is unique per outer template so it works with both component and inner components/interpolations
  • it is possible to distinguish between runtime properties or children, as opposite of needing any other way to do so, via the interpolation extra callback, which is not available in common JSX transformers

… with an intermediate static template …

The idea behind these details is that from the intermediate representation, based on all those calls, it is possible map the outer template into static HTML:

<div class="any">
<p class="dynamic"><!--🙊--></p>
<p>Some other content</p>
</div>

Contextually, it is also possible to map that template to a well known list of instructions to reach the node and operate on it:

const operations = [
// suggests an attribute operation on root
{child: [], props: ['data-runtime'], args: [1]},
// suggests an operation through the comment
{child: [0, 0], props: null, args: [2, 2]}
];

Allow me to explain what are those info about:

  • the child hints which node in the template should be handled. An empty array means the root node, but [0, 0] means root.childNodes[0].childNodes[0] which is the comment placeholder for the content to deal with once revealed.
  • the props hints, if not null, that something should happen at the attribute or, generally speaking, props level. A null value means skip, otherwise it means handle these values from the props object.
  • the args property hints where in the call stack is the props object or any comment that needs to be reached, so that [1] means the argument at index 1 of the top most (root) createElement call, while [2, 2] means the second argument at the second argument received … confused?
const createElement = (...args) => args;

With such simple function the logic has nothing to do but crawl arguments and find the position of each argument that needs special care, allowing us to reach any props as well as any ...children the component, or the callback, receives.

Putting these details all together we now have a template referenced by that token that can be cloned per each time that very same div is returned or needed, with a pre-made list of operations that suggests what to do and where in the stack, without needing to compute any of this in the future.

P.S. please note all these info is simplified for blog post sake, but hopefully the gist of the process is clear!

… and an almost instant logic to run …

As convoluted as it sounds, we have a JSX DSL, that results into an HTML & JS instructions IR, so that we can create a list of optimized updates to perform per each new invoke of the same content (effects, hooks, render, etc), aiming to further JIT those operation once revealed (rendered):

// inline demo/simplification of the logic
// interpolations are {value: any} like instances
const updates = [
// handle attributes
args => {
const {value}= args[1]['data-runtime'];
root.setAttribute('data-runtime', value);
},
// handle interpolations in the root
args => {
const comment = root.childNodes[0].childNodes[0];
const {value} = args[2][2];
const node = document.createTextNode(value);
comment.replaceWith(node);
// JIT optimize this update for future changes
updates[1] = args => {
const {value} = args[2][2];
node.textContent = value;
}
}
];

… and reveal on the DOM …

So we have a cached template content we can clone at any time, plus instructions to map nodes and attributes or comments to then perform an update that will optimize itself while executing at least once, also reducing on the heap retention of unnecessary references in the future, like the comment node itself, as example.

const templates = new WeakMap;
const render = (args, where) => {
// we know for sure args has a __token in props
const {__token} = args[1];
// we check for known tokens
let details = templates.get(__token);
// we parse once if unknown
if (!details)
templates.set(__token, details = parse(args));
const [content, operations] = details; // we create a new node
const node = content.cloneNode(true);
// we map updates through this node
const updates = createUpdates(node, operations);
// and we invoke each update with args
for (const update of updates)
update(args);
// show the result on the DOM
where.replaceChildren(node);
};

I know dozen questions might be rising around components, effects and whatsoever, but like I’ve said this is an extremely simplified logic of how I have implemented things but fear not, the library whole logic fits in about 3.5Kb brotli/minified, and includes, but also exports, all signals features!

udomsay

udomsay social image

This silly named library has already submitted its presence in the famous js-framework-benchmark repository, as it applies all the concepts described in this post and also it scores extremely well, being even faster than uhtml but definitively faster than preact or React with hooks, svelte, vue 3, or stencil.

Because of the template and operations related to it, udomsay also doesn’t strictly need any vDOM and, on top of that, it includes with its 3.5Kb footprint signals and a battle tested DOM differ, slightly tweaked in this case to obtain best possible performance and care less about persistent fragments, as these are not used, nor needed, with the current logic.

… and in case you are wondering: Yes, udomsay works with JSX fragments as well as components, and it’s a great starting point, or playground, to explore all these possible optimizations around a better hinted JSX.

I hope you’ll appreciate its different approach, and give it a try, or even help me moving it forward, making it production ready ❤️

… still room for improvements!

Because createElement simply returns arguments, when these are used within effects, and a single page render could be such effect, it makes little sense to retain all already parsed values in memory for future updates, as static parts are really never necessary more than once, or better, never necessary within a component once its content related to its token is parsed and revealed once.

This might result into an even smaller memory footprint, but right now it’s mostly aligned with uhtml and better than other JSX based libraries that don’t already translate such JSX into templates, like solid-js does.

There are also other areas where performance could be tuned further, but results are already so good that I’d rather try to explore the SSR possibilities this new kind of JSX transformer allows, so please keep following this space to read more around this topic 👋

--

--

Andrea Giammarchi
Andrea Giammarchi

Written by Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

Responses (3)