[Translation] Vue3 design process

At around 3:30 in the morning today, You Yuxi published an article on his Weibo.
Of course, the boss is in another time zone. Our early morning should correspond to the afternoon in the time zone where the epidemic is the most severe.

Original link: https://increment.com/frontend/making-vue-3/

Lessons learned from refactoring the new version of Vue.js

In the past year, the Vue team has been working on the next major version of Vue.js, and we hope to release this version in the first half of 2020. (At the time of writing, this work is still in progress). The main version of Vue was formed at the end of 2018, when the Vue 2 code base was two and a half years old. It may not sound long in the life cycle of general-purpose software, but during this period, the front-end environment has undergone tremendous changes.

There are two main considerations that led us to develop a new major version of Vue (and rewrite it): First, mainstream browsers generally provide new JavaScript language features. Secondly, over time, the design and architecture problems in the current code base have gradually been exposed.

Why refactor

Take advantage of new language features

With the standardization of ES6, JavaScript (formally known as ECMAScript, abbreviated as ES) has been greatly improved, and mainstream browsers have finally begun to provide good support for these new features. In particular, some new features provide us with an opportunity to greatly improve the functionality of Vue.

The most notable of these is Proxy, which allows the framework to intercept operations on objects. The core function of Vue is the ability to monitor changes to user-defined state and update the DOM with data. Vue 2 achieves this ability by replacing properties on state objects with getters and setters. Switching to a proxy will allow us to remove the existing limitations of Vue, such as the inability to detect new property additions and provide better performance.

However, proxy is a function of the language itself and cannot be fully polyfilled in older browsers. In order to take advantage of it, we knew that we had to adjust the framework’s browser support range, which was a major breakthrough and could only be released in a new major version.

Solve architectural issues

To solve these problems in the current code base will require a lot of risky refactoring, which is almost equivalent to a complete rewrite.
In the process of maintaining Vue 2, due to the limitations of the existing architecture, we have accumulated many difficult problems. For example, the way the template compiler is written makes it very difficult to support source maps. Similarly, although Vue 2 technically allows building higher-level renderers for non-DOM platforms, we have to derive a code base and copy a lot of code to achieve this. To solve these problems in the current code base will require a lot of risky refactoring, which is almost equivalent to a complete rewrite.

At the same time, we have accumulated hidden dangers in the form of implicit coupling between the internals of various modules and floating code, and floating code does not seem to belong anywhere. This makes it more difficult to understand parts of the codebase in isolation, and we noticed that contributors rarely feel confident about important changes. Refactoring will give us the opportunity to rethink code organization based on these considerations.

Initial prototype stage

We started prototyping Vue 3 at the end of 2018, with the initial goal of verifying solutions to these problems. At this stage, we mainly lay a solid foundation for further development.

Switch to TypeScript

Vue 2 was originally written in pure JS. Soon after the prototype development phase, we realized that the type system would be very helpful for projects of this size. Type checking greatly reduces the chance of introducing unexpected errors during refactoring and helps contributors to make important changes more confidently. We pass Facebook's Flow type check because it can be gradually added to existing JS projects. Flow helped to a certain extent, but we didn't get much benefit from it. Especially the changing requirements make the upgrade very painful. Compared with the deep integration of TypeScript and Visual Studio Code, Flow's support for integrated development environments is not ideal.

We also noticed that users are increasingly using Vue and TypeScript at the same time. To support their use cases, we must author and maintain TypeScript declarations separately from using different type systems. Switching to TypeScript will allow us to automatically generate declaration files, thereby reducing the maintenance burden.

Decoupling internal packaging

We also adopted monorepo, where the framework consists of internal software packages, each of which has its own separate API, type definition and testing. We want to make the dependencies between these modules more clear, so that it is easier for developers to read, understand and make all changes. This is the key to our efforts to lower the project’s contribution barriers and improve its long-term maintainability.

Set up the RFC process

By the end of 2018, we had a working prototype using the new data-driven view system and virtual DOM renderer. We have verified the internal architecture improvements we want to make, but only include drafts of public-facing API changes. Now is the time to turn them into concrete designs.

We know that we must do this early and carefully. The widespread use of Vue means that breakthrough changes may lead to a large number of user migration costs and potential ecosystem fragmentation. To ensure that users can provide feedback on major changes, we adopted an RFC (Request for Comments) process in early 2019. Each RFC follows a template, which focuses on motivation, design details, trade-offs, and adoption strategies. Since this process is carried out in the GitHub repository, the proposal is submitted as a pull request, so the discussion will naturally unfold in the comments.

The RFC process has proven to be of great help. As an ideological framework, it forces us to fully consider all aspects of potential changes, involve our community in the design process, and submit well-thought-out functional requirements.

Faster and smaller

Performance is critical to the front-end framework. Although Vue 2 has excellent performance, by trying new rendering strategies, refactoring provides the possibility for further development.

Overcome the bottleneck of virtual DOM

Vue has a rather unique rendering strategy: it provides a template syntax similar to HTML, but compiles the template into a rendering function that returns a virtual DOM tree. The framework recursively traverses two virtual DOM trees and compares each attribute on each node to determine which parts of the actual DOM need to be updated. Because modern JavaScript engines perform advanced optimizations, this somewhat brutal algorithm is usually fast, but still involves a lot of unnecessary CPU work. When you look at a template that contains a lot of static content and only a small amount of dynamic binding (the entire virtual DOM), it is inefficient, especially if there is only a small change but you still need to recurse the entire virtual DOM tree to understand what happened.

Fortunately, the template compilation step gives us the opportunity to statically analyze the template and extract information about dynamic parts. Vue 2 does this to some extent by skipping static subtrees, but due to the overly simple compiler architecture, it is difficult to implement more advanced optimizations. In Vue 3, we rewrote the compiler with an appropriate AST conversion pipeline, which allows us to write compile-time optimizations in the form of conversion plugins.

With the new architecture, we hope to find a rendering strategy to minimize overhead. One option is to abandon the virtual DOM and directly generate imperative DOM operations, but this will eliminate the ability to directly write virtual DOM rendering functions, which we found to be very valuable to advanced users and library authors. In addition, this will be a huge breakthrough change.

Secondly, the best way is to eliminate unnecessary virtual DOM tree traversal and attribute comparison, which tends to bring the greatest performance overhead during the update process. In order to achieve this, the compiler and the runtime need to work together: the compiler analyzes the template and generates code with optimization hints, and the runtime will pick up the hints and take the fast path when possible. There are three main optimization tasks here:

First, at the tree level, we noticed that the node structure is completely static when there are no template instructions (for example, v-if and v-for). If we divide the template into dynamic and static "blocks", the node structure within each block becomes completely static again. When we update the nodes in a block, we no longer need to traverse the tree recursively, because we can track the dynamic binding within the block in a flat array. By reducing the amount of tree traversal we need to perform by an order of magnitude, most of the overhead of the virtual DOM is saved.

Second, the compiler will actively detect static nodes, subtrees and even data objects in the template, and promote them beyond the render function in the generated code. This avoids recreating these objects on each render, which greatly improves memory usage and reduces the frequency of garbage collection.

Third, at the element level, the compiler also generates an optimization flag for each element with dynamic binding based on the type of update that needs to be performed. For example, an element with dynamic class binding and many static properties will receive a flag indicating that only class checking is required. The runtime will obtain these prompts and use a dedicated fast path.

In summary, these technologies have significantly improved our rendering updates, and running Vue 3 can sometimes even be ten times faster than Vue 2.

Extremely small size

The size of the frame also affects its performance. This is an important concern for web applications, because resources need to be downloaded dynamically, and the application will be interactive before the browser parses the necessary JavaScript. This is especially true for single-page applications. Although Vue has always been relatively lightweight (the runtime size of Vue 2 is compressed to 23 KB), we have noticed two problems:

First of all, not everyone uses all the features of the framework. For example, applications that have never used transitional components will still download transition-related code and take time to parse it.

Secondly, when we add new features, the framework will become infinitely larger. When we consider the addition of new features, we have to consider the size of the issue. Therefore, we tend to include only the functions that most users will use in the framework.

Ideally, users should be able to remove the code for unused framework features at build time-also known as "Tree Shaking"-and only package the code they use. This will also allow us to publish features that some users will find useful, without increasing the effective download cost for the rest of the users.

In Vue 3, we achieved this goal by moving most of the global APIs and internal helpers to ES module exports. This allows modern packaging tools to statically analyze module dependencies and remove code related to unused exports. The template compiler also generates Tree Shaking-friendly code. If the function is actually used in the template, the code only imports the helper for the function.

Certain parts of the framework will never be Tree Shaking, because they are essential for any type of application. We call the metrics for these indispensable parts the base size. Despite the addition of many new features, the base size of Vue 3 after gzip is only about 10KB, which is even less than half of Vue 2.

Meet the needs of scale

We also want to improve Vue's ability to handle large applications. Our initial Vue design focused on a gentle learning curve. But as Vue became more and more widely adopted, we learned more about the needs of projects, which 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 reusable code are crucial, 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 (such as class fields and decorators) that we need to make classes available are still proposals and may change before they formally become part of JavaScript. The complexity and uncertainty involved make us doubt whether the addition of the Class API is really reasonable, because it does not provide any other features besides providing better TypeScript integration.

We decided to study other ways to solve the expansion problem. Inspired by React Hooks, we considered exposing lower-level data-driven views and component lifecycle APIs to achieve a more free-form way of writing component logic, called Composition API. There is no need to specify a long list of options to define components. The Composition API allows users to express, write and reuse stateful component logic as freely as writing functions, while providing excellent TypeScript support.

We are very excited about this idea. Although the Composition API is designed to solve specific categories of problems, technically speaking, it can only be used when writing components. In the first draft of the proposal, we hinted that we might replace the existing Options API with the Composition API in a future version. This led to a lot of dissatisfaction among community members, which taught us a valuable lesson, allowing them to clearly communicate their long-term plans and intentions, as well as understanding the needs of users. After listening to the feedback from our community, we completely redesigned the proposal to make it clear that the Composition API will be a complement and supplementary API to Options. The revised proposal was more active and many constructive suggestions were received.

Seek balance

Among the more than one million Vue developers, there are beginners who only know the basics of HTML and CSS, old programmers who migrated from jQuery, and the front-end migrated from another framework, and some are even looking for a front-end solution. The back-end engineer of the solution, and the software architect of large-scale processing software. Some developers may wish to add interactivity to older applications, while others may require agile development but one-off projects with limited maintenance requirements. Throughout the life cycle of the project, you may have to deal with large, multi-year projects and a volatile development team.

While we seek to balance various compromises, Vue's design is constantly influenced and inspired by these needs. Vue is known as the "progressive framework", which encapsulates the hierarchical API design resulting from this process. Beginners can use CDN scripts, HTML-based templates and intuitive Options API to learn easily, while masters can use the full-featured CLI, rendering functions and Composition API to handle more complex projects.

There is still much work to be done to realize our vision. The most important thing is to update the surrounding libraries, documents and tools to ensure a smooth migration. In the next few months, we will continue to work hard, and we can’t wait to see what the community will create with Vue 3.

Guess you like

Origin blog.csdn.net/GetIdea/article/details/106405464