preact icon indicating copy to clipboard operation
preact copied to clipboard

Experimental support for resuming suspended hydration

Open developit opened this issue 6 years ago • 4 comments

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;
}

developit avatar Dec 27 '19 23:12 developit

Coverage Status

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

coveralls avatar Dec 27 '19 23:12 coveralls

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

prateekbh avatar Dec 31 '19 21:12 prateekbh

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

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

prateekbh avatar Jan 02 '20 07:01 prateekbh

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

github-actions[bot] avatar Feb 18 '20 22:02 github-actions[bot]