JSX is inefficient by default … but …

Andrea Giammarchi
8 min readSep 30, 2022

--

Photo by Javier Mazzeo on Unsplash

Follow up

Please don’t miss JSX can be more efficient by default post, which explain my journey to achieve awesome performance out of JSX!

Update

I’ve written a transformer that fulfill my needs/requirements, yet I am trying to find out how to make these requirements actually profitable in terms of performance. I always end up re-wiring what a template literal tag based solution would do, and that’s a show-stopper to me, as I am repeating what exists already without any perf improvement. Keep watching this space to know more about this issue and the possible solution I have 👋

With undeniable enhanced DX and its extreme popularity among frameworks and tools, the JSX specification has been on my radar for quite a while and this post goal is to analyze its pros and cons, also proposing an improved default parsing that fixes its inefficiency in a backward compatible fashion.

What is JSX?

I believe nobody reading this post would need an answer but, since we are here, JSX is a DSL to represent XML like views, with runtime interpolations, directly through JS, or any other environment.

<Component>
<tag static="property" dynamic={property}>
Static content
<inner-tag />
<>
Static fragment content
{'dynamic' || <content />}
</>
{'dynamic' || <content />}
</tag>
</Component>

If you are new to this topic, consider using the awesome Babel playground to counter-validate everything I am writing down 👍

What is JSX used for?

On the Web, it’s an alternative to template literals tags based libraries, or vice-versa, except it doesn’t usually need a plugin to highlight its content on our IDE of choice, plus it can be used and transformed for native platforms too that don’t really care about ECMAScript or the Web specs in general.

As a matter of fact, one of the best feature JSX offers these days, is its ability to transform itself into native platform directives or APIs too, while enforcing template literal tags standard and solution in those platforms would be pretty much pointless and practically slow (in terms of both adoption and execution).

What makes JSX inefficient by default

The way JSX is currently transformed by pretty most “transformers” out there, is that tons of information around its usage, or developer intent, is “lost in translation”!

Take the initial code snippet as example, and see it transformed as such:

React.createElement(
Component, // this is a static component
null,
React.createElement(
"tag", // this is a static tag
{
static: "property", // this is a static property
dynamic: property // this needs to be worked out
},
"Static content", // this is a static text node
React.createElement(
"inner-tag", // this is a static element
null
),
React.createElement(
React.Fragment, // this is a static fragment
null,
"Static fragment content", // this is a static text node
'dynamic' || React.createElement("content", null)
// this is the only dynamic part of the fragment
),
'dynamic' || React.createElement("content", null)
// this is the only dynamic part of the component
)
)

To understand why it’s inefficient by default, let’s have a look at the React.createElement signature:

function createElement(kind, props, ...children) {}

It is true that this signature covers all possible XML-ish tree related cases, but it’s also true that there is no way to understand:

  • was that component static and well known at parsing time?
  • were any of its children static and well known at parsing time?
  • are properties all dynamic with a need to diff their value in the future?
  • is that child something to deal with in the future?

All answers are currently a “maybe”, with all default transformers I could try, and this is a bummer!

How do template literal tags easily win here?

// this whole template gets a unique, memory friendly, identifier
// as the template reference passed as html(template, ...values)
// note: only interpolations are within values!
html`
<Component>
<tag static="property" dynamic=${property}>
Static content
<inner-tag />
<>
Static fragment content
${'dynamic' || html`<content />`}
</>
${'dynamic' || html`<content />`}
</tag>
</Component>
`;

Template literal tags are “unique per code parsing”, meaning that even if a function has some tag in it, their reference will be preserved over time:

const templates = new WeakSet;
const tag = (template, ...values) => {
if (!templates.has(template)) {
console.log('new template');
templates.add(template);
}
return values;
};
function Component(value) {
return tag`<div>whatever ${value}</div>`;
}
Component(1);
// logs: "new template"
// logs: Array [ 1 ]
Component(2);
// logs: Array [ 2 ]
Component(3);
// logs: Array [ 3 ]

In short, each template literal tag is “unique (until GC) forever” in any scope, which is what makes uhtml, lit or other libraries, able to exist and perform really well out of the box!

… but …

How do template literal tags easily lose here?

In a tweet:

i explored built in extends so consumers can only bind my logic to certain element types but what if i want to change the element tomorrow? i can do that with a component without any impact to consumer. i can’t do that with built ins. a component encapsulates all these things — jenna

The componentizing part of the equation is indeed fully lost in template literal tags based solutions, because templates are just strings, hence unaware of the surrounding scope, and non-translated as such.

html`<Component />`

has no special meaning in the ECMAScript standard, so it’s impossible to grant, without tooling doing weird things around, unable to get developer intent from shenanigans, the very same DX React, or JSX users in general, expect.

Sure thing, template literals can pin-point every single interpolation in the template that needs extra attention on future, reactive, updates, but this feature alone is not super compelling for the current JSX’ based industry.

Who cares anyway …

Well, as the one that pioneered template literal tags based solutions before anyone else (yes, hyperHTML existed months before lit even got published), and as the one that proposed alternatives to these issues, I do care about “de-facto standards” and their adoption, so that blending all features together has been a personal research-field I am finally confident to share, so please bear with me … I’m getting there!

A better JSX transform

What if JSX could have hints around its well defined interpolations?

This is a topic I brought up at Babel level, but I feel like it should reach a wider audience, React team to start with.

Let’s see an example of an already improved transform:

React.createElement(
Component,
null,
React.createElement(
"tag",
{
static: "property",
dynamic: React.interpolation(property)
// this is the only changing property at runtime
},
"Static content",
React.createElement(
"inner-tag",
null
),
React.createElement(
React.Fragment,
null,
"Static fragment content",
React.interpolation(
'dynamic' ||
React.createElement("content", null))
// this is the only fragment's part that needs updates
),
React.interpolation(
'dynamic' ||
React.createElement("content", null))
// this is the only component's part that needs updates
)
)

Not only I’ve already started a conversation through a Babel issue, this optional pragma around interpolations could already speed-up tons of unnecessary work done by inevitably vDOM based solutions, as there’s finally an explicit hint on what’s dynamic and what’s static out there.

If you’re wondering how the opt-in interpolation helper would look like, this is my current best guest:

// basic interpolation
class Interpolation {
constructor(_) {
this._ = _;
}
valueOf() {
return this._;
}
}
React.interpolation = value => new Interpolation(value);

That would allow, especially at the props level, to distinguish between props that need to be further processed, and props that don’t (static).

To understand if a prop is static or not, typeof props[key] would be enough, as string is the resulting type for static props, while object is the one for dynamic props, where prop.valueOf() would always return the initial property with these, backward compatible too.

But what about {...props} ?

Well, for those we need a better transform:

// ...props transform
/*#__PURE__*/ function _extends(target) {
for(var i = 1; i < arguments.length; i++){
var source = arguments[i];
for(var key in source){
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = new Interpolation(source[key]); // here!
}
}
}
return target;
}

That would flag any property as dynamic, because that’s what ...props does behind the scene, so that any library could decide how to deal with that information 👍

But what about ...children ?

Once again, the typeof child being string would hint for static content, while child.valueOf() would bring in the actual content as either interpolation or well known component, where the difference, still one-off parsing, would be revealed by the fast and cheap child instanceof Interpolation check.

Please keep in mind these are all one-off checks to perform at bootstrap time of any component, as the following updates would be mapped and as quick, and cheap, as possible.

Using a template literal tag too

So far my proposal addresses static VS runtime content, but it doesn’t address the fact every JSX template is also static and well known upfront, but there’s no need to not treat it as such:

// by using a tag we can confine stack operations independently
// through the React.createElement(template) reference.
// this can return directly once a createElement.bind(template)
React.createElement``( // makes the outer component static and unique
Component,
null,
React.createElement(
"tag",
{
static: "property",
dynamic: React.interpolation(property)
},
"Static content",
React.createElement(
"inner-tag",
null
),
React.createElement(
React.Fragment,
null,
"Static fragment content",
React.interpolation(
'dynamic' ||
React.createElement``("content", null)
// makes the inner component static and unique
)
// still the only part that needs to be worked out
),
React.interpolation(
'dynamic' ||
React.createElement``("content", null)
// makes the inner component static and unique
)
// still the only part that needs to be worked out
)
)

Now, because we’re using the same utility either as regular, old fashion, method, or a template literal tags based, we might wonder how would that helper looks like in the future?

// use a single WeakMap per each statically known JSX outer template
const templates = new WeakMap;
React.createElement = function createElement(template) {
// called as template tag (JSX backward compatible)
if (arguments.length === 1) {
let bound = templates.get(template);
// bind the callback once if it's unknown
if (!bound) {
bound = createElement.bind(template);
templates.set(template, bound);
}
// return a bound reference that's always the same
return bound;
}
// allow creating a well known stack for the current template
if (templates.has(this)) {
// do any smart parsing using the template context as reference
// create a stack from scratch, update the previous one, etc
return runtimeElement.apply(this, arguments);
}
// here a regular create element case, meaning it's static content
return staticElement.apply(null, arguments);
};

Backward compatible by default

This proposal would allow migration between previous default JSX transform, to the future one, by opting in via pragma definition the template literal tag version, or having a pragmaInterpolation in the mix that, if enabled, uses React.interpolation through the class, instead of beig a no-op.

As Summary

This post shows deficiencies around JSX but it also proposes a new way JSX could be used not only by transformers, but also efficiently at runtime after default transformation.

Last, but not least, some of you might have thought: “but why not using Solid-JS transformer instead, as it’s better?

The whole point of this post is not to be better than JSX as single library/framework transformer, is to make the default transformer better and more competitive with everything else out there that might do this or that to fix JSX issues and/or performance 👋

--

--

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.

No responses yet