core
core copied to clipboard
types(reactivity): Refactor the `UnwrapRef` type to improve type checking performance.
Because Vue.js heavily uses the ref method, its return type is Ref<UnwrapRef<T>>. This also means that UnwrapRef<T> is one of the most frequently used generics in Vue.js, and its type-checking speed determines the overall speed of type-checking for the entire application. Therefore, extra attention is required.
First, let me explain the current implementation principle of the UnwrapRef<T> type. It uses a top-down recursive pattern. When it encounters an unknown array or object, it continuously searches deeper to see if there is a Ref<U> one level deeper. When encountering Ref<U>, an action is taken; otherwise, it continues downwards until touch all leaf nodes.
However, this implementation has a significant performance cost and may even fail to complete the mission, or causing strange bugs. The main reason is that the implementation assumes T is a directed tree, starting from the root to the leaf and continuing endlessly.
In fact, in the TypeScript world, types can also nest with each other and reference each other in a graph. For example, the Element type in HTML, combined with various other derived types, forms a complex directed cyclic graph. This brings significant obstacles to the DFS algorithm. Although TypeScript can stop the recursion after a certain level, the nested level is sometimes too deep to achieve the intended design.
The current UnwrapRef<T> implementation adopts a reserved space approach to avoid encountering this problem. Specifically, an interface RefUnwrapBailTypes is reserved, and in different situations, TypeScript's interface merging strategy is used to allow it to skip some complex type graphs. For example, in @vue/runtime-dom package, Node | Window, these two types can be skipped.
However, in some cases, TypeScript cannot merge such interfaces accurately, especially when there are conflicts between different versions during module installation. If the interface cannot merge, UnwrapRef<T> will not skip the Dom Node type, which may cause performance problems or even make the type completely unusable.
Another situation is that, in addition to the complex types brought by the DOM, the user itself may also define a set of complex nested type relationships. At this time, using UnwrapRef<T> will fall into the same dilemma as above, unless the interface RefUnwrapBailTypes is manually extended.
One solution is to modify UnwrapRef<T> into a lazy-expanded definition method. Instead of immediately expanding each level, the generic is only expanded downwards when this level is needed, greatly reducing the time overhead of type checking.
export type UnwrapRef<T> = T extends any ? UnwrapRefLazy<{ ref: T }>['ref'] : never
type UnwrapRefLazy<T> = {
[K in keyof T]: K extends symbol
? T[K] :
T[K] extends object & { [ShallowReactiveMarker]?: never }
? T[K] extends Function
| CollectionTypes
| BaseTypes
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
| { [RawSymbol]?: true }
? T[K] :
T[K] extends ShallowRef<infer V>
? V
: T[K] extends Ref<infer V>
? UnwrapRefLazy<V>
: UnwrapRefLazy<T[K]>
: T[K]
}
With the new UnwrapRef<T>, whether encountering HTMLElement or the user's own defined deeply nested types, they can be easily checked. In fact, we don't need interface RefUnwrapBailTypes anymore, feel free to keep it or not.
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result |
|---|---|
| naive-ui | :x: failure |
| nuxt | :x: failure |
| pinia | :white_check_mark: success |
| router | :white_check_mark: success |
| test-utils | :white_check_mark: success |
| vant | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success |
| vitepress | :white_check_mark: success |
| vue-macros | :x: failure |
| vuetify | :x: failure |
| vueuse | :white_check_mark: success |
Can you trigger the ecosystem CI again? I feel a bit confused after looking at these error messages from the test suites, and I'm not even sure if they are caused by this PR. If you could help me sort out the errors, that would be even better. Thank you.
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result |
|---|---|
| naive-ui | :x: failure |
| nuxt | :x: failure |
| pinia | :white_check_mark: success |
| router | :white_check_mark: success |
| test-utils | :white_check_mark: success |
| vant | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success |
| vitepress | :white_check_mark: success |
| vue-macros | :x: failure |
| vuetify | :x: failure |
| vueuse | :white_check_mark: success |
With regards to Naive-UI and Nuxt, it seems likely that the issue is related to their own support for the current version of Vue 3.3.0-alpha.4 and not caused by my PR.
However, I have fixed the issue that occurred in vue-macros. As for the issue with Vuetify, I found that it was using an incorrect syntax in one of its code sections. I have submitted a corresponding PR on the Vuetify side.
When https://github.com/vuetifyjs/vuetify/pull/16894 is merged, The test for vuetify suite should be success.
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result |
|---|---|
| naive-ui | :x: failure |
| nuxt | :x: failure |
| pinia | :x: failure |
| router | :white_check_mark: success |
| test-utils | :white_check_mark: success |
| vant | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success |
| vitepress | :white_check_mark: success |
| vue-macros | :white_check_mark: success |
| vuetify | :white_check_mark: success |
| vueuse | :white_check_mark: success |
Size Report
Bundles
| File | Size | Gzip | Brotli |
|---|---|---|---|
| runtime-dom.global.prod.js | 86.5 kB | 32.9 kB | 29.7 kB |
| vue.global.prod.js | 132 kB | 49.6 kB | 44.5 kB |
Usages
| Name | Size | Gzip | Brotli |
|---|---|---|---|
| createApp | 47.9 kB | 18.9 kB | 17.3 kB |
| createSSRApp | 51.2 kB | 20.2 kB | 18.4 kB |
| defineCustomElement | 50.3 kB | 19.7 kB | 17.9 kB |
| overall | 61.3 kB | 23.7 kB | 21.6 kB |
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :white_check_mark: success | :x: failure |
| nuxt | :white_check_mark: success | :white_check_mark: success |
| pinia | :x: failure | :white_check_mark: success |
| quasar | :white_check_mark: success | :white_check_mark: success |
| router | :white_check_mark: success | :white_check_mark: success |
| test-utils | :white_check_mark: success | :white_check_mark: success |
| vant | :white_check_mark: success | :x: failure |
| vite-plugin-vue | :white_check_mark: success | :white_check_mark: success |
| vitepress | :white_check_mark: success | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :white_check_mark: success | :white_check_mark: success |
| vuetify | :white_check_mark: success | :white_check_mark: success |
| vueuse | :white_check_mark: success | :white_check_mark: success |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
Deploy Preview for vue-sfc-playground failed.
| Name | Link |
|---|---|
| Latest commit | b995eb87cec81badb6c32221ea5b6b1bf4a4b0f1 |
| Latest deploy log | https://app.netlify.com/sites/vue-sfc-playground/deploys/6537d64488824200085a763c |
Deploy Preview for vue-next-template-explorer failed.
| Name | Link |
|---|---|
| Latest commit | b995eb87cec81badb6c32221ea5b6b1bf4a4b0f1 |
| Latest deploy log | https://app.netlify.com/sites/vue-next-template-explorer/deploys/6537d64436442c00082b8506 |
@browsnet thank you for the change, Ref type is quite complex 🙏
pinia is failing, I've added the failing test if you could give it a go pls :)
@browsnet if you could also provided proof of this is improving performance it would be great :)