preface

At around 3:30 am today, Yu Yuxi posted an article on his Micro blog. Of course, the boss is in another time zone, so the morning here should correspond to the afternoon in the worst-affected time zone.

The original link: increment.com/frontend/ma…

Lessons learned from refactoring new versions of Vue.js

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. (At the time of writing, this is still a work in progress). The main version of Vue was formed at the end of 2018, when the Vue 2 codebase was two and a half years old. That may not sound like a long time in the life cycle of general-purpose software, but the front-end environment has changed dramatically during that time.

Two main considerations led us to develop the new major version of Vue (and to rewrite it) : First, the new JavaScript language features are generally available in major browsers. Second, design and architectural issues in the current code base have come to light over time.

Why refactor

Take advantage of new language features

With the standardization of ES6, JavaScript (officially called ECMAScript, or ES) received major improvements, and major browsers are finally starting to offer decent support for these new features. In particular, some new features offer us the 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 functionality of Vue is the ability to listen for changes to user-defined state and update the DOM data-driven. Vue 2 implements this capability by replacing properties on state objects with getters and setters. Switching to Proxy will allow us to remove existing limitations of Vue, such as the inability to detect new property additions and provide better performance.

However, Proxy is a feature of the language itself and cannot be fully polyfilled in older browsers. To take advantage of it, we knew we would have to adjust the framework’s browser support range, a major breakthrough that would only be released in a new major release.

Solving architectural problems

Addressing these issues in the current code base would require a lot of risky refactoring, which is almost equivalent to a complete rewrite. In the process of maintaining Vue 2, we accumulated a number of problems that were difficult to solve due to the limitations of the existing architecture. For example, the template compiler is written in a way that makes it difficult to support source mapping. Similarly, while Vue 2 technically allows us to build renderers for non-DOM platforms, we have to derive the code base and copy a lot of code to achieve this. Addressing these issues in the current code base would require a lot of risky refactoring, which is almost equivalent to a complete rewrite.

At the same time, we’ve accumulated pitfalls in the form of implicit coupling between the insides of various modules and floating code that doesn’t seem to belong anywhere. This makes it more difficult to understand parts of the code base in isolation, and we’ve noticed that contributors rarely feel confident about significant changes. Refactoring gives us an opportunity to rethink the organization of the code in light of these considerations.

Initial prototype phase

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

Switch to the TypeScript

Vue 2 was originally written in pure JS. Shortly 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 make important changes with more confidence. We pass Facebook’s Flow type check, as it can be added gradually to existing JS projects. Flow helped to some extent, but we didn’t get much benefit from it. In particular, changing requirements make upgrading painful. Flow’s support for integrated development environments (ides) is also less than ideal compared to the deep integration of TypeScript and Visual Studio Code.

We’ve also noticed that users are increasingly using both Vue and TypeScript. To support their use cases, we must write and maintain TypeScript declarations separately from using a different type system. Switching to TypeScrip will allow us to automatically generate declaration files, thus reducing the maintenance burden.

Decouple the internal packaging

We also adopted Monorepo, where the framework consists of internal packages, each with its own separate API, type definition, and testing. We want to make the dependencies between these modules more explicit, making it easier for developers to read, understand, and make all the changes. This is key to our efforts to lower the barriers to project contribution and improve its long-term maintainability.

Set 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 architectural improvements we want to make, but only included draft 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 radical changes could lead to significant migration costs for users and potential ecosystem fragmentation. To ensure that users can provide feedback on major changes, we adopted an RFC (Ask for comments) process in early 2019. Each RFC follows a template that focuses on motivation, design details, tradeoffs, and adoption strategies. Since this process takes place in the GitHub repository, the proposal is submitted as a request request, so the discussion naturally unfolds in the comments.

The RFC process has proven to be tremendously helpful, as a framework for thinking that forces us to fully consider all aspects of potential change, involve our community in the design process, and submit well thought-out functional requirements.

Faster and more small

Performance is critical to the front-end framework. Although Vue 2 has excellent performance, refactoring offers the possibility of further development by experimenting with new rendering strategies.

Overcome virtual DOM bottlenecks

Vue has a rather unique rendering strategy: it provides template syntax similar to HTML, but compiles the template into a rendering function that returns a virtual DOM tree. The framework determines which parts of the real DOM need to be updated by recursively traversing two virtual DOM trees and comparing each attribute on each node. Due to the advanced optimizations performed by modern JavaScript engines, 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 few dynamic bindings (the entire virtual DOM), it’s inefficient, especially if only a small change is made and you still need to recurse the entire virtual DOM 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 it is difficult to implement more advanced optimizations 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 as much as possible. One option would be to abandon the virtual DOM and directly generate imperative DOM operations, but this would eliminate the ability to write the rendering capabilities of the virtual DOM directly, which we found very valuable to advanced users and library authors. Plus, it would be a huge breakthrough.

Second, the best approach is to eliminate unnecessary virtual DOM tree traversal and attribute comparisons, which often incur the greatest performance overhead during the update process. To achieve this, the compiler and the runtime need to work together: the compiler analyzes the template and generates code with optimization hints, while the runtime picks up the hints and takes the fast path where possible. There are three main optimizations:

First, at the tree level, we notice that the node structure is completely static without template directives (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 a node within a block, we no longer need to recursively traverse the tree, because we can track the dynamic bindings within that block in a flat array. Much of the virtual DOM overhead was saved by reducing the amount of tree traversal we needed to perform by an order of magnitude.

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

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

Taken together, these technologies have significantly improved our rendering updates, running Vue 3 sometimes ten times faster than Vue 2.

Minuscule size

The size of the framework 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 until the browser parses the necessary JavaScript. This is especially true for single-page applications. Although Vue has always been relatively lightweight (Vue 2’s run-time size is compressed to 23 KB), we noticed two issues:

First, not everyone uses all of the framework’s features. For example, an application that has never used a transition component will still download transition-related code and take the time to parse it.

Second, as we add new features, the framework grows infinitely larger. When we think about adding new features, we have to take the size into consideration. Therefore, we prefer the framework to contain only the functionality that most users will use.

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

In Vue 3, we accomplished this by moving most of the global apis and internal helpers to the ES module export. This enables modern packaging tools to statically analyze module dependencies and remove code associated with unused exports. The template compiler also generates Tree Shak-friendly code that imports only the helper for the feature if it is actually used in the template.

Certain parts of the framework will never be Tree Shaking because they are essential for any type of application. We refer to these essential component metrics as base dimensions. Despite the addition of many new features, Vue 3’s base size after Gzip is only about 10KB, less than half the size of Vue 2.

Meeting scale requirements

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 widely adopted, we learned more about the requirements for projects that consisted of hundreds of modules and were maintained over time by dozens of developers. For these types of projects, a type system like TypeScript and the ability to reuse 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 classes available, such as class fields and decorators, are still a proposal and may change before they become part of JavaScript. The complexity and uncertainty involved make us wonder if it really makes sense to add the Class API, which offers nothing but better TypeScript integration.

We decided to look at other ways to solve the scaling problem. Inspired by the React Hooks, we considered exposing lower-level data-driven views and component lifecycle apis to implement 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 freely express, write, and reuse stateful component logic as if it were a function, while providing excellent TypeScript support.

We are very excited about this idea. Although the Composition API is intended to address specific categories of problems, it is technically only available 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 from community members, and it was a valuable lesson for us 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 have completely redesigned the proposal to make it clear that the Composition API will be complementary to the Options API. The revised proposals received were more positive and many constructive suggestions were received.

Seek a balance

Among Vue’s million-plus developers, there are beginners who only know the basics of HTML and CSS, older programmers migrating from jQuery, front-end programmers migrating from another framework, and even back-end engineers looking for front-end solutions, and software architects who process software on a large scale. Some developers may want to add interactivity to older applications, while others may need a one-off project with agile development but limited maintenance requirements. You may have to deal with large, multi-year projects and a fluctuating development team over the life of a project.

Vue’s design is constantly influenced and inspired by these needs as we seek to balance various tradeoffs. Vue is a so-called “progressive framework” that encapsulates the layered API design resulting from this process. Beginners can easily learn using CDN scripts, HTML-based templates, and the intuitive Options API, while masters can handle more complex projects using the full-featured CLI, rendering capabilities, and Composition API.

There is still much work to be done to realize our vision. Most importantly, update the surrounding libraries, documentation, and tools to ensure a smooth migration. As we continue to work hard over the next few months, we can’t wait to see what the community will create with Vue 3.

Previous excellent article

  • The Svelte community is fully integrated.
  • Create your own Visual Data Map without any libraries
  • A fun new feature of Vue: Introducing JS variables into CSS
  • Microsoft launches comments section on GitHub
  • Vue 3.0.3: New CSS variable passing and the latest Ref Proposal
  • “Double 11 small black box is cool? Let’s use CSS variables to improve!”
  • “Don’t underestimate the nine grid, one question can let a candidate reveal his true colors!”
  • “Mobile Layout Interview Questions to test your CSS Skills (Center)”
  • A series of confusing behaviors after setting prototype Objects as Proxies
  • Vue’s Super Fun New Feature: DOM Portal
  • “Use of React’s Super-popular CSS-in-JS Library in the Vue Project: Styled – Components”
  • Is It Finally Vue’s Turn to Inspire React?
  • A Small Pit in Vue3 on IOS
  • Upgrade your React project to Immer instead of Immutable by 2020
  • “Soul Interrogation from the Author of React Hooks and Immutable”
  • Hooks use of the New VUe-Router
  • React 17 is officially a transition version!
  • The Father of Node’s refactoring Deno is finally released. Will it Replace Node after all?