Vue.js is an up and coming framework in the the front-end Javascript ecosystem. Experienced software developers must consider how Vue is built in order to understand how it scales. Moving forward we will explore the internals of Vue and see what the constraints are for building fast, scalable applications.
There are two essential concepts to consider when trying to grasp the workings of Vue, especially when concerned about performance for large applications. First is how it renders its components (small, reusable pieces of an application). Armed with that, we can understand why Vue is so fast when re-rendering parts of your application, but also what possible memory issues can pop up at scale.
Secondly, we will take a look at some of what Vue does internally to optimize itself. Its primary tactic is to hoist parts of the virtual DOM into constant variables in order to avoid rendering them during virtual DOM updates. Seeing how Vue makes the distinction between what should and shouldn't be hoisted helps us understand how we can build applications such that Vue has an easier time optimizing.
An Overview of the Vue Rendering Engine
First let us take a look at how the rendering engine updates its components. The Vue rendering engine is quite different from the rendering engine of frameworks React and Angular. You might be familiar with what React refers to as the virtual DOM. Vue also has the same concept in its internals. The virtual DOM is essentially a tree data structure that contains an abstract representation of the components that will map to dom elements. Since it is a tree, the structural optimization strategies of tree-like data structures work well. However the relationship between nodes are a little different from virtual DOM implementations like React.
React has a rendering structure in its virtual DOM that behaves somewhat like this:
In React, as seen above, when data changes in a parent node in the tree, all the children are re-computed. This means that React has a chance of wasting time processing children that have not changed. What makes Vue different is that every node is directly aware of all its subscribers. As a result the rendering looks like this:
Vue does a much better job of changing only what needs to be changed. This is possible through Vue’s way of updating related components. Each component keeps a record of the related components that depend on its internal data. When data changes in a Vue component all of its subscribers (related components) are immediately notified and updated. Since every component knows who needs its data immediately, data updating is quick ( effectively updated in O(1) time ).
However, with most things related to optimization where, there is less computation, there is more memory use. Since each node is aware of its subscribers, Vue developers need to be aware of the memory profile of their application. The good news is that principles that work to optimize these kind of abstractions still work in Vue. Organizing the components so that their relationships can be limited will reduce the number of relationships. One way data flow is still the way to go in frameworks like Vue. Hoisting important information high in your Vue application structure mirrors the way a virtual DOM is constructured, and makes clear the significance of certain components and data in the application.
Once we understand how Vue renders, we are then careful about how the relationships between our components are defined. However, since relationships are defined by the application, Vue must do something else internally to try and optimize. Earlier it was mentioned that hoisting parts of the virtual DOM into constants is Vue's primary optimization tactic. Lets explore why and how that decision is made.
Going Deeper By Considering Vue's Internal Optimization.
In order to optimize Vue, we must understand how Vue optimizes the components and nodes in its virtual DOM. Taking a look at how the compiler works gives us that deeper understanding.
The primary tactic Vue uses to optimize components is taking components that are static away from the virtual DOM and hoisting them in constants. Since these components do not change, they do not need to be re-rendered.
There are three different types of nodes in Vue’s virtual DOM Abstract Syntax Tree (will be referred to as AST from henceforth).
- ASTExpression
- ASTText
- ASTElement
ASTExpression's functions are conditional operations attached to the DOM that are later processed to apply the computation. ASTText are simple components containing mostly text.
ASTElement are the primary pieces carefully considered in order to make static components that are optimized. Several things affect how optimizable the ASTElement are. Whether they have conditions and whether the node is a direct child of a for container tag are things that plays a role in whether a component can be optimized. The next section will talk at length about ASTElement and how they affect how we think about writing Vue applications.
ASTElement
ASTElement are a more general albeit less abstract data structure that exists in the Vue virtual DOM AST. The optimizability (the ability to make the ASTNode static) is determined by the isStatic function.
function isStatic(node) {
if (node.type === 2) {
// expression
return false;
}
if (node.type === 3) {
// text
return true;
}
return !!(
node.pre ||
(!node.hasBindings && // no dynamic bindings
!node.if &&
!node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey))
);
}
The isBuiltInTag and IsPlatformReservedTag are simply functions that check if the tag is present in the node is disallowed by the Vue compiler. The built in tags are component and slots. These tags serve a general purpose within the Vue compiler so they cannot be specifically hoisted in any particular way. Platform reserved tags are HTML tags that are native in the DOM. As a consequence the platform reserved tags do not get hoisted into constants. For example, its possible to have more than a single div in an application, so hoisting a div has no advantage.
Most of what the isStatic function does involves considering the presence if and for property in the ASTToken. It also further consider whether a token is a child of a for loop. To take advantage of the knowledge that they are not optimized, we have to think of where we place the condition and iterative code in the application. Since these kinds of tokens cannot be hoisted it is best you do what you can to ensure that they are optimized. If you have a for loop iterate over large components, you will not allow the compiler to optimize large chunks. If there are sections of your nested block that you can afford to statically render then you should. For example,
Original
<p v-for="item in items">{{ item }} is Large </p>
Optimized
<label-component> Large Items</label-component>
<p v-for="item in items">{{ item }}</p>
Since the label component contains only static text, it's easy to hoist that label into a constant that avoids the re-rendering process.
The eventual consequence of the kind of optimization is that v-for components are then nested deeper and deeper in the virtual DOM tree. This allows most of the static components to be hoisted early, and even give your application a seemingly faster load time than a quantitative assessment of the application speed would infer.
The exact same thinking can be applied to v-if components. Again, we try and remove what we understand can possbily be static in the application any way we can. For example:
Original
<custom-component v-if="ok">
<title>Title</title>
<paragraph>Paragraph 2.1</paragraph>
</custom-component>
<custom-component v-else>
<title>Title</title>
<paragraph>Paragraph 2.2</paragraph>
</custom-component>
Optimized
<tittle>Title</tittle>
<custom-component v-if="ok">
<paragraph>Paragraph 2.1</paragraph>
</custom-component>
<custom-component v-else>
<paragraph>Paragraph 2.2</paragraph>
</custom-component>
Last but not least, since platform reserved tags are not optimized, optimizing condition or loops with them has to have that in mind. For example, these have the same consequence:
Original
<custom-component v-if="ok">
<h1>Title</h1>
<paragraph>Paragraph 3.1</paragraph>
</custom-component>
<custom-component v-else>
<h1>Title</h1>
<paragraph>Paragraph 3.2</paragraph>
</custom-component>
(Seemingly) Optimized
<h1>Title</h1>
<custom-component v-if="ok">
<paragraph>Paragraph 3.1</paragraph>
</custom-component>
<custom-component v-else>
<paragraph>Paragraph 3.2</paragraph>
</custom-component>
There is one less h1 tag in the second, but since it's a platform reserved component it still gets re-rendered when the data in a Vue component is updated.
Conclusion
This completes the exploration of how Vue handles some optimizations. It should give insight into what Vue does to provide its performance, simplicity and structure. It should also give you an idea on how to code so that your application will use less memory and improve performance. Building components so their data subscribers are limited reduces the memory profile of the application. Since default HTML tags cannot be optimized, it might be best to have these HTML tags as near to the bottom of the Vue virtual DOM tree (i.e deeply nested in the constructed template). Finally, since we are aware that repeatedly generated components are not optimized ( those that are directly under loops) we can try and reduce the complexity and size of such repeated component in order to save resources. I hope that discovering these things was as interesting for you, as it was for me.
Want to learn more about Vue? Find your gateway to learning Vue.