Yu Wrote this long article detailing the design process of Vue 3 ahead of its official release earlier this year. The top of the Front has translated the full text below to help you better understand the story behind Vue 3.
For the past year, the Vue team has been working on the next major release of vue.js, and we hope to release it in the first half of this year (it was still in progress when this article was written). The idea for the new version of Vue took shape in late 2018, when the Vue 2 codelbase was already two and a half years old. That may not seem that long in comparison to the life cycle of general-purpose software, but the front-end world has changed since then. When we updated (and rewrote) the major version of Vue, we considered two factors: first, the level of support for the new JavaScript language features in major browsers; Second, there are some design and architectural issues in the current code base that have been exposed over time.
Why rewrite
Take advantage of new language features
With the adoption of the ES2015 standard, JavaScript (formerly known as ECMAScript, or ES) has received major improvements, and mainstream browsers are finally starting to support these new features well. Some of these features allow us to significantly increase Vue’s capabilities. The most notable of these is Proxy, which provides the framework with the ability to intercept operations on objects. One of the core features of Vue is to listen for changes in user-defined state and update the DOM in a responsive manner. Vue 2 implements this responsiveness by replacing getters and setters for state object properties. By moving to Proxy, we were able to address many of Vue’s current limitations (such as the inability to detect new attributes) and provide better performance. But Proxy is a native language feature and cannot provide full polyfill in older browsers. To do this, we needed to change the scope of browser support for the new framework — a disruptive change that can only be implemented with a new major release.
Resolving architectural issues
Fixing these problems on an existing code base would require a lot of high-risk refactoring, almost a rewrite.
In the course of maintaining Vue 2, we accumulated many problems that were difficult to solve due to the existing architecture. For example, the template compiler is written in a way that makes it difficult to achieve good source mapping support. In addition, while Vue 2 technically supports building advanced renderers aimed at non-DOM platforms, to achieve this support required forking a code base and copying a lot of code. Fixing these problems on an existing code base requires a lot of high-risk refactoring that amounts to a rewrite. At the same time, we built up a lot of technical debt by creating a lot of hidden coupling between many internal modules and bits of code that seemed to have nowhere to go. It is now difficult to understand the meaning of one part of the code base in isolation, and we have noticed that contributors rarely feel confident enough to make breakthrough changes. By rewriting, we were able to rethink the way the code was organized based on these issues.
The early prototype stage
We started prototyping Vue 3 in late 2018, with the primary goal of proving a solution to the above problems. At this stage, we mainly lay a solid foundation for the subsequent development work.
Steering TypeScript
Vue 2 was originally written in pure ES. Soon after the prototype phase began, we realized that a type system would be very useful for such a large project. Type checking can greatly reduce the chance of introducing unexpected bugs in your refactoring, and can increase the confidence of contributors when making breakthrough changes. We adopted Facebook’s Flow type checker because it can be incrementally added to an existing pure ES project. Flow worked, but our revenue was not as good as expected; In particular, it has so many major changes that upgrading is a pain. It also doesn’t support integrated development environments as well as TypeScript’s deep integration with VS Code. We’re also seeing more and more users combine Vue with TypeScript. To support their usage scenarios, we need to write and maintain a separate set of TypeScript declarations outside of the source code that use a different type system. By moving to TypeScript, we can automatically generate declaration files, reducing maintenance costs.
Decouple the inner package
We also adopted a monolithic warehouse scenario, where the framework is made up of many internal packages, each with its own independent API, type definition, and test case. We want to make the dependencies between modules more obvious and make it easier for developers to read, understand, and modify all of these dependencies. This is a key part of our efforts to lower the contribution threshold for projects and improve their long-term maintainability.
Develop RFC process
In late 2018, we had a prototype with a new responsive system and a virtual DOM renderer. We validated the planned internal architecture optimizations, but only sketched out ideas for external-facing API changes. Now it’s time to turn these ideas into concrete designs. We know this is a step that needs to be taken carefully early on. The widespread popularity of Vue means that significant changes can result in significant migration costs for users and fragmentation of the ecosystem. To allow users to submit feedback on significant changes, we developed an RFC (request for Comments) process in early 2019. All RFCS have a template that includes motivations, design details, trade-offs, and adoption strategies. This process takes the form of submitting the proposal as a pull request in a Github repository, where it is naturally discussed in the comments. The results show that this RFC flow is very useful. As a mental framework, it forces us to think through all aspects of a potential change and allows the entire community to participate in the design process and submit well-thought-out feature requirements.
Faster, smaller
The performance of the front-end framework is critical.
The performance of the front-end framework is critical. While Vue 2 already provided competitive performance, this rewrite gave us the opportunity to experiment with new rendering strategies to further improve performance.
Break the bottleneck of virtual DOM
Vue has a unique rendering strategy: it provides an HTML-like template syntax, but compiles the template into a rendering function that returns a virtual DOM tree. The framework recursively traverses the two virtual DOM trees, comparing all the attributes of each node to determine which parts of the DOM are updated. This relatively violent algorithm is generally fast, thanks to so many advanced optimizations implemented by modern JS engines. But the update process still involves a lot of unnecessary CPU work. Updating is especially inefficient when your template has a lot of static content but only a few dynamic bindings — you still have to recursively traverse the entire virtual DOM tree to figure out what to update. Fortunately, this template compilation step allows us to statically analyze the template and extract information about the dynamic parts. Vue 2 does this to some extent by skipping static subtrees; But because of the oversimplified compiler architecture, more advanced optimizations are difficult to achieve. In Vue 3 we rewrote the compiler to include a suitable AST Transform pipe, allowing us to do compile-time optimization in the form of a Transform plug-in. Now that we have a new architecture, we want to find a rendering strategy that minimizes the extra overhead. One option is to discard the virtual DOM and generate the imperative DOM operations directly, but this would lose the ability to write the virtual DOM rendering functions directly, which we find very valuable for advanced users and library authors. Also, it would be a big change with a big impact. The next option is to get rid of unnecessary virtual DOM tree traversal and attribute comparison, which is the most performance expensive part of the update process. To do this, the compiler and the runtime need to work together: the compiler analyzes the template and generates code with optimization clues, while the runtime picks up the clues and selects the fastest path. There are three major optimizations here: First, at the tree level, we noticed that the structure of the node remained completely static without template instructions (such as V-if and V-for) that dynamically adjusted the structure of the node. If we split the template into nested “blocks” based on these structured instructions, the node structure in each block would also remain static. When we update nodes in a block, we no longer recursively traverse the tree — dynamic bindings within the block can be tracked in a flat array. This optimization greatly reduces the number of trees that need to be traversed and avoids most of the virtual DOM tree overhead. Second, the compiler aggressively detects static nodes, subtrees, and even data objects in the template and extracts them out of the rendering function in the generated code. This avoids the need to recreate these objects every time you render, greatly reducing memory footprint and reducing the frequency of garbage collection. Finally, at the element level, the compiler generates an optimization flag for each dynamically bound element based on the type of update it needs to make. For example, if an element has a dynamic class binding and some static attributes, it will get a flag indicating that only class checking is required. The runtime retrieves these flags and selects the fastest path.
CPU time: refers to the time consumed by JavaScript operations, not including browser DOM operations.
Combined with these optimizations, our rendering update speed improved significantly, with Vue 3 using less than one-tenth the CPU time of Vue 2 in some scenarios.
Reduced package volume
The size of the framework also affects its performance. This is unique to Web applications because assets need to be downloaded online and applications need to wait until the browser has parsed the necessary JavaScript code before they can interact. Single-page applications are particularly conflicted in this regard. Although Vue has always been a relatively lightweight framework — Vue 2 has a runtime size of 23KB (gzip compressed), we noticed two issues: First, not everyone needs the full functionality of the framework. For example, applications that never require transitional features still need to download and parse the code. In addition, we are constantly adding new features to the framework, and the framework is constantly getting bigger and bigger. This makes the package size weighting very important when weighing the pros and cons of a new feature. As a result, we tend to only include features that most users will use. Ideally, users can remove features from the framework they don’t need at build time (aka “tree shaker”) and keep only the ones they do. This way we can add features that only some users will use without burdening other users with application volume. In Vue 3, we achieved this goal by moving most of the global APIS and internal helpers into the ES module export. This allows modern packagers to statically analyze module dependencies and remove code associated with unused exports. The template compiler also generates code suitable for tree shaking and imports helpers only for features that are actually used by the template. There are parts of the framework that can never be tree-shaken because they are important for all application types. We refer to this volume of code that cannot be discarded as the baseline size. While Vue 3 adds many new features, its baseline size is only about 10KB (after gzip), less than half that of Vue 2.
Meet expansion requirements
We also want to improve Vue’s ability to handle large-scale applications. When we first designed Vue, our main focus was to lower the barrier to entry and smooth out the learning curve. But as Vue became more popular, we saw more and more project requirements expand over time to include hundreds of modules, requiring dozens of developers to maintain. For this type of project, a type system like TypeScript and its ability to provide cleanly organized, easily reusable code are essential, but Vue 2’s level of support in these areas is less than ideal. In the early design stages of Vue 3, we tried to integrate TypeScript with built-in support for writing components using class. The problem here is that many of the language features needed to make a class usable, such as Class Fields and Decorators, are still in the proposal stage and could change in the final release. The resulting complexity and uncertainty leads us to question whether the Class API is really appropriate, since it only improves TypeScript integration a bit. So we decided to explore other ways to solve the scaling problem. Inspired by React Hooks, we came up with an API that exposes the underlying responsiveness and component lifecycle to provide a more flexible way to write component logic, the Composition API. Instead of defining components with a long list of configurations, the Composition API allows you to define, compose, and reuse component logic as if you were writing functions, while providing full TypeScript support. We like the idea very much. Although Composition apis are designed to solve specific types of problems, they can also be used for pure component development. We got carried away in the first draft of the proposal, hinting that we might replace the current Options API with Composition API in future releases. This generated a tremendous backlash from community members and taught us an important lesson about communicating our long-term plans and direction with the community and understanding the needs of our users. After listening to community feedback, we completely reworked the proposal and made sure that the Composition API was just icing on the cake and complementing the Options API. Feedback on the new version of the proposal has been much more positive, and we have received a lot of constructive comments.
To grasp the balance
Diversity of developers means diversity of usage scenarios.
More than a million developers use Vue today, including beginners who know only a little HTML/CSS, experts who have worked their way up from jQuery, veterans who have migrated from other frameworks, back-end engineers looking for front-end solutions, and architects who design large-scale software. Diversity of developers means diversity of usage scenarios: some developers may want to improve the interactive experience of an old project, while others may want to quickly develop a low-cost one-off project; Architects may have to deal with large, long-term projects and changes in development team members over the life of the project. Vue’s design continues to change and evolve in response to these needs, and we try to balance many trade-offs. Behind Vue’s slogan “progressive framework” is the layered API design that emerged from this process. Beginners can get started with CDN Script, HTML-based templates, and the intuitive Options API. Experts can handle complex requirements through full-featured CLI, rendering functions, and Composition apis. There is still a lot of work to be done to achieve our vision, the most important of which is updating the support libraries, documentation, and tools to ensure a smooth migration. We’ll continue to work on this in the coming months, and we can’t wait to see what the community can do with Vue 3.