Closed bartlomieju closed 1 year ago
I think the overall JSX transform is now ready to be explored in the real apps. Only thing left is adding more attribute name serialization mappings, and probably some rust stuff as I just hacked to get it to work. Other than that I think I accounted for all use cases I could come up with. I think the transform is ready.
This PR introduces a new JSX transform that is optimized for server-side rendering. It works by serializing the static parts of a JSX template into static string arrays at compile time. Common JSX templates render around 200-500 nodes in total and the transform in this PR changes the problem from an object allocation to string concatenation one. The latter is around 7-20x faster depending on the amount of static bits inside a template. For the dynamic parts, this transform falls back to the
automatic
runtime one internally.Quick JSX history
Traditionally, JSX leaned quite heavily on the Garbage Collector because of its heritage as a templating engine for in-browser rendering.
Every element expands to two object allocations, a number which grows very quickly the more elements you have.
The later introduced
automatic
runtime transform, didn't change that fact and merely brought auto importing the JSX factory function, some special handling of thekey
prop to the table and puttingchildren
directly into the props object. The main goal of that transform was to improve the developer experience and avoid an internal deopt that most frameworks ran into by having to copy thechildren
argument around....still expands to many objects:
Both transforms are quite allocation heavy and cause a lot of GC pressure. JSX element is at minimum converted into two object allocations and for many frameworks even a third one if they are backed by fiber nodes. This easily leads to +3000 allocations per request.
Precompiling JSX at transpile time
The proposed transform in this PR moves all that work to transpilation time and pre-serializes all the static bits of the JSX template.
The
jsxssr
function can be thought of as a tagged template literal. It has very similar semantics around concatenating an array of static strings with dynamic content. To note here is that theclassName
has been automatically transformed toclass
, same fortabIndex
->tabindex
. Instead of creating thousands of short lived objects, thejsxssr
function essentially just callsArray.join()
conceptually. Only the output string is allocation, which makes this very fast.Benchmarks
I've put this transform through the tests in a benchmark. The first is the
automatic
transform with Preact, second is the transform from this PR rendered by Preact and third is a custom HTML transform that skips component instances entirely. The latter is obviously the fastest, as component instances are quite heavy too, and currently those need to be instantiated for backwards compatibility reasons in Preact.Usefulness for the ecosystem
The transform in this PR was intentionally designed in a way that makes it independent of any framework. It's not tied to Preact or Fresh at all. Rather, by setting a custom
jsxImportSource
config indeno.json
you can point it to your own factory functions. Each factory function needs to implement the following functions:jsx(type: Function, props: Record<string, unknown>, key?: unknown)
theautomatic
runtime jsx factory for dynamic elements or components.jsxssr(tpl: string[], ...dynamicParts: unknown[])
the main templating function which merges the static bits and with the dynamic bitsjsxattr(name: string, value: unknown)
used to serialize dynamic attributes when an element can be serialized. By using a function call frameworks can also return an empty string for event listeners for example and avoid them being serializedNote: That custom implementations don't have to adhere to specific return values. The transform only expects a certain signature, so if you can also use it for frameworks which don't need to materialize component instances.
If you're interested in more of the transpilation output, checkout the tests.