Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Research about replacing Knockout.js by a virtual DOM with maintaining full back compat #383

Open
exyi opened this issue Jul 15, 2017 · 33 comments

Comments

@exyi
Copy link
Member

exyi commented Jul 15, 2017

We plan to replace the Knockout.js library by an immutable viewmodel with virtual DOM - something similar to React.

Motivation

Although knockout.js is IMO well implemented and stable library, the UI updating architecture is a bit glitchy - for example, the <select> binding made us a whole bunch of problems because it's bound to multiple properties that are not changed atomically and the combination of properties between the changes is invalid, so the control breaks. And even though we came up with few hacks to fix the concrete problems, we can IMHO expect similar issues with other controls.

The second problem is client side performance with larger than the small data set. I have no idea how to make it better with knockout and sometimes it is a real problem. And again, the problem lies in the observable notifications.

Another problem is rendering a template without wrapper tag - although knockout has a concept of virtual elements, the performance is really bad (~6x), not all bindings are supported and HTML comments are sometimes stripped off by compression proxies.

How would it work with DotVVM

I propose migration to virtual DOM architecture. https://github.com/Matt-Esch/virtual-dom library looks lightweight and extensible enough, so let's talk about it, but all the V-DOM implementations are quite similar, so we can choose the better one later.

ViewModel

The viewmodel would be a simple JS object without the observables that will never mutate (maybe we could use a immutable object library but I think it's unnecessary). Because the properties can't be mutated, there is no need to observe the property changes and the viewmodel must be replaced by a completely new one. On the first glance this may look like a crazy performance hit, but as you can reuse parts from the old one, it only does O(number of changes) allocations, and has completely superior performance in comparison with the change notifications. It would also enable us to track old states for debugging purposes and has IMO much simpler to understand architecture.

Rendering

Now DotVVM controls render HTML enriched by the knockout data-bind attributes, which is intuitive to read and understand. The knockout binding handlers are tightly integrated with the DOM and are created based on the data-bind attributes on page init. React and other V-DOM frameworks on the other hand use Javascript functions instead of enriched html to create the virtual DOM and update the real one, which is quite incompatible with the DotVVM control architecture. I think the best solution would be to generate enriched html by a data-render attribute and generate the render function from that. But there is a bunch of other possible options like rendering plain html with the render function on the server.

Compatibility

Now, you probably think that all this is nice, but this is going to break everything or how the hell do you want to maintain backward compatibility. Of course this change is going to break a lot of things, but I'd like to maintain compatibility for basic usage of the Javascript API and for simple controls. I'd reimplement part of the knockout API for the immutable viewmodel and export ko global.

  • dotvvm.viewModels.root.MyProperty().Prop123() and ko.contextFor - we can return something that will look like a ko.observable and the subscriber will be notified when the entire viewModel is changed.
  • Knockout Binding Handlers - This is a bit more tricky, but I think it's possible. As the binding handler is manipulating the DOM directly, it will have to bypass the V-DOM. The mentioned virtual-dom library supports a concept of hooks which are functions ran when the real element is created, so we can apply the old binding handler on it. It will basically update the DOM after it is created from the v-dom. And because the V-DOM only updates only parts that needs to be changed, it should not wipe the handler's element on every re-render.
@exyi exyi added this to the DotVVM 2.0 Maybe milestone Jul 15, 2017
@tomasherceg
Copy link
Member

I really like this idea.

I can imagine a Knockout JS compatible wrapper over the viewmodel which would provide the subscribes and value update callbacks.
Is there something like IDynamicMetaObjectProvider in JavaScript? We wouldn't have to copy the structure of the plain viewmodel object - we would just got a callback when the user calls object.something.

I can also imagine preserving the compatibility of the controls with excluding a particular elements from the VDOM diffs - a control could say "hey, this is my element, I am going to manage its contents by myself, so don't diff inside, just call update in the binding handler when something changes".

But let's face it - this is a very big thing and I don't think we should start with this until DotVVM has significantly more users than now.

@exyi
Copy link
Member Author

exyi commented Jul 15, 2017

I didn't think that copying the properties would be a problem, but may be a problem for correctness when a property would be added to the object. But ES2015 has a Proxy object for that. Unfortunately, it's not supported by IE, so we will probably have to copy the properties https://caniuse.com/#feat=proxy

The virtual-dom library supports thunks - objects that handle the diff by itself, so it would be possible, but I don't think it'd be necessary, the V-DOM will not update if it hasn't changed, and if it did, it would re-register the binding handler.

@exyi
Copy link
Member Author

exyi commented Jul 15, 2017

I agree that this is probably the biggest refactoring we did in DotVVM and it certainly would not be that straightforward as I have just described, but when we will have more users, there will be more stuff to break and more time spent in hacking knockout flaws.

@djanosik
Copy link
Contributor

I think it will be necessary to exclude some elements from diff, but only for compatibility reasons. Complex UI controls may have some state that would be reset by re-registering the binding handler. That's not always what you want. We can't store everything in viewmodel.

We will need better concept of UI controls. Controls will always have internal state that will not be stored in viewmodel and there must be a way to update VDOM when state is changed. It's actually very similar to how React components are updated. Also we need to handle communication between two controls.

Rendering is the tricky part. We need to render everything on server and make updates on client. But we should keep the rendering logic in one place. But how? With enriched HTML we will probably never get rid of eval and we will never be CSP-compliant.


If we wait for DotVVM to have significantly more users, it will be significantly more difficult to make this real.

@exyi
Copy link
Member Author

exyi commented Jul 15, 2017

@djanosik These are all good points, thank you.

  1. The VDOM will not touch the element unless something has changed in the virtual DOM, so it will not reset the handler unless the control disappears and reappears later, for example using an if binding. And it should also work if the ko handler would be applied to a control with VDOM bindings inside unless the control completely reorganizes the content (which is unsupportable).

  2. Do you think that it would be better to build it on top of React? I would probably simplify integration with 3rd party components as there is quite a lot of them, and allow you to use all the React libraries. But the control state should be solvable even in the virtual-dom library by the Thunks.

I'm not exactly sure what do you mean by control communication, but the ViewModel should be the best place where to exchange information, why is it unusable for the controls? Maybe it would be better to make the viewModel more usable for control state than hiding the state somewhere.

@exyi
Copy link
Member Author

exyi commented Jul 15, 2017

@djanosik

  1. Maybe the best solution would be to render pure HTML for server rendered controls and a JS function from the client-side bindings. We have quite decent id addressing infranstructure, that may help with that.

@djanosik
Copy link
Contributor

  1. Ok, it might actually work that way. Depends on how we will construct the VDOM.
  2. Let's split this into three parts:
    1. From my point of view, VM is here to transfer data from server to client and the other way around. But I am talking about temporary state, like "what item in list has focus", "is dropdown opened", etc. You don't want to store this in viewmodel. Well, you can, but it would make controls far more complicated.
    2. By control communication I mean some system of events / callbacks / whatever. Consider this scenario: you have RichTextBox control, inside you have Toolbar control and inside you have Button. How the hell do I call one of RichTextBox methods when Button is clicked? It's not just about exchanging data.
    3. It's really tempting to use an existing library, but I am afraid we will not find anything compatible with DotVVM concepts.
  3. Hmm. But we will still need eval or inline scripts to execute binding expressions. There is probably no way to avoid that.

@exyi
Copy link
Member Author

exyi commented Jul 15, 2017

  1. This looks almost like an independent issue, I think @martindybal had some notes about someting similar.
  2. I think we could generate first the HTML and then the javascript resource, that will be linked from the page, allowed in the CSP header and would make the static HTML live (like React server rendering does when it works).

@djanosik
Copy link
Contributor

djanosik commented Jul 15, 2017

  1. Yes, it's not actually related to VDOM. But we should keep that in mind when we do some changes to control architecture.
  2. There are also other options: a) Use nonce attribute to allow specific inline scripts. b) Use iframe as a sandbox where you can safely evaluate expressions.

@darilek
Copy link
Contributor

darilek commented Jul 18, 2017

Hello,
Have You looked at Aurelia framework? Especially for binding and rendering features?

  • It is pretty modularized - You don´t have to use entire framework
  • has superior binding/rendering performance (even without virtual DOM) compared to React, Angular and Knockout
  • supports two-way bindings without observables
  • uses comopnent-like rendering based on standardized Web Components (no ugly JSX :-)

@exyi
Copy link
Member Author

exyi commented Jul 18, 2017

@darilek I don't know Aurelia at all, so it may be very helpful you would share some experiences. Do you have tips for concrete parts, that I should have a look at?

However, I don't like the concepts very much as the two-way binding is actually the thing that I'd like to get rid of and nicely usable components are not a killer feature for a generated code. I'm convinced that immutable VDOM is much cleaner architecture at a cost of being less comfortable for direct usage, which does not matter for a generated code. The mentioned virtual-dom library has very simple yet quite powerful API, in contrast with Aurelia's complex templates (at least seems to be complex from a quick glance, and it actually makes sense for "end-developer" framework, DotVVM also has very complex control infrastructure and React with all the libraries to make it useful would not be much simpler)

@zulq
Copy link

zulq commented Jul 19, 2017

Really was thinking to use DotVVM as main framework but this issue is making me skeptical. I have just added it to my asp.net core app and was impressed how it integrate seamlessly.
I believe not to scare away potential users, you should put a proper roadmap and any other ways of restoring potential/existing users confidence.
Nonetheless, kudos for such a great framework.

@rigantiteamcity
Copy link
Contributor

rigantiteamcity commented Jul 19, 2017 via email

@exyi
Copy link
Member Author

exyi commented Jul 19, 2017

@zulq Actually the backward compatibility is the main concern of this issue, so you can be quite sure, that everything will work (in the compatibility mode) even after this change unless you'd use some knockout's "deep API". As I said in the first comment, we plan to export the ko global, so don't be afraid to use it ;)

We don't want to take the Angular's or Python's way of breaking the most used APIs :)

@tomasherceg tomasherceg changed the title Replace Knockout.js by a virtual DOM Research about replacing Knockout.js by a virtual DOM with maintaining full back compat Jul 20, 2017
@zulq
Copy link

zulq commented Jul 20, 2017

Thanks @tomasherceg and @exyi for the feedback. Will continue exploring.
Not related but have you guys thought of implementing your controls for Asp.net core using tag helpers.

@tomasherceg
Copy link
Member

@zulq DotVVM started before ASP.NET Core and Tag Helpers were announced. That's why we have our own view engine, parser and things like that.
The controls have lot of dependencies - Knockout JS and DotVVM JS scripts, and there are also many things that the controls do on the server - invoking all the lifecycle events and so on.

Months ago, we was talking about making some integration between Razor and DotVVM, so you could host a DotVVM user control in a Razor view or vice versa, or to be able to share "master pages", but I don't know if that's really useful.

@zulq
Copy link

zulq commented Jul 21, 2017

@tomasherceg I think it will be useful especially sharing master pages. I believe it will also encourage people to use DotVVM knowing that one can mix and match both.

@tomasherceg
Copy link
Member

@zulq Yes, but there are very few master pages in every project, so the advantage of sharing them is not so significant.

@zulq
Copy link

zulq commented Jul 21, 2017

Agreed.

@exyi
Copy link
Member Author

exyi commented Jul 24, 2017

Just an idea: it could be cool to write user control in Fable using the virtual dom in a similar way fable-arch works and render it client-side and server-side using the same piece of F# code.

@zulq
Copy link

zulq commented Jul 24, 2017

Have you looked at Vuejs? Comparison with Knockout https://vuejs.org/v2/guide/comparison.html#Knockout

@tomasherceg
Copy link
Member

@zulq We know it. The problem is that basically, Vue is Knockout with all its goodnesses and flaws. They mention it on the page - "the Vue's reactivity system is very similar".
In Knockout, we have a lot of problems with virtual elements and things like that, and they can be solved by the VDOM. That's why we are looking into this.

exyi added a commit that referenced this issue Aug 27, 2017
* Created a knockout compatibility layer, so that TaskList and simple GridView samples can run unchanged. This includes knockout virtual elements and binding handlers that manipulate the datacontext of their content
* There is actually no way to use the v-dom functionality, the only interface is through the knockout compat layer

see #383 for some context
@exyi
Copy link
Member Author

exyi commented Aug 27, 2017

Ok, I suppose that backward compatibility can be fun, at least sometimes - this was a quite cool weekend project ;)

I managed to create a proof-of-concept implementation of the knockout compatible virtual-dom based dotvvm. Current status is that the task list and some GridView samples seem to work, basic features like (two way) data binding and postbacks are probably ok, but more advanced features like Postback.Update, SPA, validation are probably completely broken. The point is that I have not made any change to dotvvm .NET code (except for resource registration) nor the knockout binding handlers, so the generated code looks exactly the same and everything looks like nothing has changed. And everything like knockout virtual elements, binding that change data context (like with and foreach) run in the compatibility mode. Currently, there is actually no way to use the advantages of the virtual-dom because I'm not sure how to exactly should the API look like, so everything has to work on top of the knockout-compat layer.

On page init it builds an AST from the DOM that was transferred from the server, that is then used as a render function. Each element is checked if it does contain anything knockout would have to handle - if it has a data-bind attribute or contain knockout comment. Then some magic happens that defers creation of the element content, because the knockout bindingHandler may change the datacontext. It creates a virtual-dom Widget that creates the element with fake content (some spans), creates knockout binding context (more magic here, it can automatically refresh all properties that are later changed in the immutable viewModel and can also write the changes back ...and it can also probably leak memory...) and runs the knockout binding handlers. Then it enumerates the fake spans from the element, detects dataContext changes, creates children (using virtual-dom) and then appends it to the element body. Updates of the Widget basically work only by changing the bindingContext - knockout handles the changes itself and invokes the binding handler and again replacing potential new fake spans that the binding created.

If you have any questions, you can probably find the answers in the code, it's in virtual-dom branch. And if something does not work, it can be solved by editing the code, it's in virtual-dom branch. :P

@exyi
Copy link
Member Author

exyi commented Sep 15, 2017

Implementation Status Update

I have moved the implementation quite significantly in past few days. It seems to me that majority of the samples should now work (completely on the compatibility api!), but I have not run the UI tests

Validation

It's migrated to the new architecture - the validation errors are stored in the immutable state object next to the validated property in a field called <propertyName>$validation. The validation process (dotvvm.validation.validateViewModel) simply returns an object that does copy the structure of the validated object and contains the errors instead of values. When this ValidationResult object is later applied to the ViewModel (using dotvvm.validation.applyValidationErrors), the errors are copied to the <propertyName>$validation fields. The knockout compatibility layer understands this structure and creates corresponding ValidationError array on the ko.observables, so validation binding handlers work unchanged 🎉

Postback.Update and SPAs

All controls that have data-dotvvm-id attribute are wrapped in a decorator that always checks an updatedControls dictionary in the render context for an updated render function. So the postBack function only has to replace the render functions for Postback.Updated controls that arrived in the response. This also handles the old problems caused by replacing entire element in the page, because VDOM only applies the difference ;)

@quigamdev quigamdev modified the milestones: DotVVM 2.0 Maybe, Future Sep 18, 2017
@exyi exyi modified the milestones: Future, VDOM Update Sep 26, 2017
@exyi
Copy link
Member Author

exyi commented Jan 3, 2018

I have looked at some virtual-dom implementation which would be usable in dotvvm. The proof-of-concept is implemented using https://github.com/Matt-Esch/virtual-dom, which is simple, works and I don't have any problems with it. Anyway, there is a ton of alternatives, so it may be worth considering them.

  • React - obviously, the most popular JS framework of this kind. The main advatage is that there is a ton of controls for React, that could obviously work well with dotvvm. On the other hand - React is pretty big and contains some unnecesary functionality for us. And it has a Facebook brand (this is the main disadvatage).
  • virtual-dom - it does not seem to be actively developed anymore (JS library, what would you expect...). I'm not aware of server-side rendering support.
  • Preact - my favorite, it seems to be smallest in size (they say ~3kb gziped). It's almost React compatiple, most React control just work with Preact out of the box, sometimes a comatibility library is required (few more bytes). JSX, React tools, HMR should just work, and it should be quite fast. We could be also able to switch to React, as the API is mostly compatible.
  • ... few more libraries that I did not find interesting in a short look, maybe have better performance. If you have any tip, comment is welcome ;)

@tomasherceg
Copy link
Member

I think that we should use React or Preact. All major companies that develop controls support React right now. And being a framework based on React, we'll look cool!

@djanosik
Copy link
Contributor

djanosik commented Jan 5, 2018

I really like the Preact library. Do you want to use its server-side rendering? Is it ok for DotVVM to require Node.js?

@tomasherceg
Copy link
Member

I am not sure how this would actually work.

@exyi
Copy link
Member Author

exyi commented Jan 5, 2018

Do you want to use its server-side rendering? Is it ok for DotVVM to require Node.js?

Yes, I'd really like to support server-side rendering of Preact controls, although I don't think we should use that in DotVVM itself to avoid the node.js dependency. It may be ok to use that in Bussiness pack, but there is not much benefit of using it in the core of dotvvm, the controls are usually quite simple and I don't want to spend performance on the .NET/Node communication.

I am not sure how this would actually work.

A (P)react control is basically a function that gets viewModel and returns a virtual dom, so you invoke the function on the server and serialize the virtual dom to html.

@exyi
Copy link
Member Author

exyi commented Jan 23, 2018

FYI: I have arraged a weekend planaton for this saturday/sunday with @tomasherceg about dotvvm 3.0, mainly about new control rendering system. If you'd be interested to participate (physically, a call about certain idea, whatever), drop me an email. We will certainly publish the results.

cc @djanosik @adamjez

@naasking
Copy link

I'm just learning about DotVVM, and it looks quite interesting. This thread piqued my interest as well because I've been looking into client side JS frameworks. If you're looking for something React-compatible, then Inferno is the way to go. It's by far the fastest and leanest React-compatible library.

If you just want a simple virtual DOM library, then something like PicoDOM or Ivi are very lightweight and the best performers.

If you want the best performer overall, then you're looking at Surplus based on S.js. It doesn't actually use a virtual DOM, it manipulates the DOM directly using reactive expressions, which is why it's typically the fastest in benchmarks.

That's my two cents!

@exyi
Copy link
Member Author

exyi commented Feb 14, 2018

@naasking Thank you for the recomendation, but I still like using Preact the most, so I'd like to briefly explain why. Using something React-like is a great advantage as most React controls would just work with DotVVM (without any compromises). And it does not actually matter that much which one will we choose, as swaping them should be easy. I like the Preact, because it's really lightweight and pretty simple, but you could certainly switch to Inferno or React in your application or we could change the default later, if Preact would be causing some problems.

S.js seems to be really similar to knockout.js's obserables, which is reasonably fast for simple scenarios, but gets a significant hit when you want to update the entire viewModel after a postback. And I suppose that it will have all the problems with the order of updating values.

Note that we are also looking at integration with Blazor, which looks pretty promising. Although I'm not really sure what will come from that, it could also substitute the VDOM migration.

@naasking
Copy link

Certainly choose which better matches your model, but I wouldn't hesitate to recommend Surplus based on S.js, as it's superbly engineered. It takes pretty much every top spot for performance and memory use in the comprehensive JS framework benchmark suite. Peruse the filterable results table here.

Knockout.js is in that suite as well, and it falls well below the others with an average overhead of ~2.14x over vanilla js, where Inferno is ~1.14x, Preact ~1.31x, and Surplus is ~1.05x. Definitely worth considering at least!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants