Hi, everyone.
It's really nice to be here.
How's it - How's your day been goin'?
Great. Okay.
So hopefully this one is not too boring to be, you know, a talk at this time of the day
because I'm pretty sure you have seen a lot of talks already.
But just briefly introducing myself for those who don't know me.
My name is Evan You.
My Twitter handle is @youyuxi, which is my Chinese name.
I am an independent open source developer since 2016, so
that is to say, I've been working just on open source independently for three years now.
And I work primarily on Vue.js.
How many of you actually use it?
Alright, great.
So it started as a side project in 2013 and I've been working on it full time since 2016.
And over the years of working on frameworks, right, I've learned a lot of stuff.
And that kind of also gives me a lot of perspective on the internal design, some of the trade-offs, decisions that people make when it comes to building frameworks, right?
How many of you remember the days back in like 2013,
when there's probably a new JavaScript framework coming out every single day?
ToDoMVC has a list of like 40-50 frameworks, all building the same thing?
And Vue kind of started around that time where I was just looking at some of these existing solutions and trying to figure out,
what would I do if I were to build something like this?
Okay, but obviously, my ideas around what should be done has evolved over time and changed a lot.
But today, I'm going to be talking about some of these findings, specifically, front end framework design.
I bet a lot of you have been using a framework, even if you're not using Vue, you probably use React, Angular or some other framework.
It's hard to imagine building a complex front end application without something of that sort today, right?
You can probably still build things with vanilla JavaScript, but we know it's going to take a bit more time.
However, at the same time, like because of all these frameworks,
I guess most of you are also kind of tired of all these framework comparisons, right?
This is how I look like whenever I see yet another framework comparison article pop up on Medium,
like "The best seven new frameworks to using 2019".
I just like "ew".
Not because, you know, not because I built Vue, and I want people to use it,
or I don't want to see people hating on it, right?
But because most of the time, these articles just focus on things like GitHub stars,
or NPM download counts,
or Stack Overflow questions,
these stats that you can just easily find anywhere, anybody can just google them up, right.
But these stats, while still kind of useful to a certain extent, maybe for marketers, right.
But if you're trying to make a technical decision,
and you're trying to compete some of these relatively mature technologies,
these numbers are less and less relevant above a certain threshold, right?
Like most of these, you know, what we use in production today probably have over 10,000 stars on GitHub, right.
Above that threshold, like, really? Does it really matter how many stars, like, several thousand stars this library has got?
It doesn't really matter that much.
What you should probably care more about is some of the internal technical decisions, like what actually differs these frameworks.
So before we dig into that, take a step back and think about all the common goals of these frameworks.
We all are working towards the same goal -
all these framework authors are trying to provide you with something that helps you to build web apps as efficiently as possible,
then why do we have all these different competing ideas?
And is it a good thing or is it a bad thing? Right?
So why do we have so many different frameworks, each with a decent sizable following, right?
Because I believe, like, React Angular Vue, each have more than half a million users.
So why is that?
I think the source, the reason is that there isn't a single good versus bad spectrum for frameworks, right?
People tend to ask questions like, which framework is better?
Just stop asking that. Because it's not that simple to say framework A is better than framework B.
We know that software design is about trade offs. So are front end framework design.
There's so many trade offs, in fact, especially in the web world, right,
because the web is the platform with probably the most diverse set of use cases.
We build all kinds of crazy stuff on the web, from the simplest page possible to the most complex application you use every day.
So to accommodate all these use cases, frameworks have to make trade offs not just on a single spectrum.
We have to make trade offs on multiple spectrums, a lot of different areas. So -
And today, I'm going to be talking about some of these, hopefully, giving you some insight on what to look at,
but due to the time constraints I won't be able to dig in to all of these possible dimensions.
So I'll focus on a few of them - scope.
So this is essentially how much the framework is trying to do for you.
And then the render mechanism -
when you use the framework, how do you express your view structure? And how does the framework handle your code? How does it actually render stuff to the page?
And then there's the state mechanism - mutable versus immutable, dirty checking versus dependency tracking, reactivity versus simulated reactivity.
Actually, I won't have time to dig into this one, maybe this will be another talk at a later time.
So I'll go right into scope - how much a framework is trying to do for you.
So let's put it on a spectrum and you'll have one end where the framework, or the library, is doing intentionally smaller things
where on the other end, we have more monolithic things, like frameworks that try to provide as many features as possible.
Excuse me.
So some of the iconic examples, probably most people know about, right?
React kind of nicely falls onto the end of libraries, primitives.
And Angular falls on the side of frameworks providing you with a lot of abstractions, right?
So React exemplifies the philosophy where the library is focused on providing a very fundamental model for thinking about UI.
It tries to focus on providing you with these lower level good primitives on which you can build your own abstractions with.
The scope is intentionally small.
So that's why also why React has a very, very active ecosystem, right.
So along with the ecosystem, React is like the bazaar where the system is sort of organically built bottom up by the community around the core model that React has established.
On the other end, so Angular and alongside to some of the other frameworks like Ember, Aurelia are more like the cathedral, right.
They're designed top down, where most of the conceivable problems that the users will probably run into has been sort of considered during the design process -
form validation, animations - most of the common things you will encounter during the daily development, the framework tries to provide a solution for you.
And in order to do that, the framework has to be designed in a way that is very top down,
we have to think about everything how everything works together, fits together, from day one, right.
And this, this scope is intentionally big, because the goal of such a design is so that when you are trying to solve a problem,
you will be able to find the solution within the framework.
Now, we call this small scope versus big scope and it's not necessarily about good or bad.
Again, I want to emphasize that.
So some pros of the small scope is, there are fewer concepts to get started with and there's more flexibility.
So there's more userland opportunities, right, because the framework, the library is only providing with some really low level primitives.
So you have a component model, you have some props, you can pass props to it, and then you can return some virtual DOM tree.
That's pretty much it. That's pretty much it about React, right.
But on top of that, you can build arbitrary complex systems.
So React has a very, very active ecosystem.
People see that they have these very flexible tools and, you know, they just run wild with their creativity.
So there are a lot of great ideas coming out of the React community.
And in addition, being intentionally small in scope, also allows the team to have a smaller maintenance surface
so that they can focus on the things that they believe that matters, right.
So the team can focus on exploring new ideas, which is why react can spend such a long time working on things like
concurrent mode, suspense, React hooks, and all these interesting stuff they've been cranking out in the past few weeks or months.
Right, they've actually been working on it for a few years already so.
But this is because, you know, they have a small scope, so they can focus on these things.
So there are some cons of small scope, obviously.
So first is there's more plumbing work needed when solving inherently complex problems with simple concepts.
So there was a talk that I really liked, by Guy Steele, called "Growing a Language".
So during the talk, Guy set himself a rule so during that talk, he can only use single syllable words.
And if you want to use any words that has more than one syllables, he has to define it first with single syllable words.
So he is given a very limited list of primitive stuff to to be able to construct more complex ideas with.
So you can imagine how that talk went, right?
Before he speaks every sentence, he has to like, take out a few slides and define a bunch of words before he can proceed.
And that's kind of like building a really complex production grade app with very, very low level primitives -
you have to build a lot of abstract abstractions, to make yourself more efficient along the way.
Now, because of that, patterns naturally emerge over time, right?
When we say React is really simple to get started with,
we're kind of ignoring the fact that you sort of more or less have to learn Redux before you can consider yourself a real React developer.
And then you have to know about things like higher order components, or prod - render props,
and then now you have to learn hooks, and the many different ways of using CSS in JS, right.
So all of these patterns emerge over time, and they kind of become semi required, right?
If you don't know about these, you really can't call yourself a React developer.
And oftentimes, these things are not officially documented.
If you go to the React website, they're not going to tell you which CSS JS solution to use.
You have to do your own research and learn it.
So that's kind of the cons of having a really, really, you know, userland dominated system.
Now, the ecosystem can be moving too fast, and can lead to fragmentation and constant churn.
And I believe anyone who's been following along in the early days of the flux, sort of, every day there's a new flux implementation,
and then later on, everyday there's a new CSS JS implementation, right?
It's good and bad at the same time.
The good is there are always these new ideas coming out,
and we're trying to figure out what is actually the best way to do it.
The bad thing is, for people who are just trying to follow along and get something built,
you have this constant FOMO, right, you're always in fear of missing out on the next best thing.
So we're done with the small scope, cons, and let's talk about the pros of being large in scope.
The most obvious advantage is most common problems can be solved with building abstractions.
So if you're just trying to get something built, I just need a router, I need some animation,
I need to fetch some data with an HTTP client...
A framework like Angular provides everything you need to get that out of the door.
So you don't actually have to look elsewhere.
Just read the documentation, use the framework, and you can get things done.
Centralized design process ensures consistent and coherent ecosystem, right.
So you don't have to shop for different solutions when you're running into a specific problem.
You just look at the framework, see what the framework tells you to do, right?
Most likely, it has an opinion on it.
So you don't have to go dig into 10 different competing solutions and figure out which one fits your use case best.
Now, the cons of being large in scope is there is a higher upfront learning barrier.
In order to get the pixel onto the screen, the hoops you have to jump through to get there.
This can be a big deterrent for beginners, for people without proper, or
I wouldn't say proper - people without prior experience in dealing with back end languages.
Like if you've never used Java or C#, you've only learned HTML, CSS and JavaScript,
reading the Angular documentation is probably a pretty humbling experience, I would say.
It is still for me.
Now, it can be inflexible if built in solution doesn't fit the use case, right?
Sometimes you might just feel like, I wish I could do it the other way but
I don't have the option to swap it out.
And finally, a framework that is large in scope inherently makes introducing fundamental new ideas much more costly,
because there are so many associated pieces that need to work consistently together.
And when you try to change a fundamental idea,
it affects every component in your system.
So making changes is just a much harder thing.
Whereas if you think about in the React ecosystem,
because the core team is not really responsible for a lot of these solutions in the wild.
When you, say, introduce hooks, which made Redux more or less redundant, it's not eally a problem.
So there's that.
OK, so now this is where Vue kind of falls on.
But before we dig into what Vue is doing, I want to emphasize like this is not about like how Vue is better than both of these, right?
Because being in the middle doesn't necessarily indicate it's the best.
If you stretch this spectrum long enough, you zoom out long enough,
you'll see like, they are actually all kind of in the optimal zone already.
So it's just like we slightly differ on where we think the optimal point is.
So each choice also kind of fits the needs of different groups of users, right?
It's not like one thing that can fit all.
So what I call the way Vue takes on the scope problem is,
you probably know the tagline Vue is called - we call Vue the progressive framework.
So being progressive in scope means framework uses a layer design that allows features to be opted in, in a progressive manner.
That is to say, if you don't need routing, if you don't need state management, if you don't need a built step, even,
you can use Vue without any of those.
You just pull in Vue.JS onto your page, and you can instantly start working on something.
The learning barrier of jumping from beginner to getting a pixel on the screen
is shortened by removing anything that could be in your way in your first minute of learning.
So this is - the low entry learning barrier is really important for us because
one of the missions of Vue is to allow more people to get into web development,
to allow people to learn this and to focus on building things instead of
learning about a lot of concepts that might not be necessary for your current use case.
But we still have documented solutions for these common problems, like as your use case gets more complicated,
as you're building something more complex,
you realize, okay, I do need a router.
So you're looking to the documentation, you see, okay, Vue actually does provide a router, which I can use.
But at the same time, the router is not a required piece and you can actually implement your own if you want to,
because you see how Vue's router is built and it is cleanly decoupled from the core implementation,
so you kind of realize that you can build your own solution too, if you want to.
So it's not perfect, because we being in the middle actually means we are sharing the cons of both sides.
So first, although we are making the adoption incremental, we are still responsible for maintaining all these things.
So we share the same maintenance surface problem of big scope -
when we want to change something fundamentally, we have to make sure the whole ecosystem moves along with it,
so this maintenance burden is almost the same with big scope.
And at the same time, because we do provide these pre built solutions,
our ecosystem probably will be not as diverse as, say, React, the small scope system,
because small scope is inherently like leaving the problem to the community
whereas in our case, a lot of users will be happy with our solutions and they probably won't spend time on trying to figure out their own solutions.
So that is the scope problem.
So hopefully, you can kind of, you now have an idea of where sort of -
this is what I think is the most fundamental difference between React, Vue and Angular.
This exact positioning is what defines our different user bases.
And I think a lot of times, we're making intentional decisions, in terms of where we stand.
And we, as framework designers, we know that we are attacking different sectors
and I think that's a good thing because different developers needed different solutions.
And having major frameworks covering the whole spectrum ensures everyone gets what they want.
Okay, so now let's talk about our rendering mechanism, i.e.,
how a framework allows you to express your UI structure and how it renders stuff.
And primarily, this is actually a pretty complex spectrum spectrum in itself.
It's not just a single thing.
So it's like a multi dimension in itself, but let's briefly simplify it and think of it as JSX versus templates,
that is, dynamic render functions versus static string-based compilation-based Vue expressions.
And then there's expressiveness versus raw performance,
then there's runtime scheduling versus ahead of time optimizations.
Some people have really strong opinions on this,but I personally feel that they are more alike, inherently,
they're just different strategies of expressing the same underlying idea.
So it's more about the technical trade offs, right?
So on the left, the spectrum, obviously JSX, React, and all the React-like libraries that use some sort of virtual DOM, like, pre-act, Stencil, Infernal, right.
And then on the other side, template-based solutions, so I'll talk about Vue later but
the more representative template-based solution, Svelte,
and then there's Ember, so that logo is actually Glimmer's logo - so Glimmer is the rendering engine inside Ember -
and then Angular as well.
So these are primarily template-based, and they compile templates into relatively lower level instructions to render stuff.
Now, let's talk about the pros of the JSX and virtual DOM approach.
The most important reason people like JSX or virtual DOM is they have the full expressiveness of JavaScript -
you're not confined to a arbitrary syntax, you have a language at your disposal - you can
it's Turing complete, you can pretty much do anything you want.
So you can build arbitrarily complex logic, you know, components.
And it's really powerful and liberating, right?
A lot of people like React for this particular reason.
And it also allows you to treat the view as data.
When you render a component, it always returns something -
it returns the tree, the virtual DOM tree that represents the current state of a component.
And this data can be used for a lot of interesting purposes.
It gives you userland possibility to build, say, testing solutions.
You can take snapshots based on the virtual DOM, you can render it to alternative targets,
people have been doing things like rendering it to terminals, PDF, Canvas, WebGL, anything you can think of that you can render to.
Because the view is data, and you can do anything with data.
Now, the cons of virtual DOM, is that it is actually inherently expensive, right?
Think about it, when React first came out, a lot of people are like, isn't this going to be slow?
And React's answer was, "Yes, it's slow, but it's fast enough".
But still, like, from a pure technical perspective, you're doing a lot of unnecessary work.
Think about this simple template where the amount of work needed to just update that single message binding in there,
we have to walk through the whole virtual DOM tree, and diff diff diff diff diff -
you just have to recursively keep going down until somehow you update this in this process.
So standard virtual DOM diffing cost is relative to the total size of your view, rather than the number of nodes that may change.
Even if you only have one node that may change, the virtual DOM diffing algorithm doesn't know.
The reason is the dynamic nature of render functions makes it hard to optimize for.
By dynamic I mean, you can write code like this,
you can just use a for loop to construct a children array and then give it to your parent node.
And God knows what other things you can do.
Say you can create this parent node first, then mutate its children,
you can push additional elements into it.
The compiler won't be able to cover all the possible edge cases you can do with JavaScript,
because JavaScript is simply too dynamic.
There are a lot of attempts in this space but inherently, it's hard to provide safe optimizations in this way,
because there isn't simply enough assumptions you can make.
The more assumptions you can make about the user intention, the easier it is to optimize the code.
And with JavaScript, it's just really, really hard.
Now, finally, React's solution to this problem is to instead of focusing on making virtual DOM itself faster, how about making perceived performance better?
So it introduces runtime scheduling, concurrent mode, time slicing.
But this, having this runtime solution, having this whole fiber, kind of like almost managing your own stack,
like entering and exiting, rendering, all the stuff requires a heavy runtime.
So this means whenever you load React, you have to load all the code that is necessary for handling all these complex runtime scheduling stuff.
That's like several 20, 30KB of JavaScript, right.
And that, in turn, also makes your initial loading suffer a little bit.
And, on the other hand, if you are compiling, rendering code from a template,
usually it can produce far more direct render instructions with better raw performance.
The reason being that the template is, by definition a very, very constrained language -
you can only write template a certain way.
For example, when you write code like this,
we can instantly tell that there's no way the order of these p can ever change,
there's no way this ID can ever change,
there's no way this class can ever change,
the only thing that can change is this.
So being static, and very restrictive actually allows the compiler to make more assumptions about your intention.
And that gives it more room to perform optimizations.
Think about what Svelte does when it compiles your code.
Everything else is static, but only name could possibly change, so
this P is the update function in Svelte code, and the only thing it does is change when a name has changed,
and then update it if it has changed.
So compare this to all the things a virtual DOM diffing algorithm tries to do.
The difference is just orders of magnitude faster.
So depending on strategy, template compilation, or in general,compilation-based approach can also result in much lighter runtime baseline size,
because it doesn't need all the complex runtime scheduling to trying to make things look faster,
because it's already fast, right?
So Svelte can produce extremely lightweight output without having to require heavy, heavy baseline runtime to accommodate to all the possible runtime behavior.
Now, the cons of template compilation is obviously, you are constrained by the template syntax -
you lose the expressiveness of JavaScript.
So when you are trying to build a really complex component, you'll feel like
Oh, I wish I can do this in the template, but the compiler does not support it, right,
you're out of luck, there's no escape hatch if you go the full compilation route,
because the more lower level the compiler output is, the less likely you'll be able to actually hook into it and do your custom operations there.
It's like an opa compiler that you won't be able to dig into assembly with C.
that's just, you know, the way it is.
It's as if you won't be able to debug your assembly code with C.
So now, lighter runtime, lighter baseline runtime may also come at the cost of more verbose output per template, because
when you're trying to produce code that executes as efficiently as possible, sometimes you would have to encode more information in the output directly.
For example, the code that Svelte produces actually imperatively creates all the elements line by line, insert them one by one, and they have a separate function for updating them.
In comparison, virtual DOM based results, you would have to just have one line.
And that's just a single expression that returns the virtual DOM structure.
So, runtime compilation.
There's a runtime compilation cost if you compile on the fly.
So most likely, for production use case, you would require users to compile beforehand so that places a hard requirement on a build step,
which is something, you know, it's just inevitable.
You either compile on the fly or pre built, which then involves all the node.js tool chains, which we are more or less used to now.
But still, if you can avoid it, it would be really nice when people are getting started.
Ok. So again, Vue kind of falls in the middle.
Again, I want to emphasize, this is not saying Vue is the best.
But the unique thing about Vue's rendering mechanism is we have both virtual DOM and template compilation.
Vue actually compile those templates into virtual DOM under the hood.
So we kind of get the best of both worlds.
We have performance - the compilation step produces specially optimized, vdom render function.
I'll talk about this in a bit of more details later.
In ver 2.x, we actually haven't fully exploited this opportunity.
The current Vue 2.x virtual DOM performance is probably just average virtual DOM, typical virtual DOM performance.
But I'll talk a bit about what we're doing in 3.0 to make this much, much faster.
And then there's expressiveness.
You can actually skip the template layer, you can drop down into render functions and directly leverage JavaScript to perform arbitrarily complex logic.
So this gives you an escape hatch when you feel you're constrained by templates.
Now, the downside is, right, although we can be really fast, we're not
we can probably never be as fast as say, Svelte because Svelte, it's the vanilla - its output is pretty much vanilla JavaScript.
Whereas in order to be compatible with handwritten render functions, Vue still has to maintain a virtual DOM, so that cost is inevitable.
And on the other hand, it kind of also creates a split between like, "which one should I use" problem.
So a lot of users, although they can actually use render function, they'll probably just never use it.
So now let's dig into what we're doing in v3.0 to make Vue's template compile to virtual DOM run faster than normal virtual DOM.
This is what we already talked about, right, this template there's only one node that would change -
the ideal update path is just diff this message string directly.
No structure is static and never changes.
There's only one dynamic node.
So if we consider this template, it's a very, very simple case.
It becomes a bit more complicated when you have things like v-if, which is what we call a structural directive.
In JSX, this is pretty much an equivalent of a ternary - based on a condition you return different branches.
Now, this creates a dynamic node structure, because the node may be there or may not be there.
So to deal with this, a naive virtual DOM diffing algorithm would have to just assume the note list has changed and try to diff the two children arrays.
But if we try to split it apart and see v-if separates the template into two nested blocks -
the outer block, if we consider the v-if itself as a node, then the outer block has a static node content, node structure.
Inside the v-if block it also is static.
Now we have two static blocks, where within each block, you will have no need to diff any node order changes,
the only thing you need is a flat array of the things that could possibly change inside this block.
Similarly, for v-for - every v-for iteration, we consider it a static block.
So if you have more like a v-if inside v-for you just split further into nested blocks.
So we end up with something I call a block tree.
This is just a play on something we know, but the block tree is a nested block - blocks, because
within each block, you have a completely static node structure, so there's no need for any sort of like recursively going down and trying to diff two children lists.
Within each block, you just have one single flat array of the nodes that could possibly change.
On top of this, we also have additional organization hints on, for example, if a node has only a dynamic class binding,
we have a fast path that you just directly set the class and you can go ahead, you don't have to diff the props if you don't want to.
You don't have to, right.
So the before and after is pretty obvious.
For the same template, previously, we had to do all the full algorithm for different everything.
And afterwards, we just do a single, flat, there will be a single array containing just this node with a dynamic text.
And the only thing you need to do is compare whether the text has changed.
So we did a brief benchmark with this is a list of 1000 v-for iterations - within each block, you have around 12 DOM nodes, that is a total of 12,000 DOM nodes.
Within each iteration, you have four dynamic bindings, some classes, some text, so that is 4000, dynamic bindings on the page.
And we update all of these bindings, and we do 100 runs, take the average.
Current v2.6, per update is 36 milliseconds and current v3.0 prototype with the new compilation strategy it takes only like around 5.4 milliseconds.
So that is more than six times faster in this benchmark, right.
Thanks.
So note in this benchmark alone, probably your real app probably will have a different number but you know, more or less, it's going to be a lot faster.
That's the baseline.
And then state mechanism, I probably don't have time to really dig into this.
So that'll be another talk.
But to wrap up, where is the perfect balance point here, when you're trying to design a framework?
The question should probably be rephrased - does a perfect balance even exist?
And is a single perfect balance point even optimal for JS devs as a whole, right?
Because, like, all of us are trying to optimize for different things, when you're building something specific.
For example, like, for Svelte, it's advantage is it can produce extremely lightweight code when you're building something small.
And it is also extremely fast uses very, very little memory.
So it can be used on like, even embedded devices.
But if you're targeting use cases where, you know, you probably have a more complex use case,
you have more components,
you want the expressiveness of JavaScript but you also want a bit more performance from the templates,
you can probably go with Vue.
And then if you don't really care that much about extreme performance but you're like "I like React's ecosystem",
you can go with React.
There's all these options you can pick.
So I think it's nice that the framework landscape is like a multi-dimensional space with multiple ever moving entities,
like think of each framework as an entity, trying to look for the balance point it believes is the best.
We're all trying to figure out, what's the best way to do things.
And there will be multiple of us always, right.
So as developers, you're like floating around in between these entities.
You're dragged towards one of them by their gravity.
And sometimes you may just, you know, switch around, you hop around and try to figure out which one is best for you.
I think that's a good thing, right?
But as a user, right, trying to navigate this multi-dimensional space can be daunting.
But if you want to pick up the framework properly the hard way,
then you have to understand some of these internal trade offs the frameworks are making.
You have to be aware of which direction this framework is heading towards and whether it aligns with what you are prioritizing in the thing you're trying to build.
So hopefully this talk has shed some light on that topic and could help you when you are trying to pick between frameworks in the future or
tell other people how they should pick a framework.
Thank you.