DEV Community

loading...
Cover image for Million.js - The Future of Virtual DOM

Million.js - The Future of Virtual DOM

Aiden Bai
student @ camas high school tinkerin' away on the web
・4 min read

Plug: I work on Million.js: <1kb virtual DOM - it's fast!

TL;DR

Virtual DOM needs to leverage the compiler, so that unnecessary diffing is not incurred.

Introduction

Recently, I published some articles detailing the Virtual DOM paradigm, implementation, and the benefits and flaws using it. These articles received mixed reception, with some developers agreeing with the points in the article, while others disagreed. They argued that compilation based frameworks that do some level of static analysis (notably Svelte) compile to imperative DOM operations and therefore bypass the overhead of a Virtual DOM engine.

You may be wondering: What's the point of using Virtual DOM, if you can just use a compiler-based framework like Svelte? While static analysis and compilation is the future, Virtual DOM should not be completely ruled out as an obselete technology. Svelte only is possible if API conditions are constrained, so that the code is predictive and therefore analyzable. For libraries that need more flexibility, such as React or Vue, conditions cannot be constrained easily and therefore a variant of the Virtual DOM is necessary for those libraries.

This is why Million.js exists—to bring the Virtual DOM into the future by leveraging the compiler for static analysis and optimizations that makes DOM manipulation be performant and flexible.

Virtual DOM Optimizations

Traditionally, Virtual DOM engines do a significant amount of computation during the diffing process. For example, when diffing children, the Virtual DOM engine not only linearly calculates which nodes need to be updated, but also determines the possible swaps/moves that can be done. Although this incurrs the least amount of DOM modifications, the computational cost can be great. Even with extremely efficient list diffing algorithms (like list-diff2), the time complexity is O(n) in the best case (not including the O(n^3 time complexity baseline for diffing). Repeat this for all the children in a vnode tree and you can just imagine how inefficient this can be.

This is why one of the major concepts to create a future oriented Virtual DOM is to be aware and construct the architecture based on the compiler. This not only increases performance by allowing for straight O(1) operations, but also gracefully falls back to normal diffing when necessary. Additionally, bundle sizes decrease significantly, reducing the amount of code that needs to be executed at runtime.

Million.js attempts to implement this with three major "lines of defense":

  1. Keys: Specify the identity of a vnode

    Keys are useful when you know that a certain vnode's position, data, and children will not change between two states. Keys can be provided by the user manually, or generated by the compiler. This allows for the vnode to be skipped entirely, avoiding unnecessary diffing (O(1))

  2. Flags: Specify the type of content of a vnode's children.

    Flags allow for diffing to skip certain computationally expensive condition branches. For example, if the vnode's children only contains text nodes, then just setting the textContent of the element would be significantly faster than constructing and replacing a text node. Million.js currently only supports 3 flags: NO_CHILDREN (O(1)), ONLY_TEXT_CHILDREN (O(n)), and ANY_CHILDREN (O(n^3)).

  3. Deltas: Specify predictive and consistent modifications of a vnode's children.

    Deltas can be utilized when simple, imperative micro-actions can be predicted through static analysis. Deltas by default are a series of imperative operations, but leverage the internal diffing algorithm to reduce DOM manipulations. Million.js currently supports 3 fundemental Delta operations: INSERT (O(1)), UPDATE (O(1) to O(n^3)), DELETE (O(1)).

Compiler Optimizations

First off, most—if not all of the implementation complexity will be with the compiler. This is because static analysis is really hard to pull in a way so that it operates as intended. Below is a list of possible optimizations, and is by no means "real static analysis."

  • Leveraging Million.js features:

    The primary way to optimize for Million.js is just leverage the compiler-focused features that it provides. This is the only way to reduce diffing assuming that the patch scope remains constant.

  • Prerendering + reducing dynamic content

    Another way of making performance better is to not even consider static content by reducing the patching scope—especially if your application is only interactive in certain areas. This is even more efficient than generating imperative DOM operations, as DOM manipulation won't even be needed! Additionally, the initial vnode should be prerendered the page, so that the page doesn't need to be fully initialized at runtime.

    Bad:
    <div></div> inject <button>Click Me!</button>
    
    Good:
    <div><button>Click Me!</button></div>
    
  • Static vnode + props hoisting:

    A standard optimization to hoist vnodes and props that are static, allowing them to be cached and incurr no generation computational cost. This is best illustrated with a code sample:

    // Without static VNode hoist
    const render = () => patch(el, m('div', undefined, [`My favorite number: ${1 + 2 + 3}`]))
    render();
    render(); // Static VNode needs to be constructed twice
    
    // With static VNode hoist
    const _s = <div>Hello World!</div>
    const render = () => patch(el, _s)
    render();
    render(); // Static VNode is used twice and cached
    
    // Without static props hoist
    const render = () => patch(el, m('div', { id: `app${1 + 2 + 3}` }))
    render();
    render(); // Static props need to be constructed twice
    
    // With static props hoist
    const _s = { id: `app${1 + 2 + 3}` };
    const render = () => patch(el, m('div', _s))
    render();
    render(); // Static props are used twice and cached
    

Note: If you feel that this sort of paradigm has a future and are willing to meet those ends—I highly recommend you check out Million.js and try working on an implementation of a compiler yourself.

Conclusion

Million.js is far from being done, and there is a lot of work that needs to be done. I hope that this article has brought about a new perspective to think of the Virtual DOM as it progresses into the future. Feel free to comment any suggestions or lingering questions you may have!

Discussion (27)

Collapse
mindplay profile image
Rasmus Schultz • Edited

Svelte only is possible if API conditions are constrained, so that the code is predictive and therefore analyzable.

I don't understand. What is it that React or another virtual DOM library can do, that Svelte can't?

They both create and update DOM elements - both support loops, conditionals, components, state, and so on.

Something like Sinuous can even do precise updates without virtual DOM, like Svelte, without the compile step or static analysis, JSX being an optional convenience.

What is missing that makes virtual DOM still worthwhile?

Collapse
aidenybai profile image
Aiden Bai Author

Here's a clarification on that point: dev.to/aidenybai/comment/1gkdi

Collapse
mindplay profile image
Rasmus Schultz

Sorry, that didn't help me either. Can you give a concrete case/scenario where this difference would come into play?

Thread Thread
aidenybai profile image
Aiden Bai Author

In terms of concrete examples WHILE being very different, other people on dev.to might have more experise to provide a better example. Examples I can think of right off the bat might be:

Why does React have FC while svelte has .svelte files? Why does React have JSX while you can write practically html/css/js in Svelte? Why does React have hooks while svelte you can just declare a function and attach it using a directive?

Generally, I think it's because React depends on vdom, where JSX was basically made for constructing vnodes, FC was made to make components from vnodes, and hooks based off of FC.

Svelte literally compiles down to near imperative operations, meaning that their API design can be much more radical in the sense that it's constrained to certain API decisions, but unrestricted from JavaScript (kind of), while React is constrained to the API of JavaScript, but at the same time is restricted to JavaScript.

Thread Thread
mindplay profile image
Rasmus Schultz

I think that's a peculiar point of view. From my perspective, libraries like React or Sinuous aren't restricted to JavaScript, they're enabled by it - the fact that you get access to the entire language gives you degrees of freedom. Whereas something like Svelte or Vue apparently want to usurp or replace JavaScript rather than leverage it.

I mean, I get what you're saying - that e.g. Svelte isn't limited by JavaScript; they can change or enhance it as they see fit. There is more room for creativity. But there's also limitations to that approach - for example, React users naturally got access to Typescript, whereas Svelte users have to wait for the maintainers to make that possible. It kind of exists in it's own little bubble outside the JavaScript ecosystem proper. Whereas React and Sinuous exist within that sphere.

I've never been fond of tools that take away responsibilities from the language and platform and try to reinvent, subvert or displace them. I'm more impressed with projects that attempt to build within the constraints of the language and platform, than with projects that try build around it. Projects that build within are generally simpler, whereas projects that build around tend to be massively complex and have to reinvent the wheel. I like simple things.

Thread Thread
aidenybai profile image
Aiden Bai Author

I get where you're coming from, and that's a really good point. I also add that a library can be both enabled and restricted by a language.

I really like your perspective! I have a question though: From my personal experience, libraries like React do simple things really well, but as more and more complexity is introduced in your application, it becomes more and more unweildy. You could argue that increased abstraction could simplify complex use cases but make simple use cases very difficult. Sort of like a balancing act.

Thread Thread
mindplay profile image
Rasmus Schultz

You could argue that increased abstraction could simplify complex use cases but make simple use cases very difficult.

That sounds right (and I promise I'm not just trying to be Mr. Opposite here) but again, my experience is the opposite: more abstraction tends to favor simple use-cases - the ones the author was able to conceive of.

Big libraries tend to make easy things easier, which I find unnecessary and restrictive. Complex abstractions tend to be tailored towards a very specific "happy path" - whatever the author thought was "right", or whatever the pattern he got tired of repeating. It tends to get in the way of complex use-cases, if they don't happen to fit well with the abstraction.

On the other hand, they can of course speed the development of things that are a good fit - but that's what I would call "large" or "repetitive" use-cases, rather than really "complex", by which I would mean something that isn't your average everyday problem.

Where I grow tired of the big, complex libraries, is that, if a project is large or runs long enough, you will run into one of these cases that requires you to break outside the "happy path". At that point, you're going to "pay back" some of what you saved by making easy things easier.

So I'd just rather not. I like small, simple things that focus on the hard problems and don't try to make things easier if they're already easy. In many cases, the big abstractions are focused on reducing lines of code or making things more "elegant", which usually also means taking away choices and options, increasing (but hiding) overall complexity, and making things less transparent.

To provide you with a real world example, consider state management in React. When you first learn about it, it's just setState or useState, which seems really simple, until you realize it only really works well for control state, and not so well for application state. So you end up with props, component state, and some form of application state, maybe contexts, some patterns, or even more abstraction: a third-party library. Complexity spreads.

Now in contrast, consider the state management mechanism in Sinuous: observables, which work exactly the same everywhere. If you want component state, you create observables in your component - if you want application state, you create them somewhere outside your components. There's only one way to create and use state. You have to learn one pattern and use it correctly, and it's maybe a bit more verbose than some of your favorite state management approaches in React, but then that's it. You're done. It generalizes really well. You're not going to learn bit by bit how to solve this or that state problem, and you're not going to evaluate a bevy of state management libraries, or spend months becoming an expert at various state management patterns that might be better or worse for this or that.

Don't get me wrong, I'm not promoting Sinuous (which, full disclosure, I really do enjoy) and it might have drawbacks in other areas where React or Vue or whatever does better - but in this one area, it's just a beautiful example of "right amount of abstraction", which, in my book, tends to be "as little and as general as necessary".

Do you know Rich Hickey? If you haven't seen "Simple made Easy", go watch that. In my opinion, it's the most important video on software development there is. Completely changed my perspective, in ways that have made me much more productive, successful, and happy with software development. In many ways, successful software development is less about picking the right tools, and more about avoiding complexity - choosing simplicity. 😄♥️

Thread Thread
aidenybai profile image
Aiden Bai Author

Wow, thanks so much for this. Sinuous looks really cool! It looks like a really great hybrid between the Vue composition API (which I think was derived from the concept of observables) and lit-html, and I can see why you would think it's great. I've been looking around for a daily-driver library for a while, and this seems like it is a really good fit!

Side note: I was looking through the readme and I'm not sure if this was an inconsistency or a progression of timeline I missed:

Tagged templates transform the HTML to h calls at runtime w/ the html`` tag or, at build time with sinuous/babel-plugin-htm.

The html`` tag returns a native Node instance and the components are nothing more than simple function calls in the view.

I'll be sure to watch that video. Thanks so much for your time, this has made me realize I still have so much to learn and experience

Collapse
madza profile image
Madza

Good idea, tho I believe in the future more and more solutions will avoid virtual DOM 😉 Like Svelte, which is essentially a compiler 😉

Collapse
wobsoriano profile image
Robert

Like Solid too

Collapse
jfbrennan profile image
Jordan Brennan

And Vanilla.js

Collapse
chasm profile image
Charles F. Munat

What about SolidJS?

Collapse
aidenybai profile image
Aiden Bai Author

What part of SolidJS?

Collapse
chasm profile image
Charles F. Munat

SolidJS compiles down the way Svelte does and runs in the DOM. There is no virtual DOM. And it is extremely fast -- slower only than vanilla JS. And truly reactive, like RxJS. Is uses JSX as its templating language -- no VDOM needed. It can be used with Web Components. You can use native DOM events, etc.

What does Million.js give me that SolidJS lacks? What problem are you solving that SolidJS hasn't already solved? You might chat with @ryansolid about it if you're really interested in hearing an alternative view from the source.

Thread Thread
aidenybai profile image
Aiden Bai Author

Million.js isn't competing in the same field as SolidJS -- Million.js is a Virtual DOM engine, which libraries can be built off of (e.g. React), and SolidJS is a fully fledged library.

Million.js attempts to use techniques specified to reduce diffing (the main bottleneck of Virtual DOM) by assuming that the library developer will leverage the compiler to optimize.

Thread Thread
chasm profile image
Charles F. Munat

I see. I guess if you want to stick with React or Vue or whatever (they have huge ecosystems), then it does make sense. I'm not sure I see -- if the SolidJS approach works -- what the advantage is that makes building yet more VDOM-based libraries/frameworks. But maybe it's just me.

Collapse
hugekontrast profile image
Ashish Khare😎

Now I have a project I can study and tweak. Thanks for sharing!

Collapse
aidenybai profile image
Aiden Bai Author

Happy to help!

Collapse
zakiazfar profile image
Mohd Ahmad

can we use it with JSX

Collapse
aidenybai profile image
Aiden Bai Author

Yep! It's intended to be used with JSX

Collapse
zakiazfar profile image
Mohd Ahmad

how to use it with webpack, and can I use it with next js

Thread Thread
aidenybai profile image
Aiden Bai Author

You probably have to use a bundler

Collapse
freakcdev297 profile image
Phu Minh

Love the project, still waiting for more posts about Million :D

Collapse
imagineeeinc profile image
Imagineee

i have created something like this in the past but its just the conversion from json to html, no editing or other features

Collapse
aidenybai profile image
Aiden Bai Author

Nice!

Collapse
ianwijma profile image
Ian Wijma

What about no vdom and no compiler? 🤯

Collapse
aidenybai profile image
Aiden Bai Author

Virtual DOM in only runtime suffers from the pitfalls specified here: dev.to/aidenybai/million-js-the-fu....