Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental support for resuming suspended hydration #2214

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

developit
Copy link
Member

@developit developit commented Dec 27, 2019

This PR adds two metadata properties to VNodes which are used to store the isHydrating flag and current DOM element (oldDom) when bailing out of diffing as a result of a suspension (or error).

This is particularly useful during hydration, as it means we can flip back into hydration mode and pick up the previously-attempted DOM element when we attempt to render the previously-suspended tree's root VNode.

The implementation here is not great in terms of filesize, and I haven't added dedicated tests for it yet. I have verified that this works with a prototype Suspense-based AsyncComponent implementation for Preact CLI (included below) that can be tested via npm link here.

experimental Suspense-based async-component.js for preact cli

The main thing worth noting here is that there is no use of internals.

How it works: the generated wrapper component (AsyncComponent) renders a child component that it knows will throw a given value (an object for simple referential equality). It then checks for that value having been thrown in componentDidCatch and absorbs the error, which has the effect of dropping its subtree's rendering on the floor. This preserves the DOM tree in-tact during the period where the lazy-loaded component is not yet available.

It perfom this throw routing for all renders up until its payload (whatever is passed to the callback given to load) becomes available. Once available, it "unsuspends" by triggering a re-render using setState({}). This is where the magic happens:

Since Preact knows the original "owner" VNode behind this component was suspended (in hydrate mode!) and has maintained a reference to its associated DOM element at time of suspension, it will immediately jump back into hydrate mode in the diff() call originating from setState and re-use that stored DOM element.

import { h, Component } from 'preact';

const PENDING = {};

function Pending() {
	throw PENDING;
}

export default function async (load) {
	let component;
	function AsyncComponent() {
		Component.call(this);
		if (!component) {
			this.componentWillMount = (() => {
				load(mod => {
					component = mod && mod.default || mod;
					this.componentDidCatch = null;
					this.setState({});
				});
			}
			),
			this.componentDidCatch = err => {
				if (err !== PENDING) throw err;
			};
			this.shouldComponentUpdate = () => component != null;
		}
		this.render = props => h(component || Pending, props);
	}
	AsyncComponent.preload = load;
	(AsyncComponent.prototype = new Component).constructor = AsyncComponent;
	return AsyncComponent;
}

@coveralls
Copy link

coveralls commented Dec 27, 2019

Coverage Status

Coverage increased (+0.0009%) to 99.789% when pulling c8eb59f on experimental-suspense-hydration into 0da8005 on master.

Copy link
Member

@JoviDeCroock JoviDeCroock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good to me

@@ -215,6 +225,11 @@ export function diff(

if ((tmp = options.diffed)) tmp(newVNode);
} catch (e) {
// Before bailing out, mark the current VNode with the DOM element and hydration state.
// We can use this information if we return here to render later on.
newVNode._dom = oldDom;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be reduced to:

Suggested change
newVNode._dom = oldDom;
oldVNode._hydrateDom = newVNode._dom = oldDom;

This removes the need for the next line (multi-line suggestions aren't available)

// If this was the innermost VNode at a point where the tree suspended,
// pick up diffing where we left off using the saved DOM element and hydration state.
if (oldVNode._hydrateDom && excessDomChildren == null) {
oldDom = oldVNode._hydrateDom;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
oldDom = oldVNode._hydrateDom;
newVNode._dom = oldDom = oldVNode._hydrateDom;

@prateekbh
Copy link
Member

I tried reproducing the broken test in a real world app but couldn't everything worked fine there

@prateekbh
Copy link
Member

@developit am i supposed to see this with your given instructions?

Screen Shot 2020-01-01 at 11 33 21 PM

@github-actions
Copy link

Size Change: +188 B (0%)

Total Size: 38.3 kB

Filename Size Change
dist/preact.js 3.77 kB +47 B (1%)
dist/preact.min.js 3.77 kB +47 B (1%)
dist/preact.module.js 3.79 kB +48 B (1%)
dist/preact.umd.js 3.83 kB +46 B (1%)
ℹ️ View Unchanged
Filename Size Change
compat/dist/compat.js 3 kB 0 B
compat/dist/compat.module.js 3.03 kB 0 B
compat/dist/compat.umd.js 3.05 kB 0 B
debug/dist/debug.js 2.95 kB 0 B
debug/dist/debug.module.js 2.93 kB 0 B
debug/dist/debug.umd.js 3.01 kB 0 B
devtools/dist/devtools.js 175 B 0 B
devtools/dist/devtools.module.js 185 B 0 B
devtools/dist/devtools.umd.js 252 B 0 B
hooks/dist/hooks.js 1.05 kB 0 B
hooks/dist/hooks.module.js 1.08 kB 0 B
hooks/dist/hooks.umd.js 1.13 kB 0 B
test-utils/dist/testUtils.js 390 B 0 B
test-utils/dist/testUtils.module.js 392 B 0 B
test-utils/dist/testUtils.umd.js 469 B 0 B

compressed-size-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants