core icon indicating copy to clipboard operation
core copied to clipboard

feat: introduce suspensible option to <Suspense> to fix suspense flicks

Open antfu opened this issue 3 years ago • 0 comments

Resolve https://github.com/vuejs/core/issues/5513

This PR introduces a new prop for Suspense - <Suspense suspensible> to allow nested suspense to be captured by the parent suspense.

Reproduction

Thanks a lot to @danielroe for the great reproduction: https://stackblitz.com/edit/vitejs-vite-ftc8df?file=src%2FApp.vue

The Problem

When we have multiple async components like:

<Suspense>
  <AsyncOuter>
    <AsyncInner />
  </AsyncOuter>
</Suspense>

Suspense creates a boundary that will resolve all the async components down the tree, which is how it is expected to be.

When both async components need to be async (nested route page for example)

<Suspense>
  <component :is="DynamicAsyncOuter">
    <component :is="DynamicAsyncInner" />
  </component>
</Suspense>

At the initial mount, it works as expected, both DynamicAsyncOuter and DynamicAsyncInner are waited before rendering.

The problem comes with patching. When we change DynamicAsyncOuter, Suspense awaits it correctly, but when we change DynamicAsyncInner, the Suspense does not get patched causing the nested DynamicAsyncInner renders an empty node until it has been resolved, causing the flickering.

In order to solve that, we could have a nested suspense to handle the patch for the nested component, like:

<Suspense>
  <component :is="DynamicAsyncOuter">
    <Suspense> <!-- this -->
      <component :is="DynamicAsyncInner" />
    </Suspense>
  </component>
</Suspense>

This solves the patching issue but makes the mounting break as the nested Suspense now behaves like a sync component and has its own fallback.

The Solution

This PR introduces a new prop for Suspense - <Suspense suspensible> to allow nested suspense to be captured by the parent suspense. Making it able to handle async patching for branches and keep the mount as one render to avoid flickering.

<Suspense>
  <component :is="DynamicAsyncOuter">
    <Suspense suspensible> <!-- this -->
      <component :is="DynamicAsyncInner" />
    </Suspense>
  </component>
</Suspense>

Unlike async components, Suspsense's suspensible prop is the default to false in order to not break the current behavior.

antfu avatar Sep 24 '22 06:09 antfu