JSX is inefficient by default … but …

Photo by Javier Mazzeo on Unsplash

Follow up

Update

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?

<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?

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

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?

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 …

A better JSX transform

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

// 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

As Summary

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 👋

--

--

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Andrea Giammarchi

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