core icon indicating copy to clipboard operation
core copied to clipboard

types(reactivity): Refactor the `UnwrapRef` type to improve type checking performance.

Open browsnet opened this issue 2 years ago • 15 comments
trafficstars

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.

browsnet avatar Mar 10 '23 17:03 browsnet

/ecosystem-ci run

sxzz avatar Mar 10 '23 17:03 sxzz

📝 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

vue-bot avatar Mar 10 '23 17:03 vue-bot

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.

browsnet avatar Mar 11 '23 03:03 browsnet

/ecosystem-ci run

Justineo avatar Mar 11 '23 03:03 Justineo

📝 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

vue-bot avatar Mar 11 '23 03:03 vue-bot

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.

browsnet avatar Mar 11 '23 15:03 browsnet

/ecosystem-ci run

Justineo avatar Mar 16 '23 10:03 Justineo

📝 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

vue-bot avatar Mar 16 '23 10:03 vue-bot

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

github-actions[bot] avatar Oct 24 '23 14:10 github-actions[bot]

/ecosystem-ci run

pikax avatar Oct 24 '23 14:10 pikax

📝 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

vue-bot avatar Oct 24 '23 14:10 vue-bot

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

netlify[bot] avatar Oct 24 '23 14:10 netlify[bot]

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

netlify[bot] avatar Oct 24 '23 14:10 netlify[bot]

@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 :)

pikax avatar Oct 24 '23 15:10 pikax

@browsnet if you could also provided proof of this is improving performance it would be great :)

pikax avatar Nov 03 '23 19:11 pikax