krausest/js-framework-benchmark

Question: Should we be setting any ground guidelines for what a Data Driven implementation is?

ryansolid opened this issue · 49 comments

I know this sort of thing has come up before (#230). But that's not my intention. When I first submitted Solid I did several things unknowingly mimicking the vanillajs implementation not realizing it was generally seen as bad form. They might not be the type of considerations a VDOM library would consider, but in the range of data-driven libraries perhaps there is something here that should be said.

Roughly I'm thinking:

  1. A data driven library should provide at minimum:

    • mechanism to update data
    • automatically/manually trigger DOM updates
    • provide a mechanism or abstract complexity of reconciliation of basic template control flow (conditionals, loops)
  2. An implementation should:

    • At any at rest view state should be re-constructable from data only. (Ie. any atom of state cannot exist solely in DOM nodes at rest)
    • Treat DIV.main as the mount/entry point. All DOM interactions within should be bound as part of the library's mechanisms.
    • Not explicitly exploit Chrome's unclear performance boost using requestAnimationFrame to clear rows.

Is calling this out unreasonable? Is this too restrictive or specific?

Similarly is it worth calling out reference implementations like VanillaJS, WC, etc.. since including them has value and they shouldn't be held to the same standards.

Yeah, this is a fair point. For example DOMc has html hard coded in index.html

https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/domc/index.html

So it does not need to create the DOM which is major slow down in JS API. This makes comparison difficult.

there are a lot of libs that are starting to get into questionable territory lately.

even for ivi, where @localvoid has historically held the philosophy of a straightforward, obvious implementation without "tricks" has drifted into using custom delegation logic and utilizing textContent pulled from the dom to improve ivi's numbers. :/

fundamentally it's still "data driven" but certainly not without "tricks".

https://github.com/krausest/js-framework-benchmark/blob/master/frameworks/keyed/ivi/src/main.js#L86-L97

I'm totally fine to move reactive view into JS for the sake of consistency.

I just can't understand blaming with the words "hard coded" and "does not need to create the DOM".
@Havunen , can you provide some metrics that proves your point that inlined HTML provides any performance benefit for domc.
I'm sure you, ParseHTML page stage by no means can be faster than JS API. You can clearly see that by comparing domc and stage0, where latest does inline view into template element and then attach it to the dom. Surplus also proves that by recreating the view from scratch using just JS.
And domc doesn't show any exceptionable startup metrics that could be the result of so-called "trick".

domc was initially designed to work like VueJS, that has ability to reactivate the view defined in html. And it's not shortpath.

Talking about event handling in top's: Yes, I also feel strange about working with the DOM, especially after long discussion about how 'Selected Row' should not be implemented switching classNames on target elements.

can you provide some metrics that proves your point that inlined HTML provides any performance benefit for domc.

I think the benefits of having html on the page (or SSR) is in metrics that are not currently captured by this benchmark:

  • First Meaningful Paint
  • First Contentful Paint

since html parsing is done progressively (streamed as it's received over the network) it will outperform anything you do in JS (which must be fully parsed, compiled and executed before any painting can happen).

EDIT: the amount of html that's pre-rendered by domc is pretty tiny and the real-world effect may not be noticeable. but if you pre-render 1,000 dom elements, then the benefits become very obvious.

Thanks, @leeoniya ,
Yeah, that could be reasonable.
I just ran Lighthouse and got following results:

  • domc(inlined HTML) - FMP/FCP - 1.9s
  • stage0(parsing from JS) - FMP/FCP - 1.9s
  • Surplus(classic DOM API) - FMP/FCP - 2.0s

stage0 performs same as inlined code. Therefore, I can't say that inlined html is always faster.
I hardly believe that loading prerendered page with 10k rows could be much faster than generating it from JS.

Though I'm fine to move view into JS, I still see no signs that it provides any meaningful benefit over other solutions to be able to say that it's making comparison difficult. I'm not stating that SSR provides zero-benefit, but looks like those benefits are hard to argue.
I think all parse-through-template solutions will show no difference in performance. At the end of the day, it's still parsing, no matter how it's done.

stage0 performs same as inlined code.

for a tiny dom, sure.

I hardly believe that loading prerendered page with 10k rows could be much faster than generating it from JS.

you should try it, especially on a throttled connection and cpu ;)

I'm not stating that SSR provides zero-benefit, but looks like those benefits are hard to argue.

there's quite a lot more that can be done during stream-parsing of html. for instance, images and fonts can start to be loaded in parallel, etc.

Yes, I just built 1k prerendered domc page(without JS) and stage0 that runs 1k run on load.
Here are results from Lighthouse with "Fast 3G, 4x CPU slowdown" settings.

  • SSR - FCP/FMP - 3.3s/3.4s
  • stage0(from JS) - FCP/FMP - 1.9s/2.0s

I got literally same results with "No throttling" settings. And that's because benchmark server doesn't perform gzipping, therefore placing any SSR solutions in bad position.
Again, as I said, it's very hard to argue about SSR performance benefits, as it requires very good tuned environment with server setup, that is not the case in this benchmark nor to say that it does something dirty and makes comparison difficult. And domc doesn't perform any kind of SSR, instead of 'reactivating' the view.

Edit: Actually, I just figured out that Lighthouse stated throttling in Runtime settings, despite that 'No throttling' setting. That's why I have same results.

Edit: I was able to run benchmark's http-server with -g flag to serve gzipped content and perform page gzipping using standard Mac gzip tool, making the page size down from 294Kb to 2.2Kb
And tests with throttling showing:

  • SSR - FCP/FMP - 1.9s/1.9s

Making it still hardly noticeable from performance perspective.
And all that without view rehydration, that will add additional cost as JS probably should perform dom walking and setting up environment for reactivity.
Maybe we just discovered dirty SSR truth :)

when i have some time, i'll try to set up a proper test on my linode/nginx server (http2) with domc/1k and 1k pre-rendered static html. then run both variants through https://www.webpagetest.org/ with these settings:

setup

@leeoniya

even for ivi, where @localvoid has historically held the philosophy of a straightforward, obvious implementation without "tricks" has drifted into using custom delegation logic and utilizing textContent pulled from the dom to improve ivi's numbers. :/

I just gave up convincing everyone to remove all this BS[1][2] from their implementations. I've tried :)

  1. surplus
  2. solid

I made the comment about the entry point in respect to domc 0.0.2 implementation since it didn't use template parsing to bind top level event handlers. The current implementation does not have that issue. I actually don't think the way domc currently works as a particular advantage. Maybe in terms of lines of code or memory, but it still has to parse over the template. The per item template gets rendered then removed to be cloned for each iteration. Depending on the implementation this can be more visible and expensive especially on large complex pages. There is a big difference between this and SSR which pre-renders. It's actually usually a challenge for libraries to do this sort of parsing to do SSR since they expect the child render context to be the template and once rendered the templating information is generally lost. That being said SSR has big benefits for first paint, and depending on the diff mechanism clientside hydration can be more performant than the type of parsing/reconstruction domc is doing.

But this is exactly the sort of discussion I was looking for. I do feel that a number of people consider something like React the baseline here. Even React and Angular sort of share some commonality in that React based it's original viability off the fact that Angular's digest loop wasn't really any more efficient than redrawing everything (in VDOM) and diffing. They both were fundamentally large central loop driven and top down.

Not sure if everyone is familiar with KnockoutJS but it was an early pioneer for data driven client side (c. 2010). It introduced the concept of ViewModel which differed from the controller based MVC of the time in that VM's are instances much like Components. It was the first "Just a Render" library with MV* in mind and didn't concern itself with Model/Store implementation or even how you sliced up your ViewModels (Components). It has custom directives, an extension system, and disposal hooks. It does the same real dom parsing domc does. It's core is event driven rather than scheduled.

In many ways it was way ahead of its time, and solved a lot of the same issues that React "revolutionized" nearly 4 years later. Vue's early design, Riot.js and a number of other libraries were inspired by KO. Over time there wasn't much innovation in this approach and times moved on. But it is a perfectly valid approach to data driven implementation. I submit this as another baseline for expectation of data driven library.

I honestly don't see what the whole concern is with event delegation. Here's a whole official documentation page from Knockout.js on event delegation. To take this further having had to hack this for React, most libraries that auto do event delegation are broken by the Shadow DOM. With Solid I actually did attempt an automatic syntax(precompiled) and directive mechanism for this before realizing not only was it more convoluted, but it was an unnatural fit. I looked at Inferno and a couple other libraries to only come to the conclusion it was an example where the solution was worse than the problem.

I believe in real applications people dont host their event listeners outside the component either so why here? In my opinion that is a task for the library to handle and then keep the application implementation simple.

Also in early days this benchmark had React version where was one table and then each row was a component. Now the component is no longer there in many benchmarks. I understand that all libraries here dont have components in them, but why do we keep components in React then? Somehow I feel like the improvements and optimizations should be done in libraries and not in the app layer.

What about correctness vs the speed? We could implement basic swaps / test cases before the test starts just to see it works as expected, we could port those from React/Inferno/Ivi repo. Also what about errors? Previously Inferno did not have any fallback behavior If application encountered ja exception in the middle of render. All your future renders would be stuck and application jammed for that vNode tree.

If application encountered ja exception in the middle of render. All your future renders would be stuck and application jammed for that vNode tree.

Bugs aren't recoverable errors, there shouldn't be any future renders.

@localvoid
Imagine following:
You render vNode tree which contains a button that fails. Ok. Then your end user goes away from that page and you render another vNode tree on top of that, InfernoJS will handle it and not fail here. But this comes with cost of marking all vNodes during keyed, non keyd algorithms

See code:
https://github.com/infernojs/inferno/blob/master/packages/inferno/src/DOM/patching.ts#L477

Also I started wondering how do these libraries handle vNode hoisting, this is also a cost that Inferno and all inferno like libraries should do. Imagine you hoist a vNode and add it to multiple places in your application, in InfernoJS we need to clone the vNode or application might fail to runtime error. This is also done for all vNodes. Lately it was optimized to use bit for detection but because JS its still semi expensive.

https://github.com/infernojs/inferno/blob/master/packages/inferno/src/DOM/patching.ts#L472-L474

@Havunen If you catch unhandled exceptions(bugs) and try to automatically recover from it, it means that you've traded correctness/security/etc for DX. It is a visual basic all over again.

No, it does not catch any errors it marks vNodes which have been succesfully patched, so next render will correctly continue. If your library compares lastVNode to nextVNode and nextVNode becomes lastVNode at the end of render, then if your app crashes mid way of diff process. lastVNode will not be correct anymore and results into strange behavior. Unless there is internal vNode tree similar to React fibers this needs to be done.

I think react does this too to internal VNode tree

if your app crashes mid way of diff process.

Then there is a possibility that some external state(application state) is corrupted and since everything lives in a shared memory, it is impossible to safely recover. Just because you've restored state for a virtual dom tree after you've encountered a bug doesn't mean that it will continue to work correctly. The only correct way is to crash an application.

Yes, however user experience for end user would suffer

I can create JSFiddle later to demonstrate it better.

Agree, but with "auto-recovery" it maybe even worse (security issues, unexpected behavior). It is a trade-off, not something that everyone should implement.

Well I would not call it recovery, it just keeps vNode tree in sync with real DOM tree.

Another questionable area is Component root node swapping and vNode tree hoisting, many libraries copied patch / mount logic from InfernoJS but did not include logic of cloning vNodes with them, sure it looks like "wtf is this" because its everywhere, but isn't it expected behavior for a library to handle?

Another questionable area is Component root node swapping and vNode tree hoisting, many libraries copied patch / mount logic from InfernoJS but did not include logic of cloning vNodes with them, sure it looks like "wtf is this" because its everywhere, but isn't it expected behavior for a library to handle?

Not so sure how other libraries are handling root node swapping, I think that almost all vdom libraries are handling this use case.

vNode tree hoisting maybe not so important as it seems, Vue is doing fine without it, just another quirk that developers should be aware, like understanding the behavior of a children reconciliation algo.

most libraries have specific useful features that add overhead, cannot be dead-code-eliminated, and do nothing useful in this benchmark. it's impossible to argue for unused feature parity in the context of perf.

there's good reason why the impl requirements are rather loose. there's no need to enforce that each row is a component, because it adds overhead in some libs without any concrete benefit here. in react, the only way to short-circuit vdom regeneration is via shouldComponentUpdate, so row-components are a necessary optimization in that case, not simply a structural choice.

i've mentioned this before, but imo library-supported optimizations/constructs is where the line should be drawn. it's easy to see when the ergonomics of each impl take a steep nose-dive into departure from initial uniformity and devolve into userland hacks like reading the row id back from textContent, wonders like firstChild.nextSibling.lastChild.previousSibling in userland, or using classnames to discern the origin of a delegated event. we all know how to do this, we've all used jquery. the only thing keeping some from going that route is being honest with the shortcomings when they exist in the idiomatic versions of the impls.

i could make domvm's impl a few % faster by making similar impl clarity concessions. however, i've been simplifying the impl over time rather than optimizing it for this bench.

i dont have too many issues with libs that are upfront about using the dom directly from start to finish. but seeing JSX (or hyperscript) for initial creation and then reading back or manipulating raw dom for perf reasons is a major smell to me.

I don't really differentiate from JSX or String Templates or DOM templates (or maybe even some flavour of Hyperscript) at this point. It all is just a way to declare structure and bindings. Any choice made can be made in any. Like do you use js-dsl or some sort of fixtures to handle loops and conditionals? It's most customary to see these "fixtures" this in String or DOM templates but something I've seriously considered for JSX since using JS requires mapping then reconciliation which is extra work. Template Literal based libraries generally have the same js as DSL as JSX. At this point it feels a bit more of looks like HTML, effectively is HTML.

@Havunen Event delegation doesn't necessarily mean outside of the component responsible for the state. That's actually one of the reasons I liked the most recent mechanism I did for Solid. It lets any event binding be the delegated handler. The idea that the row has to be a Component is where I have the most issue. It's actually one of my biggest awkwardnesses with React like libraries. When dealing with a List generally the parent has the most input on what the behavior should be when items are added/removed/selected. The row should reflect this state but all ownership is from the parent. In some cases yes most of the row/item is a child component other cases it's unnecessary, but I feel it's a very important choice for the developer to avoid unnecessary boilerplate, or rather to ease into the structure that fits as their application scales. I use a Web Components with Knockout at my day job in media heavy application and we use event delegation a lot in photo/video grids, feeds, etc. Mostly it is used at a Component level (our Webcomponent Base Class has an this.on method built in that listens for events from the component's Shadow Root)

My experience with auto delegation at the library level has been fighting against it as creator of generic Web Component wrapper library. There is a great little library someone made for react to retarget events to be Shadow DOM compatible for React, and so there is a way to make these sort of libraries work. I think it's a perfectly valid choice for a Framework to make, it's just not the only choice.

======================

@leeoniya I think getting past event delegation, 'select' does have the greatest "smell" to it. It's been an area that I've been struggling with the most. From my experience with Knockout new Developers hit issues with understanding how to properly handle select(hover)/multi-select. It's because for fine grained you have essentially 2 different ways to solve the issue:

  1. Maintain the initial list and a selection and compare per rendered row
  2. Map over the initial list and add a selected observable that can be toggled before iterating for rendering

Knockout docs would lead you to believe approach 2 is the approach, but it is very verbose. 1 isnt inline with how they are thinking about the problem. One of the things I wanted to do with Solid is make this sort of operation "a thing", a binding delegation. The idea that if the source of change is local state outside of the data model (ie selection isn't persisted), that the handler could also be removed from the main render iteration much like an event handler is. Unfortunately lack of imagination or JSX syntax limitations sort left attempts to use standard binding syntax to represent this more confusing than useful. After seeing the Surplus implementation I felt it was much more inline to view this as a data transformation operator acting on the DOM directly. In so no need to introduce a new thing and just use a standard operator pattern for Solid. Does it look a bit fishy? Probably, although I designed it in a re-usable way to solve what I consider a real issue for these sort of libraries. It is packaged as part of the library.

=========================

The suggestions I made in the original post were mostly what I saw could be framework agnostic constraints. I do get the sense that among the VDOM crowd because of expectation of VDOM implementations you guys judge other more specific things to be baseline atleast for those frameworks, since compromising those goes against the desired abstraction. I get it, Solid built using S.js (the same as Surplus) and I am very absolute in making the implementation use ES Proxies for simple object access instead directly using S Signals which would considerably improve the performance in a number of benchmarks. In someways Solid will never be more performant than Surplus so I'm constantly trying to find other ways to improve performance or make the developer experience better (in my eyes).

I guess the challenge here is are there any constraints that makes sense as much for React as they do for say DomC. I'd venture to say that Stage0 is arguably a reference implementation and that it breaks some of the rules, but I don't believe I've captured that in the initial post. Nothing against Stage0 but it doesn't share several of the same characteristics that even DomC does. It has no declarative binding system and requires hand written update methods. But maybe I'm wrong since sometimes when I read some VDOM implementations, where there are chained specialized functions adding inlined event handlers and the like, while it's a different DSL and I know it's using the VDOM it looks just as sequential control flow oriented(dare I say imperative) as the underlying DOM manipulations that Stage0 is doing. And that's the thing because VDOM is scheduled and redraws downward despite the tools like JSX we use to make it look different. Is the running of say IVI or React any less imperative than Stage0. Is that even a thing? How much squinting does it take for React.createElement to look like document.createElement?

I asked the question because I actually don't know. Maybe because we shouldn't restrict on features that aren't necessary for the Benchmark this whole thing is a wash. I was looking to see if there was a concrete definition for data driven. Like if data binding could be defined. If there was something that makes these libraries/frameworks recognizably different from jQuery (or perhaps something like Stage0). I think if there is a way the conversation starts at what we consider the minimum, and then see if we can raise the bar over time.

My experience with auto delegation at the library level has been fighting against it as creator of generic Web Component wrapper library.

If you are trying to solve a "performance" problem with auto event delegation, then it is definitely not worth it. React and ivi are doing it to get more control over event order, event flow and scheduling, there are use cases like RTE that require doing this stuff to solve issues with different browsers. Also, when I've designed synthetic events in ivi, I've tried to make them flexible enough, so I can try to experiment with an efficient generic solution for Gestures/DnD.

By using explicit event delegation in the benchmark implementation, you just forcing everyone else to rewrite their implementations(every single one can be implemented this way), because obviously it can reduce alot of memory allocations. Synthetic events in ivi require additional 72+(closure size) bytes per each event handler, so to those who compete at this low numbers it can make a noticeable difference. If I remove explicit event delegation from the ivi implementation, it will consume more memory than Inferno implementation and I am ok with it.

@localvoid I meant from a Shadow Root perspective. Like I've written different auto event delegation patterns, versions of Solid have been submitted here with them until I removed them. I didn't feel the complication was worth it. I was trying to look at the DOM structure to decide where to put the handler mount points, or make a system that involved multiple bindings. I needed to change how the events worked in order to make it fit for everything so it introduced awkward caveats for events not fired from the framework. Basically the complexity almost completely negated any benefit and I don't just mean performance, I mean cognitively.

Like do you stick the handler on the body? Does it consider composed and non-composed events over Shadow Roots? I know there are some solutions in this zone, but events are a universal DOM concept that go beyond any frameworks ability to use/wrap them. Do you guys know if your framework's event delegation works if your mount point is in a Shadow Root or if you are dealing with multiple nested Shadow Roots?

=======================================
Anyway I was thinking of categorizing the perceived levels of violations of the abstraction. Where level 1 is the most core. I'm not sure if it's a matter of drawing a line somewhere but when I'm progressively categorizing implementations I was thinking something like this:

  1. A given rest state(I not during rerendering/CSS updates) is not unserializable from pure data representation.
    Ex. Storing DOM nodes in the applications data
    Ex Impl: VanillaJS

  2. Data updates require handwritten DOM updates
    Ex. Implementation Code writing update/reconcilliation code
    Ex. Impl: Stage0

  3. Implementation requires knowledge of DOM Nodes
    3a. API's that expose DOM nodes for direct mutations
    Ex. (el, selected) => el.className = selected ? 'danger' : ''
    Ex Impl: Solid, Surplus
    3b. Implementation uses knowledge of the DOM structure
    Ex target.closest("tr").firstChild.textContent
    Ex Impl: ivi, Surplus

  4. Implementation reads from a DOM Node directly
    Ex. e.target.matches('.delete')
    Ex Impl: ivi, Solid, Surplus

The only reason I'm differentiating 3 from 4 is that I think there is a big difference between getting passed an object from an event and reading it, and say being expected to mutate it or know how to traverse over the tree. And the difference between 2 and 3a is the former is the main update loop, and the latter is essentially an event handler/directive. But again this separation is probably largely due to how I perceive severity of issues. Like I don't really consider 4 a thing. If a library can write:

const div = <div />

Then you are aware of a DOM node. Similarly event.target is a thing. Level 3 is a bit more egregious in that you are telling people to mutate or traverse the structure. It's easy to see how depending on the implementation some view refactoring could break the functionality here. Still it's a reasonable place for optimization in your actual projects and a library exposing this as an option isn't terrible but it crosses a line. Everything above that is generally where I draw my line here. But I've definitely broken all the rules in real applications.

I know I could make my Solid implementation be completely compliant, but I don't feel that is the most representative of how you'd use the library. I think Level 3 compliant is possible too without compromising really but it's a small technicality (a small API abstraction) that wouldn't stop this general use pattern in the future elsewhere in the library.

Really interesting conversation. I wish I could take part more, but my startup has been getting some traction over the last few months and I'm totally slammed.

Whatever comes out of this discussion, can I ask as a courtesy between us framework authors that we not disparage and spread falsehoods about each others' work on other forums? And if you do, would you at least identify that you have a horse in the race on which you're commenting? (Funniest part of the above exchange is when @naasking asks @localvoid "As an aside, do you have any experience with Ivi?") We can respectfully disagree about approaches without anonymously spreading FUD about each others' work.

My position with Surplus, as I've said before, is that I think it's a flaw for modern frameworks to redefine as core a part of DOM semantics as event propagation. That's not the final word on it -- it's a design decision, meaning it has tradeoffs, and those may play differently in different scenarios. For instance, in the past, when there was so much difference between browsers' event behavior, a normalized synthetic event system made sense. For VDOM authors, the encapsulation provided by VDOM makes it hard to add event delegation outside of the core library.

Surplus doesn't have that issue. Since it exposes real nodes, an event delegation system can easily be added as a library. I haven't, because a) I haven't found an abstraction I liked well enough, and b) I haven't felt a need great enough.

@localvoid I have nothing but resepect for your work with with ivi. I think it's a great library. We're pursuing different approaches, which is a good thing for the ecosystem as a whole. If I ever do write an event library, I'll certainly take a look at what you've done. I hope there's some use to you in Surplus as well.

As for domc, I think the approach of real dom nodes with code gen and top-down data binding is a promising one and I'm glad @Freak613 is exploring it. It's the only approach here that makes me think I might not be able to beat it with Surplus :).

That said, the implementation was clearly at a very early stage when I looked at it (the initial submission). Basic things caused it to blow up:

  • your app couldn't have more than 26 binding locations globally or the compiler emitted bogus code
  • strings couldn't have spaces, so you could set a classname to "danger" but not "danger foo" or the compiler emitted bogus code

I hope those were symptoms of an early stage of development, but I was a little concerned that some of them looked like they might have been intentional tradeoffs to make the compiler small and fast (e.g. processing code using .split() and regexes rather than a full parse). My advice would be to absolutely keep pursuing it and keep using this benchmark to guide design, but maybe not submit until you can use domc to build an app at least as complicated as, say, the realword demo.

Best
-- Adam

@adamhaile

can I ask as a courtesy between us framework authors that we not disparage and spread falsehoods about each others' work on other forums?

I think that you should remove this from surplus documentation: "There are no components, no virtual elements, no lifecycle, no setState(), no componentWillReceiveProps(), no diff/patch, etc etc.". This is the main reason why I thought that surplus doesn't have lifecycles, components and diff/patch algorithms.

S nodes are "components", S nodes have lifecycles, S nodes have diff/patch algorithms. It is the same dataflow pipelines, in React you are just always pushing all input data top-down and in surplus you are observing data changes/diffing/pushing further.

For VDOM authors, the encapsulation provided by VDOM makes it hard to add event delegation outside of the core library.

Here is an example https://github.com/NekR/preact-delegate , with some modifications it is possible to make it efficient and just attach some data(model) instead of event handlers.

I have nothing but resepect for your work with with ivi. I think it's a great library. We're pursuing different approaches, which is a good thing for the ecosystem as a whole. If I ever do write an event library, I'll certainly take a look at what you've done. I hope there's some use to you in Surplus as well.

I just don't understand why you need to abuse every possible "hack"/advantage in the benchmark implementation, the same thing were historically with Inferno when I've asked many times to remove recycling from the benchmark implementation. I just don't get it.

I hope there's some use to you in Surplus as well.

I strongly believe that for some use cases it is a superior approach.

Here is how I could create a simple "idiomatic" API for event delegation in React-like library that doesn't touch DOM nodes directly in userspace:

const ACTION = Symbol();
const ROW = Symbol();

function RowList({ rows }) {
  return (
    <ul
      onClick={delegatedEventHandler([ACTION, ROW], (ev, data) => {
        if (data[ACTION] === "remove") {
          remove(data[ROW]);
        }
      })}
    >
      {rows.map((row) => <Row row={row} key={row.id} />)}
    </ul>
  );
}

function Row({ row }) {
  // Here is one of the reasons why I don't like JSX :)
  // return <li [ACTION]="remove" [ROW]={row} />;
  return React.createElement("li", { [ACTION]: "remove", [ROW]: row });
}

// And this is just an implementation detail hidden from userspace in some external library.
function delegatedEventHandler(symbols, handler) {
  return (ev) => {
    const data = {};
    let node = ev.target;
    while (node !== ev.currentTarget) {
      for (let i = 0; i < symbols.length; i++) {
        const k = symbols[i];
        const d = node[k];
        if (d !== void 0) {
          data[k] = d;
        }
      }
    }
    handler(ev, data);
  };
}

@ryansolid I think getting past event delegation, 'select' does have the greatest "smell" to it. It's been an area that I've been struggling with the most.

Just took a quick look at the current surplus implementation :) It is even worse than event delegation, why almost all vdom implementations in this benchmark are even diffing all rows in the "select row" test case when it is possible just to short-circuit diffing with sCU before rendering a list and assign className directly :)

This "advanced" technique drops ratio of data binding per DOM node even lower and this benchmark is already has really low ratio and biased towards libraries like surplus.

@localvoid Thanks for the example. I'm essentially doing something similar for row data, so additionally binding the action like that is at almost no extra cost. I'm going to play around with it and see how it feels.

I guess thing is with signals/observables in general. The mentality is very much about transformations and synchronization. Abstracting the DOM isn't as much of a goal as declaratively expressing behavior. I do also think the push for Web Components which are in essence DOM elements makes the abstractions more awkward. Events for example are a primary way of communication between them that extends beyond the boundary of the render library. There are clearly different goals starting to show up. And I do agree that there are some communication challenges. I feel fine grain has generally been the most misunderstood since while solving the same problem (synchronizing data updates and rendering to the screen) it attacked it at a different angle that was completely unaffected by the advent of the VDOM unlike the other approaches at the time. VDOM libraries didn't solve a problem these libraries were looking to solve or weren't already doing in their own way. So React as a baseline is not a common base.

I've been trying to educate a bit in these threads because I honestly don't believe there is intention to heap on Surplus. While some things can be used analogously the mental model is different. Is an event emitter with a dispose function a component with lifecycle functions? Maybe if you squint. You could make state a Signal, run it through a computation that called a series of functions and approximate it. But you could also have a 3000 Signals represent every cell in a 1000 row 3 column table. Surplus wraps all updates per node in a single computation, so the difference between 1 binding and 5 on a single node is not that significant. However having bindings on many different nodes is generally more expensive. But ultimately it's all just computations(automated transformations) and by dealing directly with the primitives the author decides the granularity. It isn't unlike Components in that sense it just generally isn't the boundaries by which you organize your code and it isn't so graduated that anyone would call it a lifecycle.

However, Personally I love React's simple state representation, use of props and functional composition. Solid conceptually (although not in implementation) is 90% just an API wrapper of Surplus. With a bit more work, I can probably have an API that is VDOM approved. But I also see no problem with a library like Surplus, in terms of being complete and fully functional within it's purpose. When I see a library like DomC I just immediately want to figure a clean way to autotrack and memoize the the execution tree. But I that isn't something necessary for DomC.

I guess the question is, is there any common baseline?

@ryansolid: I guess the question is, is there any common baseline?

In my opinion we should choose role models for different approaches and use them as recommendations on how to create benchmark implementations.

  • f(state) => UI with full top-down update - React implementation
  • f(state) => UI with coarse grained dirty checking on immutable data - React-redux implementation
  • f(state) => UI with coarse grained dirty checking on mutable data - ?
  • f(state) => UI with streams - ?
  • f(state) => UI with fine-grained observables - Vue
  • direct DOM bindings with fine-grained observables - basic surplus implementation
  • And a special vanilla-style group for stage0 :) I just don't care what they will do in this implementations, I don't consider libraries like this as a solution for solving complex UI problems.

And if everyone agrees to remove "advanced" tricks that can be abused in any implementation and by using them we are just forcing everyone else to use them, I'll submit this ivi implementation that doesn't use any tricks[1][2][3]. It is almost identical to react-redux implementation.

  1. mutable data wrapped in boxes to change object identity
  2. this node is cloned with childrens because I don't need to attach any event handlers
  3. event delegation

I was pretty impressed with the adaptability of ivi for those tricks. Going as far as boxing mutations in a Virtual DOM library is something I wasn't sure I'd ever see.

Like with rAF issue as long as certain libraries do it behind the scenes it wgets pretty hard for people to stop using a certain technique. I would be surprised reach agreement to not do Event Delegation given how present it is in libraries and how easy it is to wire up. I am unsure if there will be much movement there. I was mostly just thinking from the other side if there were specific conditions that had to met. Or oveararching categorization.

The idea of setting up desired implementations in each category seems interesting. Maybe if there are less to start.

Going as far as boxing mutations in a Virtual DOM library is something I wasn't sure I'd ever see.

Versioning is a quite common technique when working with mutable data, I think that Glimmer supports it out of the box. Boxing just doesn't require monotonically increasing clock.

Ok so is this just a categorization and filtering issue?(Not the everyone adhere to the same standard question, but the is there a baseline question?). I started this issue because I was wondering if there was a clean line to draw between what I consider "Reference" and "Direct DOM". But maybe it's not an inclusivity thing just an organization thing. We label implementations to allow for sub comparisons much easier by ability to filter the Results. It takes the onus off the viewer a bit and on the implementation to use the list of predefined labels. These labels don't even have to mutually exclusive. It would be impractical to split the default view.

I guess defining the labels is still a hard task. For Keyed Non-Keyed we have a simple test. This by nature would be looser. As a starting point just calling out:

Reference: VanillaJS, VanillaJS-WC, Stage0, StdWeb
DirectDOM: DomC, Surplus, Solid, *-JSX, AttoDom, LitHTML, Angular
VirtualDOM: Ivi, Inferno, PetiteDOM, DomVM, React, Vue

And so on... Maybe it's too much work, or maybe highlighting technology differences isn't primary enough concern. But I find that opinions of what is acceptable changes along these lines a bit. While not cleanly defined I do so sort of see 2 camps here. Someone looking at a glance doesn't know the decisions made by the libraries or Implementations (Direct vs VDOM, PreCompiled or not, etc) that this could facilitate conversations under specific Label/Categories. Eventually if the filters can be serialized into the url at some point people could just pass around sub tables. If a VDOM library wants to link a comparison of other keyed VDOM libraries they could.

@ryansolid There is also mixed libraries like preact which diffs vDOM to real dom.

@Havunen So perhaps it isn't so clean cut?

My thinking is that once you you have resigned yourself to rendering a Virtual DOM a certain level of abstraction is necessary and it's something that defines all these frameworks. I realize this is somewhat arbitrary labeling. But the idea is that the list is fixed and defined as well as can be and the implementations self identify.

This discussion has in one way or another been going on for over a year. I wasn't trying to get back into "Oh look how Surplus Impl knows that events have DOM elements, and that those elements have a className property. For Shame!!" But I recognize I was essentially doing the same thing with the first DomC submission (it's been addressed since) and Stage0 which invited this conversation back in. So while I strongly feel the complete list (other than the non-keyed separation) is what we should generally be looking at and that it doesn't make sense to exclude any data driven implementation (maybe that even means you jQuery, if you can get up from your walker), I think there is a potential here to define more zones that authors and viewers a like vie for their frameworks to dominate. It takes a bit of pressure off trying to hold to a single standard which I don't think we're getting any closer to reaching and for any framework except those really vying for the top spot, it can disincentivize weird hacks as it gives them a place where they can be judged more on commonality. Stefan has provided great tools to do this already but it takes knowing what you are looking for.

@ryansolid My thinking is that once you you have resigned yourself to rendering a Virtual DOM a certain level of abstraction is necessary and it's something that defines all these frameworks.

I don't understand what is the difference between LitHTML/Angular and React. LitHTML creates a static template + set of instructions that diffs input data and applies data changes, Angular generates diff instructions directly as JS code(incremental DOM) and diffs logical DOM(virtual DOM) using this set of instructions, React does the same and the only difference is that instructions are basic JS values like in the lisp(code as data).

In fact, I don't see too much difference in a level of abstraction between this libraries and Surplus, the only difference is that Surplus requires observable values everywhere and other libraries can use different strategies to manage state.

Thats fair, I guess I was trying to tie too many commonalities with my experiences where there are none. Surplus in it's fine grain execution can toss around an JSX tag as if it sees fit as if it was a DOM node but that isn't anything common. Knockout was very strict about exposing the DOM outside of directives (with one exception being event delegation).

It was working with webcomponents that really blurred the lines for me. When the container ends up being a DOM element itself there is this weird duality. But you are right it isn't a concern of the renderer. Just because say LitHTML is often used in Webcomponents doesn't necessarily make the abstraction different. Different solutions have abstracted the internals of Custom Elements differently which probably is more of a consideration than how the view is presented or changes are propagated.

So perhaps the original post covers it. And it's just a matter of identifying Reference builds. But so I understand, the primary issue here with Surplus like code is that it exposes the details to the implementation. If a library did all the same things but wrapped it's premade constructs say a directive that ships with the library that would be fine. Like that React-like Event Delegation example. If the implementation author didn't think it was worth making it part of the library ecosystem it doesn't belong here. Event Delegation being so widespread more controversial, but that covers hacks like rAF and the like.

Also more related to the original post although I'm not sure how to enforce it (if it's a matter of having a certain tests), should there be a baseline set of features. We agree that every library/approach is different so we won't have feature parity, but at the baseline. I wrote:

provide a mechanism or abstract complexity of reconciliation of basic template control flow (conditionals, loops)

I feel like if a library isn't fully functional here it is misrepresenting itself. While I'm sure it won't be hard for @Freak613 to add, DomC doesn't have conditional directives. This might not be obvious from VDOM perspective where an if statement suffices, but for a pure string templated system it has be in a directive. The benchmark doesn't need it, but from a usage standpoint the library would be glaringly incomplete. However, for users coming to view the results it isn't really apparent until they dig in. This isn't to point fingers more to wonder if there is a statement that can be made before people even submit their libraries/frameworks so they have an idea of expectations.

Fine Grained Reactive are going to continue be an issue for uniformity. Realistically you can write anything in a computation and it works.

@luwes The Sinuous implementation is worth bringing up here. Technically it isn't breaking the stipulation I made in the first post here and can recreate the view from any rest state. But it takes the Surplus/Solid approach even further not even pulling a ref from the rendered nodes. Instead just straight up doing a document.querySelector right in the middle of the computation. I'm not one to call the kettle black and while the hook Solid uses is completely abstractable, I just figured this should be on the radar.

luwes commented

@ryansolid good you brought this up, I wasn't aware at the time it was not allowed but I've been making some optimizations and coincidently changed this along. PR #575

I'm not sure it is or isn't. Technically from what I was proposing it's fine I think. I just wanted to draw attention to it more just to point out how slippery the slope is even in my definition. When html<div></div> returns a real div and a computation allows for a rendering entry point anywhere there are lots of ways to attack the problem. I'm glad you changed your implementation to be aligned with the other similar ones but I'm not even sure what to think about the original.

@ts-thomas

I can't make any suggestion about those guideline, but this is a full data-driven approach you can take as a reference: https://github.com/krausest/js-framework-benchmark/blob/177776a591b49df5ce9dba6d99f2df0b0b4b04b0/frameworks/keyed/mikado-proxy/src/main.js

Instead of forcing everyone to read through your library code, is there something special about this implementation that makes it a good reference in particular? I'm sure a vast majority of libraries here are good examples. Like no one would say the React implementation is contentious. The problem isn't the challenge finding an ideal example. It doesn't exist, every library is different and they are all beautiful in their own way. It is trying to define what the lowest common denominator is, if that is even possible.

It's impossible to totally tell if an implementation is data driven by just looking at the implementation code. Especially when you consider things like proxies which can be hiding almost anything behind it. I suppose you could tell which is definitely not, but even in the Mikado Swap case I had to look at the library. Swap could be a generic data function. You most definitely need to look at the library code to be sure to understand what is going. Which is perfectly fine and expected. So I was just asking in the sake of saving time and effort.

Truthfully I don't see anything particularly special on the surface of the implementation. I see some syntax choices but this implementation looks similar to most. MobX uses proxies a similar way at least on the surface. Vue uses getter/setter objects which can work sort of similar. Sorry if I seem dense, I'm asking because I honestly am not seeing what the difference is. Most implementations involve similar methods that set state values. Many use functions, a couple use Proxies but it is effectively the same since Proxies are just function calls. Now something your library is doing could be different but I wouldn't know that without going through the library source code of this proxy feature.

Although a better question I suppose is the point being made. Are you saying other implementations are not data driven? I would say most are and there are a few questionable decisions made by some libraries. Do you have a different take?

Yes that mobx jsx example is good example of something contentious. Half of this thread is about Surplus using that approach as an example.

If you look at the rest of that implementation or look at React MobX that isn't the case. It is proxy triggered update. Vue doesn't call a render or setState function. It triggers on data set via the setter. That being said admittedly I'm a bit surprised about the need to clone as documented but it is the same sort of mechanism. It is possible all the Vue 3 (which still hasn't come out) demos affected my view of it. There are probably more in here. NX which was removed because it hasn't been updated in 2 years also used this approach. Knockout ES5 is a library that has been doing similar since 2014. Basically it isn't a particular new approach. Solid worked that way 2016 time period (before I open sourced it). Even at that time I didn't necessarily consider new or unique. Sorry I've kind of gone off the rails a bit. I can clearly see how you would have not come across this before naturally and have developed something cool for yourself. It was just the rhetoric that has accompanied this that has me still discussing this.

But this is sort of beside the fact. I don't think you need this to be a particular good example of being data-driven. Pointing out the select row approach as being nefarious fair. By the way that isn't the only way people are getting around it. Others are adding an isSelected to each data row. And let's be fair. Your 100% data-driven proxy implementation appears doing a lookup against the view, which arguably is also the same. This is something the VDOM authors will call you out on.

store[selected = view.index(target)]["selected"] = "danger";
. I won't necessarily, a number of libraries do stuff here. But it is one of those things.

So I guess we do see different things. I tend to see the similarities in things since it makes it easier to classify and follow larger trends.

Yeah it comes with having customers and being out there for years. MobX itself is was really influenced by KnockoutJS and the other Reactive libraries in the early 2010's. Vue also has this influence too. Surplus/Solid/Surplus etc all owe their heritage to Knockout. Luckily us as newer libraries don't need to carry that legacy. This area is my passion for frontend development. I've been very keenly aware of trends and approaches here so understanding that Solid for instance was trying to take those libraries by give them the interface of VDOM libraries. There are a lot of tradeoffs. It was through this walking the line back and forth I've developed my perspective of all things be more or less similar, because I travel back and forth between these libraries to pick and choose the parts I like.. makes me very opinionated. But on the other side it is why I am not too quick to point at an idealized form. It is really all sort of the same.

Which back to the thread topic, I wonder is there some common baseline we can agree on for these benchmarks. I mean anyone can do whatever they want, but how do we possibly compare on similar axis yet let each library can highlight its strengths and tradeoffs.

With Surplus removal, #772, and #794 we remove the legacy around certain categories of hacks. I know it is still out there in a couple of places but I'm hoping to remove the precedence. For selection, that just leaves the putting the selected state on each row, as a way of sidestepping the test.

I was trying to think if I had a simple heuristic for deciding if an implementation goes against the spirit of the benchmark and I came up with one.

  • Assigning (or via setter method) nested data (on row object or equivalent dom objects) in user implementation code for anything other than the label update in Test 3.

Swap changes index in the array. Create sets top-level data as does selection. Only update every 10th row involves direct nested data manipulation. Regardless of if you expose the DOM or not if the user implementation code assigns any other nested value during due to one of the click actions you are probably side-stepping the benchmark in some way.

I'm closing older issues that won't get fixed.