Over the past year, the Vue team has been working on the next major release of vue.js, which we hope to release in the first half of 2020. (This is still a work in progress at the time of writing.) The major release of Vue was finalized at the end of 2018, when the Vue 2 codelbase was two and a half years old. That may not sound long in the life cycle of general-purpose software, but the front-end environment has changed dramatically in that time.
There are two main considerations that lead us to the new major (and rewrite) version of Vue: First, the new JavaScript language features are generally available in major browsers. Second, over time, design and architectural issues in the current code base have come to light.
Why rewrite
Take advantage of new language features
With the standardization of ES2015, JavaScript (formally known as ECMASc RIpt, or ES for short) has been significantly improved, and major browsers are finally starting to provide decent support for these new features. Some, in particular, offer us the opportunity to dramatically improve Vue functionality.
The most notable of these is Proxy, which allows the framework to intercept operations on objects. The core functionality of Vue is the ability to listen for changes to user-defined state and update the DOM in a reactive manner. Vue 2 achieves this reactivity by replacing properties on state objects with getters and setters. Switching to a proxy will allow us to remove existing limitations of Vue, such as the inability to detect new attribute additions and provide better performance.
However, proxies are native language features and cannot be fully populated in older browsers. To take advantage of it, we knew we would have to adjust the browser support scope of the framework, which was a major breakthrough and could only be released in a new major release.
Resolving architectural issues
Solving these problems in the current code base would require a lot of risky refactoring, almost equivalent to rewriting. In the course of maintaining Vue 2, we accumulated a number of difficult issues due to the limitations of the existing architecture. For example, the template compiler is written in a way that makes proper source mapping support challenging. Similarly, while Vue 2 technically allows you to build higher-level renderers for non-DOM platforms, we had to derive a code base and copy a lot of code to do this. Solving these problems in the current code base would require a lot of risky refactoring, almost equivalent to rewriting.
At the same time, we accumulated technical debt in the form of implicit coupling between the internals of various modules and floating code that didn’t seem to belong anywhere. This makes it more difficult to understand portions of the code base in isolation, and we have noticed that contributors rarely feel confident about making significant changes. The rewrite will give us the opportunity to rethink the code organization by taking these factors into account.
Initial prototyping stage
We began prototyping Vue 3 at the end of 2018 with the initial goal of validating solutions to these problems. At this stage, our main focus is to lay a solid foundation for further development.
Switch to the TYPESCRIPT
Vue 2 was originally written in pure ES. Not long after the prototyping phase, we realized that a type system would be very helpful for a project of this size. Type checking greatly reduces the chance of introducing unexpected errors during refactoring and helps contributors feel more confident about making important changes. We went through Facebook’s traffic type check as it could be gradually added to the existing Plain ES project. Traffic helped up to a point, but we didn’t get much out of hope. In particular, the ever-changing changes make upgrading painful. Support for integrated development environments is also less than ideal compared to TypeScript’s deep integration with Visual Studio Code.
We’ve also noticed that users are increasingly using both Vue and TypeScript. To support their use cases, we must compose and maintain TypeScript declarations separately from the source code that uses a different type system. Switching to TypeScript will allow us to automatically generate declaration files, reducing the maintenance burden.
Decouple internal encapsulation
We also adopted the Monorepo setup, where the framework consists of internal packages, each with its own separate API, type definitions, and tests. We want to make the dependencies between these modules more explicit, making it easier for developers to read, understand, and change all modules. This is key to our efforts to lower project contribution barriers and improve their long-term maintainability.
Set the RFC flow
By the end of 2018, we had a working prototype using a new reactive system and a virtual DOM renderer. We have verified the internal architectural improvements we want to make, but only included a draft of the API changes for the public. Now it’s time to turn them into concrete designs.
We know we have to do this early and carefully. The widespread use of Vue means that breakthrough changes can result in significant user migration costs and potential ecosystem fragmentation. To ensure that users can provide feedback on significant changes, we adopted the RFC (Request for Comments) process in early 2019. Each RFC follows a template, with sections focusing on motivation, design details, trade-offs, and adoption strategies. Because this process takes place in the GitHub repository and the proposal is submitted as a request request, the discussion unfolds naturally in the comments.
The RFC process has proved enormously helpful as a framework for thinking through all aspects of potential change, engaging our community in the design process, and submitting thoughtful functional requirements.
Faster and more small
Performance is critical to a front-end framework. Performance is critical to a front-end framework. Despite the excellent performance of Vue 2, rewriting offers the opportunity to evolve further by experimenting with new rendering strategies.
Overcome the virtual DOM bottleneck
Vue has a fairly unique rendering strategy: it provides htML-like template syntax, but compiles templates into rendering functions that return a virtual DOM tree. The framework determines which parts of the actual DOM need to be updated by recursively traversing two virtual DOM trees and comparing each property on each node. Due to the advanced optimizations that modern JavaScript engines perform, this somewhat brute-force algorithm is usually fast, but updates still involve a lot of unnecessary CPU work. The inefficiencies are especially evident when you look at templates with a lot of static content and only a small amount of dynamic binding (the entire virtual DOM) and still need to recursively walk up the tree to see what has changed.
Fortunately, the template compilation step gives us the opportunity to statically analyze the template and extract information about the dynamic parts. Vue 2 does this to some extent by skipping static subtrees, but more advanced optimizations are difficult to implement due to the simplistic compiler architecture. In Vue 3, we rewrote the compiler with the appropriate AST transformation pipeline, which allowed us to write compile-time optimizations in the form of transformation plug-ins.
With the new architecture, we wanted to find a rendering strategy to minimize overhead. One option would be to abandon the virtual DOM and generate imperative DOM operations directly, but that would eliminate the ability to write the virtual DOM rendering functionality directly, which we find very valuable for power users and library authors. Plus, it would be a huge breakthrough.
Second, the best approach is to eliminate unnecessary virtual DOM tree traversals and attribute comparisons, which tend to incur the greatest performance overhead during updates. To do this, the compiler and the runtime need to work together: the compiler analyzes the template and generates code with optimization prompts, while the runtime picks up the prompts and takes as fast a path as possible. There are three main optimizations:
First, at the tree level, we note that the node structure remains completely static without template instructions to dynamically change the node structure (e.g., V-if and V-for). If we divide the template into nested “blocks” separated by these structural instructions, the node structure within each block becomes completely static again. When we update nodes in a block, we no longer need to recursively traverse the tree – dynamic bindings within the block can be tracked in a flat array. This optimization avoids most of the overhead of the virtual DOM by reducing the amount of tree traversal we need to perform by an order of magnitude. \
Second, the compiler actively detects static nodes, subtrees, and even data objects in the template and promotes them beyond the render function in the generated code. This avoids recreating these objects on every render, greatly increasing memory usage and reducing the frequency of garbage collection. \
Third, at the element level, the compiler also generates an optimization flag for each dynamically bound element based on the type of update that needs to be performed. For example, elements with dynamic class bindings and many static attributes will receive a flag indicating that only class checking is required. The runtime takes these hints and takes a dedicated fast path.
CPU time, which is the time spent performing JavaScript calculations, does not include browser DOM operations.
Taken together, these techniques greatly improved our rendering update baseline, with Vue 3 sometimes taking less than one-tenth the CPU time of Vue 2.
Minimize package size
The size of the framework also affects its performance. This is the sole concern of a Web application, because the asset needs to be downloaded dynamically and the application is interactive only after the browser has parsed the necessary JavaScript. This is especially true for single-page applications. While Vue has always been lightweight – Vue 2’s runtime size is about 23 KB gzip compressed – we noticed two issues:
First, not everyone uses all the features of the framework. For example, apps that never use transitions will still have to pay for the download and parsing of transition-related code.
Second, the framework grows indefinitely as we add new features. This makes the bundle size out of proportion when we consider the tradeoff of adding new functionality. Therefore, we tend to include only features that will be used by the majority of users.
Ideally, users should be able to remove unused framework functionality at build time – also known as “crumbling” – and pay only for what they use. This will also allow us to release some features that users will find useful without increasing the cost of the payload for the rest of the users.
In Vue 3, we achieved this goal by moving most of the global APIS and internal helpers to the ES module export. This allows modern bundlers to statically analyze module dependencies and remove code associated with unused exports. The template compiler also generates tree-shaker friendly code that imports the helper for that feature only if it is actually used in the template.
Some parts of the framework will never wobble because they are essential for any type of application. We refer to these essential measurements as reference dimensions. Despite many new features, the base size of Vue 3 is about 10 KB compressed, less than half that of Vue 2.
Meet scale requirements
We also want to improve Vue’s ability to handle large applications. Our original Vue design focused on lower barriers to entry and a gentle learning curve. But as Vue became more widely adopted, we learned more about the requirements of projects that contained hundreds of modules and were maintained by dozens of developers over time. For these types of projects, a type system like TypeScript and the ability to cleanly organize reusable code are critical, and Vue 2’s support in these areas is not ideal.
In the early stages of designing Vue 3, we tried to improve TypeScript integration by providing built-in support for writing components using classes. The challenge is that many of the language features we need to make our classes usable, such as class fields and decorators, are still proposals and may change before they are formally added to JavaScript. The complexity and uncertainty involved makes us wonder if it really makes sense to add a Class API that doesn’t provide anything other than better TypeScript integration.
We decided to look at other approaches to the scaling problem. Inspired by React Hooks, we considered exposing lower-level reactivity and component lifecycle apis to achieve a more free-form way of writing component logic called the Composition API. Instead of defining components by specifying a long list of options, the Composition API allows users to write and reuse stateful component logic as freely as they write functions, while providing excellent TypeScript support.
We are very excited about this idea. Although the Composition API is intended to solve a specific class of problems, technically you can only use it when writing components. In the first draft of the proposal, we got ahead of the curve and hinted that we might replace the existing Options API with the Composition API in a future release. This led to a lot of opposition from community members, which allowed us to gain valuable lessons in clearly communicating long-term plans and intentions, as well as understanding the needs of our users. After listening to feedback from our community, we completely redesigned the proposal to make it clear that the Composition API would complement and complement Options. The revised proposal received was more positive and many constructive suggestions were received.
Seek a balance
The diversity of the developer information correspond to the diversity of use cases In more than one million developers Vue, only the basic knowledge of HTML/CSS beginner, professionals from the jQuery migration, migration of veterans from another frame, are looking for the front-end solutions backend software architect engineers and large-scale processing software. The diversity of developer profiles corresponds to the diversity of use cases: some developers may want to extend interactivity to older versions of the application, while others may be working on one-off projects with rapid processing but limited maintenance requirements. Architects may have to deal with large, multi-year projects and a fluctuating development team throughout the life of the project.
Vue’s design is constantly influenced and inspired by these needs as we seek to strike a balance between various compromises. Vue’s slogan, “Progressive framework,” encapsulates the layered API design that results from this process. Beginners can easily learn using CDN scripts, HTML-based templates, and the intuitive Options API, while experts can use the full-featured CLI, rendering capabilities, and Composition API to tackle ambitious use cases.
There is still a lot of work to be done to achieve our vision – most importantly, updating the support libraries, documentation and tools to ensure a smooth migration. As we continue to work on this in the coming months, we can’t wait to see what the community will create with Vue 3.
About the author
Evan You is an independent open source developer. He is the creator and project leader of vue.js, one of the most widely used front-end frameworks today.