Preview: This page is a work in progress and may not reflect the final content.

Why Butterfloat v2?

I think it is worth documenting the motivation behind compatibility breaks, because it helps to know that such things were at least thoughtful and well-considered.

I've been taking compatibility seriously in Butterfloat and I hope is shows. I was very proud to release as complex feature as Stamps in Butterfloat v1.5 without breaking exterior API compatibility, and even keeping its additional overhead mostly opt-in.

There were two big trains of thought that smashed into each other, leading into the v2 rewrite: Stamp-first/Stamp-"native" design and Contextual JSX. The combined potential for performance gains and new features seemed a strong reason to support a v2 rewrite in ways that one train or the other alone might not have suggested.

Stamp-first

There has been a lot learned since Stamps have been added to Butterfloat. In the time since Stamps launched in v1.5, I think they've become somewhat indispensable. They help performance in noticeable and subtle ways. I find they are generally worth the time to build in most projects.

In v1.5, Stamps use a completely different DOM build function than normal runtime. This was a concession for the most backwards compatibility, and while it increased duplicate code, the Stamp builder is usually a compile-time tool or server-side rendering tool, so not as much an efficiency issue in runtime performance in most v1.5 Stamps use-cases.

More interestingly/worrying, there were subtle changes to the way that Stamps build DOM elements that have somewhat diverged from how the runtime DOM builder builds them. Some of those have been improvements, but it was too much of a backwards compatibility issues to apply them to the existing runtime builder. In part because different DOM output could be seen as a compatibility break.

On the features backlog for the v1.5 version of Stamps, that would be hard to provide in the v1.5 runtime have been requests for crossing component boundaries ("deep" stamps) as a path to larger server-side rendering and static site generation gains, and "just-in-time" stamping to build Stamps at runtime if a component becomes frequently used as a path to provide some of the performance benefits of Stamps even without using SSR/SSG approaches/compile-time tools.

Contextual JSX

A core motivation of Butterfloat has always been that Butterfloat is not a Virtual DOM. I want to provide some of the benefits of a Virtual DOM like the possibility to test Components without the need for a DOM or DOM-emulation library. But I also want to provide a different performance story than a Virtual DOM has.

I had started thinking that the NodeDescription objects of v1.0 were somewhat getting in the way of some low level performance optimizations. A benefit to a TSX compile is that it compile-time unrolls the recursion into a tree as nested function calls that get called one at a time without recursion. There would be some benefits to building DOM elements directly during JSX calls, rather than the intermediate NodeDescription objects.

I was lamenting that JSX doesn't have the equivalent of "tagged templates" to easily change which "tag" function is used in different Components in the same file. I was reminded that the "react" compile of TSX relies solely on jsx functions already in scope (with no auto-import). Butterfloat was already recommending that approach to TSX compilation.

At that reminder, I nerdsniped myself into exploring scope options and realized that Butterfloat already had a good, working mechanism that could contextually inject different jsx functions depending on exact runtime needs: the ComponentContext<T> could be expanded to be a jsx injection tool.

With context-specific jsx functions, DOM elements could be eagerly built when asked to do so. The NodeDescription could remain for test contexts when DOM testing isn't desirable. Bindings could be eagerly collected with or without building DOM elements or NodeDescription objects.

I also realized that Contextual JSX could also help improve building trees or collecting bindings for full trees of components all at once, with fewer concerns for recursion limits. Though I also realized that would require additional investment into Completion Boundaries beyond the implicit model in the Component runtime since v1.0.