(fn: (a: A) => R, a: A): R;
 
-	export function unstable_batchedUpdates(
-		callback: (arg?: any) => void,
-		arg?: any
-	): void;
-
 	export type PropsWithChildren = P & {
-		children?: preact.ComponentChildren | undefined;
+		children?: preact1.ComponentChildren | undefined;
 	};
 
 	export const Children: {
-		map(
+		map(
 			children: T | T[],
 			fn: (child: T, i: number) => R
 		): R[];
-		forEach(
+		forEach(
 			children: T | T[],
 			fn: (child: T, i: number) => void
 		): void;
-		count: (children: preact.ComponentChildren) => number;
-		only: (children: preact.ComponentChildren) => preact.ComponentChild;
-		toArray: (children: preact.ComponentChildren) => preact.VNode<{}>[];
+		count: (children: preact1.ComponentChildren) => number;
+		only: (children: preact1.ComponentChildren) => preact1.ComponentChild;
+		toArray: (children: preact1.ComponentChildren) => preact1.VNode<{}>[];
 	};
 
 	// scheduler
diff --git a/compat/src/index.js b/compat/src/index.js
index 61fd2f3625..d609365ea9 100644
--- a/compat/src/index.js
+++ b/compat/src/index.js
@@ -32,7 +32,6 @@ import { memo } from './memo';
 import { forwardRef } from './forwardRef';
 import { Children } from './Children';
 import { Suspense, lazy } from './suspense';
-import { SuspenseList } from './suspense-list';
 import { createPortal } from './portals';
 import {
 	hydrate,
@@ -117,21 +116,12 @@ function unmountComponentAtNode(container) {
 function findDOMNode(component) {
 	return (
 		(component &&
-			(component.base || (component.nodeType === 1 && component))) ||
+			((component._vnode && component._vnode._dom) ||
+				(component.nodeType === 1 && component))) ||
 		null
 	);
 }
 
-/**
- * Deprecated way to control batched rendering inside the reconciler, but we
- * already schedule in batches inside our rendering code
- * @template Arg
- * @param {(arg: Arg) => void} callback function that triggers the updated
- * @param {Arg} [arg] Optional argument that can be passed to the callback
- */
-// eslint-disable-next-line camelcase
-const unstable_batchedUpdates = (callback, arg) => callback(arg);
-
 /**
  * In React, `flushSync` flushes the entire tree and forces a rerender. It's
  * implmented here as a no-op.
@@ -180,11 +170,8 @@ export {
 	useDeferredValue,
 	useSyncExternalStore,
 	useTransition,
-	// eslint-disable-next-line camelcase
-	unstable_batchedUpdates,
 	StrictMode,
 	Suspense,
-	SuspenseList,
 	lazy,
 	__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
 };
@@ -229,10 +216,8 @@ export default {
 	memo,
 	forwardRef,
 	flushSync,
-	unstable_batchedUpdates,
 	StrictMode,
 	Suspense,
-	SuspenseList,
 	lazy,
 	__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
 };
diff --git a/compat/src/internal.d.ts b/compat/src/internal.d.ts
index efc5287ca3..31795ba123 100644
--- a/compat/src/internal.d.ts
+++ b/compat/src/internal.d.ts
@@ -27,12 +27,11 @@ export interface Component extends PreactComponent
 {
 
 export interface FunctionComponent
 extends PreactFunctionComponent
 {
 	shouldComponentUpdate?(nextProps: Readonly
): boolean;
-	_forwarded?: boolean;
 	_patchedLifecycles?: true;
 }
 
 export interface VNode extends PreactVNode {
-	$$typeof?: symbol | string;
+	$$typeof?: symbol;
 	preactCompatNormalized?: boolean;
 }
 
diff --git a/compat/src/memo.js b/compat/src/memo.js
index e743199055..925e0c9eae 100644
--- a/compat/src/memo.js
+++ b/compat/src/memo.js
@@ -29,6 +29,5 @@ export function memo(c, comparer) {
 	}
 	Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')';
 	Memoed.prototype.isReactComponent = true;
-	Memoed._forwarded = true;
 	return Memoed;
 }
diff --git a/compat/src/portals.js b/compat/src/portals.js
index 2e126f9076..c082e58364 100644
--- a/compat/src/portals.js
+++ b/compat/src/portals.js
@@ -46,14 +46,9 @@ function Portal(props) {
 			parentNode: container,
 			childNodes: [],
 			_children: { _mask: root._mask },
-			contains: () => true,
 			insertBefore(child, before) {
 				this.childNodes.push(child);
 				_this._container.insertBefore(child, before);
-			},
-			removeChild(child) {
-				this.childNodes.splice(this.childNodes.indexOf(child) >>> 1, 1);
-				_this._container.removeChild(child);
 			}
 		};
 	}
diff --git a/compat/src/render.js b/compat/src/render.js
index f18cbd896b..9807c0fc0b 100644
--- a/compat/src/render.js
+++ b/compat/src/render.js
@@ -24,10 +24,9 @@ import {
 	useSyncExternalStore,
 	useTransition
 } from './index';
+import { assign, IS_NON_DIMENSIONAL } from './util';
 
-export const REACT_ELEMENT_TYPE =
-	(typeof Symbol != 'undefined' && Symbol.for && Symbol.for('react.element')) ||
-	0xeac7;
+export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
 
 const CAMEL_PROPS =
 	/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
@@ -36,44 +35,11 @@ const CAMEL_REPLACE = /[A-Z0-9]/g;
 const IS_DOM = typeof document !== 'undefined';
 
 // Input types for which onchange should not be converted to oninput.
-// type="file|checkbox|radio", plus "range" in IE11.
-// (IE11 doesn't support Symbol, which we use here to turn `rad` into `ra` which matches "range")
-const onChangeInputType = type =>
-	(typeof Symbol != 'undefined' && typeof Symbol() == 'symbol'
-		? /fil|che|rad/
-		: /fil|che|ra/
-	).test(type);
+const onChangeInputType = type => /fil|che|rad/.test(type);
 
 // Some libraries like `react-virtualized` explicitly check for this.
 Component.prototype.isReactComponent = {};
 
-// `UNSAFE_*` lifecycle hooks
-// Preact only ever invokes the unprefixed methods.
-// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method.
-// - If a component defines its own `componentDidMount()` (including via defineProperty), use that.
-// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter.
-// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property.
-// See https://github.com/preactjs/preact/issues/1941
-[
-	'componentWillMount',
-	'componentWillReceiveProps',
-	'componentWillUpdate'
-].forEach(key => {
-	Object.defineProperty(Component.prototype, key, {
-		configurable: true,
-		get() {
-			return this['UNSAFE_' + key];
-		},
-		set(v) {
-			Object.defineProperty(this, key, {
-				configurable: true,
-				writable: true,
-				value: v
-			});
-		}
-	});
-});
-
 /**
  * Proxy render() since React returns a Component reference.
  * @param {import('./internal').VNode} vnode VNode tree to render
@@ -151,7 +117,17 @@ function handleDomVNode(vnode) {
 		}
 
 		let lowerCased = i.toLowerCase();
-		if (i === 'defaultValue' && 'value' in props && props.value == null) {
+		if (i === 'style' && typeof value === 'object') {
+			for (let key in value) {
+				if (typeof value[key] === 'number' && !IS_NON_DIMENSIONAL.test(key)) {
+					value[key] += 'px';
+				}
+			}
+		} else if (
+			i === 'defaultValue' &&
+			'value' in props &&
+			props.value == null
+		) {
 			// `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined.
 			// `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property.
 			i = 'value';
@@ -245,8 +221,15 @@ options.vnode = vnode => {
 	// only normalize props on Element nodes
 	if (typeof vnode.type === 'string') {
 		handleDomVNode(vnode);
+	} else if (typeof vnode.type === 'function' && vnode.type.defaultProps) {
+		let normalizedProps = assign({}, vnode.props);
+		for (let i in vnode.type.defaultProps) {
+			if (normalizedProps[i] === undefined) {
+				normalizedProps[i] = vnode.type.defaultProps[i];
+			}
+		}
+		vnode.props = normalizedProps;
 	}
-
 	vnode.$$typeof = REACT_ELEMENT_TYPE;
 
 	if (oldVNodeHook) oldVNodeHook(vnode);
diff --git a/compat/src/suspense-list.d.ts b/compat/src/suspense-list.d.ts
deleted file mode 100644
index 0a3be0adc9..0000000000
--- a/compat/src/suspense-list.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Intentionally not using a relative path to take advantage of
-// the TS version resolution mechanism
-import { Component, ComponentChild, ComponentChildren } from 'preact';
-
-//
-// SuspenseList
-// -----------------------------------
-
-export interface SuspenseListProps {
-	children?: ComponentChildren;
-	revealOrder?: 'forwards' | 'backwards' | 'together';
-}
-
-export class SuspenseList extends Component {
-	render(): ComponentChild;
-}
diff --git a/compat/src/suspense-list.js b/compat/src/suspense-list.js
deleted file mode 100644
index 5e5d750a08..0000000000
--- a/compat/src/suspense-list.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { Component, toChildArray } from 'preact';
-import { suspended } from './suspense.js';
-
-// Indexes to linked list nodes (nodes are stored as arrays to save bytes).
-const SUSPENDED_COUNT = 0;
-const RESOLVED_COUNT = 1;
-const NEXT_NODE = 2;
-
-// Having custom inheritance instead of a class here saves a lot of bytes.
-export function SuspenseList() {
-	this._next = null;
-	this._map = null;
-}
-
-// Mark one of child's earlier suspensions as resolved.
-// Some pending callbacks may become callable due to this
-// (e.g. the last suspended descendant gets resolved when
-// revealOrder === 'together'). Process those callbacks as well.
-const resolve = (list, child, node) => {
-	if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) {
-		// The number a child (or any of its descendants) has been suspended
-		// matches the number of times it's been resolved. Therefore we
-		// mark the child as completely resolved by deleting it from ._map.
-		// This is used to figure out when *all* children have been completely
-		// resolved when revealOrder is 'together'.
-		list._map.delete(child);
-	}
-
-	// If revealOrder is falsy then we can do an early exit, as the
-	// callbacks won't get queued in the node anyway.
-	// If revealOrder is 'together' then also do an early exit
-	// if all suspended descendants have not yet been resolved.
-	if (
-		!list.props.revealOrder ||
-		(list.props.revealOrder[0] === 't' && list._map.size)
-	) {
-		return;
-	}
-
-	// Walk the currently suspended children in order, calling their
-	// stored callbacks on the way. Stop if we encounter a child that
-	// has not been completely resolved yet.
-	node = list._next;
-	while (node) {
-		while (node.length > 3) {
-			node.pop()();
-		}
-		if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) {
-			break;
-		}
-		list._next = node = node[NEXT_NODE];
-	}
-};
-
-// Things we do here to save some bytes but are not proper JS inheritance:
-// - call `new Component()` as the prototype
-// - do not set `Suspense.prototype.constructor` to `Suspense`
-SuspenseList.prototype = new Component();
-
-SuspenseList.prototype._suspended = function (child) {
-	const list = this;
-	const delegated = suspended(list._vnode);
-
-	let node = list._map.get(child);
-	node[SUSPENDED_COUNT]++;
-
-	return unsuspend => {
-		const wrappedUnsuspend = () => {
-			if (!list.props.revealOrder) {
-				// Special case the undefined (falsy) revealOrder, as there
-				// is no need to coordinate a specific order or unsuspends.
-				unsuspend();
-			} else {
-				node.push(unsuspend);
-				resolve(list, child, node);
-			}
-		};
-		if (delegated) {
-			delegated(wrappedUnsuspend);
-		} else {
-			wrappedUnsuspend();
-		}
-	};
-};
-
-SuspenseList.prototype.render = function (props) {
-	this._next = null;
-	this._map = new Map();
-
-	const children = toChildArray(props.children);
-	if (props.revealOrder && props.revealOrder[0] === 'b') {
-		// If order === 'backwards' (or, well, anything starting with a 'b')
-		// then flip the child list around so that the last child will be
-		// the first in the linked list.
-		children.reverse();
-	}
-	// Build the linked list. Iterate through the children in reverse order
-	// so that `_next` points to the first linked list node to be resolved.
-	for (let i = children.length; i--; ) {
-		// Create a new linked list node as an array of form:
-		// 	[suspended_count, resolved_count, next_node]
-		// where suspended_count and resolved_count are numeric counters for
-		// keeping track how many times a node has been suspended and resolved.
-		//
-		// Note that suspended_count starts from 1 instead of 0, so we can block
-		// processing callbacks until componentDidMount has been called. In a sense
-		// node is suspended at least until componentDidMount gets called!
-		//
-		// Pending callbacks are added to the end of the node:
-		// 	[suspended_count, resolved_count, next_node, callback_0, callback_1, ...]
-		this._map.set(children[i], (this._next = [1, 0, this._next]));
-	}
-	return props.children;
-};
-
-SuspenseList.prototype.componentDidUpdate =
-	SuspenseList.prototype.componentDidMount = function () {
-		// Iterate through all children after mounting for two reasons:
-		// 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases
-		//    each node[RELEASED_COUNT] by 1, therefore balancing the counters.
-		//    The nodes can now be completely consumed from the linked list.
-		// 2. Handle nodes that might have gotten resolved between render and
-		//    componentDidMount.
-		this._map.forEach((node, child) => {
-			resolve(this, child, node);
-		});
-	};
diff --git a/compat/src/suspense.js b/compat/src/suspense.js
index 3bb60c9eb0..1e5bd55fc6 100644
--- a/compat/src/suspense.js
+++ b/compat/src/suspense.js
@@ -125,8 +125,6 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
 	}
 	c._suspenders.push(suspendingComponent);
 
-	const resolve = suspended(c._vnode);
-
 	let resolved = false;
 	const onResolved = () => {
 		if (resolved) return;
@@ -134,11 +132,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
 		resolved = true;
 		suspendingComponent._onResolve = null;
 
-		if (resolve) {
-			resolve(onSuspensionComplete);
-		} else {
-			onSuspensionComplete();
-		}
+		onSuspensionComplete();
 	};
 
 	suspendingComponent._onResolve = onResolved;
@@ -218,29 +212,6 @@ Suspense.prototype.render = function (props, state) {
 	];
 };
 
-/**
- * Checks and calls the parent component's _suspended method, passing in the
- * suspended vnode. This is a way for a parent (e.g. SuspenseList) to get notified
- * that one of its children/descendants suspended.
- *
- * The parent MAY return a callback. The callback will get called when the
- * suspension resolves, notifying the parent of the fact.
- * Moreover, the callback gets function `unsuspend` as a parameter. The resolved
- * child descendant will not actually get unsuspended until `unsuspend` gets called.
- * This is a way for the parent to delay unsuspending.
- *
- * If the parent does not return a callback then the resolved vnode
- * gets unsuspended immediately when it resolves.
- *
- * @param {import('./internal').VNode} vnode
- * @returns {((unsuspend: () => void) => void)?}
- */
-export function suspended(vnode) {
-	/** @type {import('./internal').Component} */
-	let component = vnode._parent._component;
-	return component && component._suspended && component._suspended(vnode);
-}
-
 export function lazy(loader) {
 	let prom;
 	let component;
@@ -271,6 +242,5 @@ export function lazy(loader) {
 	}
 
 	Lazy.displayName = 'Lazy';
-	Lazy._forwarded = true;
 	return Lazy;
 }
diff --git a/compat/src/util.js b/compat/src/util.js
index 8ec376942b..43c66639a0 100644
--- a/compat/src/util.js
+++ b/compat/src/util.js
@@ -1,14 +1,4 @@
-/**
- * Assign properties from `props` to `obj`
- * @template O, P The obj and props types
- * @param {O} obj The object to copy properties to
- * @param {P} props The object to copy properties from
- * @returns {O & P}
- */
-export function assign(obj, props) {
-	for (let i in props) obj[i] = props[i];
-	return /** @type {O & P} */ (obj);
-}
+export const assign = Object.assign;
 
 /**
  * Check if two objects have a different shape
@@ -22,12 +12,5 @@ export function shallowDiffers(a, b) {
 	return false;
 }
 
-/**
- * Check if two values are the same value
- * @param {*} x
- * @param {*} y
- * @returns {boolean}
- */
-export function is(x, y) {
-	return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
-}
+export const IS_NON_DIMENSIONAL =
+	/^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;
diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js
index de78a1fd20..9aecf754bc 100644
--- a/compat/test/browser/component.test.js
+++ b/compat/test/browser/component.test.js
@@ -75,169 +75,4 @@ describe('components', () => {
 			children: 'second'
 		});
 	});
-
-	describe('UNSAFE_* lifecycle methods', () => {
-		it('should support UNSAFE_componentWillMount', () => {
-			let spy = sinon.spy();
-
-			class Foo extends React.Component {
-				// eslint-disable-next-line camelcase
-				UNSAFE_componentWillMount() {
-					spy();
-				}
-
-				render() {
-					return foo ;
-				}
-			}
-
-			React.render(foo ;
-				}
-			}
-
-			Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillMount', {
-				value: spy
-			});
-
-			React.render(foo ;
-				}
-			}
-
-			React.render(foo ;
-				}
-			}
-
-			Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillReceiveProps', {
-				value: spy
-			});
-
-			React.render(foo ;
-				}
-			}
-
-			React.render(foo ;
-				}
-			}
-
-			Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillUpdate', {
-				value: spy
-			});
-
-			React.render(foo
;
-				}
-			}
-
-			React.render(Example ;
-				}
-			}
-
-			const Wrapper = () => fallback}>
-					 ,
-				scratch
-			);
-
-			expect(scratch.innerHTML).to.equal('Example ');
-			expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called;
-		});
-	});
 });
diff --git a/compat/test/browser/events.test.js b/compat/test/browser/events.test.js
index d59dbef8ed..589dd46f59 100644
--- a/compat/test/browser/events.test.js
+++ b/compat/test/browser/events.test.js
@@ -30,10 +30,6 @@ describe('preact/compat events', () => {
 
 	it('should patch events', () => {
 		let spy = sinon.spy(event => {
-			// Calling ev.preventDefault() outside of an event handler
-			// does nothing in IE11. So we move these asserts inside
-			// the event handler. We ensure that it's called once
-			// in another assertion
 			expect(event.isDefaultPrevented()).to.be.false;
 			event.preventDefault();
 			expect(event.isDefaultPrevented()).to.be.true;
@@ -89,49 +85,16 @@ describe('preact/compat events', () => {
 		expect(vnode.props).to.not.haveOwnProperty('oninputCapture');
 	});
 
-	it('should normalize onChange for range, except in IE11', () => {
-		// NOTE: we don't normalize `onchange` for range inputs in IE11.
-		const eventType = /Trident\//.test(navigator.userAgent)
-			? 'change'
-			: 'input';
-
+	it('should normalize onChange for range', () => {
 		render(
, scratch);
diff --git a/compat/test/browser/exports.test.js b/compat/test/browser/exports.test.js
index cced23da45..d96e4bed4c 100644
--- a/compat/test/browser/exports.test.js
+++ b/compat/test/browser/exports.test.js
@@ -58,7 +58,6 @@ describe('compat exports', () => {
 		expect(Compat.Children.toArray).to.exist.and.be.a('function');
 		expect(Compat.Children.only).to.exist.and.be.a('function');
 		expect(Compat.unmountComponentAtNode).to.exist.and.be.a('function');
-		expect(Compat.unstable_batchedUpdates).to.exist.and.be.a('function');
 		expect(Compat.version).to.exist.and.be.a('string');
 		expect(Compat.startTransition).to.be.a('function');
 	});
@@ -99,7 +98,6 @@ describe('compat exports', () => {
 		expect(Named.Children.toArray).to.exist.and.be.a('function');
 		expect(Named.Children.only).to.exist.and.be.a('function');
 		expect(Named.unmountComponentAtNode).to.exist.and.be.a('function');
-		expect(Named.unstable_batchedUpdates).to.exist.and.be.a('function');
 		expect(Named.version).to.exist.and.be.a('string');
 	});
 });
diff --git a/compat/test/browser/forwardRef.test.js b/compat/test/browser/forwardRef.test.js
index f69d5ae014..4e7968dd6d 100644
--- a/compat/test/browser/forwardRef.test.js
+++ b/compat/test/browser/forwardRef.test.js
@@ -35,7 +35,7 @@ describe('forwardRef', () => {
 		expect(App.prototype.isReactComponent).to.equal(true);
 	});
 
-	it('should have $$typeof property', () => {
+	it.skip('should have $$typeof property', () => {
 		let App = forwardRef((_, ref) => foo
);
 		const expected = getSymbol('react.forward_ref', 0xf47);
 		expect(App.$$typeof).to.equal(expected);
@@ -402,8 +402,7 @@ describe('forwardRef', () => {
 		const Transition = ({ children }) => {
 			const state = useState(0);
 			forceTransition = state[1];
-			expect(children.ref).to.not.be.undefined;
-			if (state[0] === 0) expect(children.props.ref).to.be.undefined;
+			expect(children.props.ref).to.not.be.undefined;
 			return children;
 		};
 
diff --git a/compat/test/browser/memo.test.js b/compat/test/browser/memo.test.js
index 0cbd6fe8cc..89063209e7 100644
--- a/compat/test/browser/memo.test.js
+++ b/compat/test/browser/memo.test.js
@@ -72,9 +72,9 @@ describe('memo()', () => {
 
 		let ref = null;
 
-		function Foo() {
+		function Foo(props) {
 			spy();
-			return Hello World ;
+			return Hello World ;
 		}
 
 		let Memoized = memo(Foo);
@@ -99,8 +99,7 @@ describe('memo()', () => {
 		update();
 		rerender();
 
-		expect(ref.current).not.to.be.undefined;
-
+		expect(ref.current).to.equal(scratch.firstChild);
 		// TODO: not sure whether this is in-line with react...
 		expect(spy).to.be.calledTwice;
 	});
@@ -175,8 +174,8 @@ describe('memo()', () => {
 
 	it('should pass ref through nested memos', () => {
 		class Foo extends Component {
-			render() {
-				return Hello World ;
+			render(props) {
+				return Hello World ;
 			}
 		}
 
@@ -187,7 +186,7 @@ describe('memo()', () => {
 		render({message}
;
+		}
+		TestComponent.defaultProps = {
+			message: 'default message'
+		};
+
+		render({message === null ? 'null value' : message}
;
+		}
+		TestComponent.defaultProps = {
+			message: 'default message'
+		};
+
+		render({message}
;
+		}
+		TestComponent.defaultProps = {
+			message: 'default message'
+		};
+
+		render({message}
;
+		}
+		TestComponent.defaultProps = {
+			message: 'default message'
+		};
+
+		render(
+					
{title} 
+					
{message}
+					
{count} 
+				
 value
+		// that must have a unit. If we encounter a number we append "px" to it.
+		// The list is taken from: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference
+		const unitless = {
+			'border-block': 2,
+			'border-block-end-width': 3,
+			'border-block-start-width': 4,
+			'border-block-width': 5,
+			'border-bottom-left-radius': 6,
+			'border-bottom-right-radius': 7,
+			'border-bottom-width': 8,
+			'border-end-end-radius': 9,
+			'border-end-start-radius': 10,
+			'border-image-outset': 11,
+			'border-image-width': 12,
+			'border-inline': 2,
+			'border-inline-end': 3,
+			'border-inline-end-width': 4,
+			'border-inline-start': 1,
+			'border-inline-start-width': 123,
+			'border-inline-width': 123,
+			'border-left': 123,
+			'border-left-width': 123,
+			'border-radius': 123,
+			'border-right': 123,
+			'border-right-width': 123,
+			'border-spacing': 123,
+			'border-start-end-radius': 123,
+			'border-start-start-radius': 123,
+			'border-top': 123,
+			'border-top-left-radius': 123,
+			'border-top-right-radius': 123,
+			'border-top-width': 123,
+			'border-width': 123,
+			bottom: 123,
+			'column-gap': 123,
+			'column-rule-width': 23,
+			'column-width': 23,
+			'flex-basis': 23,
+			'font-size': 123,
+			'grid-gap': 23,
+			'grid-auto-columns': 123,
+			'grid-auto-rows': 123,
+			'grid-template-columns': 23,
+			'grid-template-rows': 23,
+			height: 123,
+			'inline-size': 23,
+			inset: 23,
+			'inset-block-end': 12,
+			'inset-block-start': 12,
+			'inset-inline-end': 213,
+			'inset-inline-start': 213,
+			left: 213,
+			'letter-spacing': 213,
+			margin: 213,
+			'margin-block': 213,
+			'margin-block-end': 213,
+			'margin-block-start': 213,
+			'margin-bottom': 213,
+			'margin-inline': 213,
+			'margin-inline-end': 213,
+			'margin-inline-start': 213,
+			'margin-left': 213,
+			'margin-right': 213,
+			'margin-top': 213,
+			'mask-position': 23,
+			'mask-size': 23,
+			'max-block-size': 23,
+			'max-height': 23,
+			'max-inline-size': 23,
+			'max-width': 23,
+			'min-block-size': 23,
+			'min-height': 23,
+			'min-inline-size': 23,
+			'min-width': 23,
+			'object-position': 23,
+			'outline-offset': 23,
+			'outline-width': 123,
+			padding: 123,
+			'padding-block': 123,
+			'padding-block-end': 123,
+			'padding-block-start': 123,
+			'padding-bottom': 123,
+			'padding-inline': 123,
+			'padding-inline-end': 123,
+			'padding-inline-start': 123,
+			'padding-left': 123,
+			'padding-right': 123,
+			'padding-top': 123,
+			perspective: 123,
+			right: 123,
+			'scroll-margin': 123,
+			'scroll-margin-block': 123,
+			'scroll-margin-block-start': 123,
+			'scroll-margin-bottom': 123,
+			'scroll-margin-inline': 123,
+			'scroll-margin-inline-end': 123,
+			'scroll-margin-inline-start': 123,
+			'scroll-margin-inline-left': 123,
+			'scroll-margin-inline-right': 123,
+			'scroll-margin-inline-top': 123,
+			'scroll-padding': 123,
+			'scroll-padding-block': 123,
+			'scroll-padding-block-end': 123,
+			'scroll-padding-block-start': 123,
+			'scroll-padding-bottom': 123,
+			'scroll-padding-inline': 123,
+			'scroll-padding-inline-end': 123,
+			'scroll-padding-inline-start': 123,
+			'scroll-padding-left': 123,
+			'scroll-padding-right': 123,
+			'scroll-padding-top': 123,
+			'shape-margin': 123,
+			'text-decoration-thickness': 123,
+			'text-indent': 123,
+			'text-underline-offset': 123,
+			top: 123,
+			'transform-origin': 123,
+			translate: 123,
+			width: 123,
+			'word-spacing': 123
+		};
+
+		// These are all CSS properties that have valid numeric values.
+		// Our appending logic must not be applied here
+		const untouched = {
+			'-webkit-line-clamp': 2,
+			animation: 2,
+			'animation-iteration-count': 3,
+			'border-image': 2,
+			'border-image-slice': 2,
+			'column-count': 2,
+			columns: 2,
+			flex: 1,
+			'flex-grow': 1,
+			'flex-shrink': 1,
+			'font-size-adjust': 123,
+			'font-weight': 12,
+			'grid-column': 2,
+			'grid-column-end': 2,
+			'grid-column-start': 2,
+			'grid-row': 2,
+			'grid-row-end': 2,
+			'grid-row-gap': 23,
+			'grid-row-start': 2,
+			'inital-letter': 2,
+			'line-height': 2,
+			'line-clamp': 2,
+			'mask-border': 2,
+			'mask-border-outset': 2,
+			'mask-border-slice': 2,
+			'mask-border-width': 2,
+			'max-lines': 2,
+			'max-zoom': 2,
+			'min-zoom': 2,
+			opacity: 123,
+			order: 123,
+			orphans: 2,
+			scale: 23,
+			'shape-image-threshold': 23,
+			'tab-size': 23,
+			widows: 123,
+			'z-index': 123,
+			zoom: 123
+		};
+
+		render(
+			
,
+			scratch
+		);
+
+		let style = scratch.firstChild.style;
+
+		// Check properties that MUST not be changed
+		for (const key in unitless) {
+			// Check if css property is supported
+			if (
+				window.CSS &&
+				typeof window.CSS.supports === 'function' &&
+				window.CSS.supports(key, unitless[key])
+			) {
+				expect(
+					String(style[key]).endsWith('px'),
+					`Should append px "${key}: ${unitless[key]}" === "${key}: ${style[key]}"`
+				).to.equal(true);
+			}
+		}
+
+		// Check properties that MUST not be changed
+		for (const key in untouched) {
+			// Check if css property is supported
+			if (
+				window.CSS &&
+				typeof window.CSS.supports === 'function' &&
+				window.CSS.supports(key, untouched[key])
+			) {
+				expect(
+					!String(style[key]).endsWith('px'),
+					`Should be left as is: "${key}: ${untouched[key]}" === "${key}: ${style[key]}"`
+				).to.equal(true);
+			}
+		}
+	});
 });
diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js
index 3526b96918..72b9b23d2e 100644
--- a/compat/test/browser/suspense-hydration.test.js
+++ b/compat/test/browser/suspense-hydration.test.js
@@ -723,61 +723,17 @@ describe('suspense hydration', () => {
 		});
 	});
 
-	// Currently not supported. Hydration doesn't set attributes... but should it
-	// when coming back from suspense if props were updated?
-	it.skip('should hydrate and update attributes with latest props', () => {
-		const originalHtml = 'Count: 0
Lazy count: 0
';
-		scratch.innerHTML = originalHtml;
-		clearLog();
-
-		/** @type {() => void} */
-		let increment;
-		const [Lazy, resolve] = createLazy();
-		function App() {
-			const [count, setCount] = useState(0);
-			increment = () => setCount(c => c + 1);
-
-			return (
-				
-					Count: {count}
-					 
-			);
-		}
-
-		hydrate(Count: .appendChild(#text)']);
-		clearLog();
-
-		increment();
-		rerender();
-
-		expect(scratch.innerHTML).to.equal(
-			'
Count: 1
Lazy count: 0
'
-		);
-		expect(getLog()).to.deep.equal([]);
-		clearLog();
-
-		return resolve(({ count }) => (
-			Lazy count: {count}
-		)).then(() => {
-			rerender();
-			expect(scratch.innerHTML).to.equal(
-				'Count: 1
Lazy count: 1
'
-			);
-			// Re: DOM OP below - Known issue with hydrating merged text nodes
-			expect(getLog()).to.deep.equal(['Lazy count: .appendChild(#text)']);
-			clearLog();
-		});
-	});
-
-	// Currently not supported, but I wrote the test before I realized that so
-	// leaving it here in case we do support it eventually
-	it.skip('should properly hydrate suspense when resolves to a Fragment', () => {
-		const originalHtml = ul([li(0), li(1), li(2), li(3), li(4), li(5)]);
+	it('should properly hydrate suspense when resolves to a Fragment', () => {
+		const originalHtml = ul([
+			li(0),
+			li(1),
+			'',
+			li(2),
+			li(3),
+			'',
+			li(4),
+			li(5)
+		]);
 
 		const listeners = [
 			sinon.spy(),
@@ -809,8 +765,8 @@ describe('suspense hydration', () => {
 			scratch
 		);
 		rerender(); // Flush rerender queue to mimic what preact will really do
-		expect(scratch.innerHTML).to.equal(originalHtml);
 		expect(getLog()).to.deep.equal([]);
+		expect(scratch.innerHTML).to.equal(originalHtml);
 		expect(listeners[5]).not.to.have.been.called;
 
 		clearLog();
@@ -839,4 +795,228 @@ describe('suspense hydration', () => {
 			expect(listeners[5]).to.have.been.calledTwice;
 		});
 	});
+
+	it('should properly hydrate suspense when resolves to a Fragment without children', () => {
+		const originalHtml = ul([
+			li(0),
+			li(1),
+			'',
+			'',
+			li(2),
+			li(3)
+		]);
+
+		const listeners = [sinon.spy(), sinon.spy(), sinon.spy(), sinon.spy()];
+
+		scratch.innerHTML = originalHtml;
+		clearLog();
+
+		const [Lazy, resolve] = createLazy();
+		hydrate(
+			
+				
+					0 
+					1 
+				 
+				
+					 
+				
+					2 
+					3 
+				 
+			
,
+			scratch
+		);
+		rerender(); // Flush rerender queue to mimic what preact will really do
+		expect(getLog()).to.deep.equal([]);
+		expect(scratch.innerHTML).to.equal(originalHtml);
+		expect(listeners[3]).not.to.have.been.called;
+
+		clearLog();
+		scratch.querySelector('li:last-child').dispatchEvent(createEvent('click'));
+		expect(listeners[3]).to.have.been.calledOnce;
+
+		return resolve(() => null).then(() => {
+			rerender();
+			expect(scratch.innerHTML).to.equal(originalHtml);
+			expect(getLog()).to.deep.equal([]);
+			clearLog();
+
+			scratch
+				.querySelector('li:nth-child(2)')
+				.dispatchEvent(createEvent('click'));
+			expect(listeners[1]).to.have.been.calledOnce;
+
+			scratch
+				.querySelector('li:last-child')
+				.dispatchEvent(createEvent('click'));
+			expect(listeners[3]).to.have.been.calledTwice;
+		});
+	});
+
+	it('Should hydrate a fragment with multiple children correctly', () => {
+		scratch.innerHTML = '
Hello
World!
';
+		clearLog();
+
+		const [Lazy, resolve] = createLazy();
+		hydrate(
+			
+				 ,
+			scratch
+		);
+		rerender(); // Flush rerender queue to mimic what preact will really do
+		expect(scratch.innerHTML).to.equal(
+			'Hello
World!
'
+		);
+		expect(getLog()).to.deep.equal([]);
+		clearLog();
+
+		return resolve(() => (
+			<>
+				Hello
+				World!
+			>
+		)).then(() => {
+			rerender();
+			expect(scratch.innerHTML).to.equal(
+				'Hello
World!
'
+			);
+			expect(getLog()).to.deep.equal([]);
+
+			clearLog();
+		});
+	});
+
+	it('Should hydrate a fragment with no children correctly', () => {
+		scratch.innerHTML = 'Hello world
';
+		clearLog();
+
+		const [Lazy, resolve] = createLazy();
+		hydrate(
+			<>
+				
+					 
+				Hello world
+			>,
+			scratch
+		);
+		rerender(); // Flush rerender queue to mimic what preact will really do
+		expect(scratch.innerHTML).to.equal(
+			'Hello world
'
+		);
+		expect(getLog()).to.deep.equal([]);
+		clearLog();
+
+		return resolve(() => null).then(() => {
+			rerender();
+			expect(scratch.innerHTML).to.equal(
+				'Hello world
'
+			);
+			expect(getLog()).to.deep.equal([]);
+
+			clearLog();
+		});
+	});
+
+	it('Should hydrate a fragment with no children correctly deeply', () => {
+		scratch.innerHTML =
+			'Hello world
';
+		clearLog();
+
+		const [Lazy, resolve] = createLazy();
+		const [Lazy2, resolve2] = createLazy();
+		hydrate(
+			<>
+				
+					
+						
+							 
+					 
+				 
+				Hello world
+			>,
+			scratch
+		);
+		rerender(); // Flush rerender queue to mimic what preact will really do
+		expect(scratch.innerHTML).to.equal(
+			'Hello world
'
+		);
+		expect(getLog()).to.deep.equal([]);
+		clearLog();
+
+		return resolve(p => p.children).then(() => {
+			rerender();
+			expect(scratch.innerHTML).to.equal(
+				'Hello world
'
+			);
+			expect(getLog()).to.deep.equal([]);
+
+			clearLog();
+			return resolve2(() => null).then(() => {
+				rerender();
+				expect(scratch.innerHTML).to.equal(
+					'Hello world
'
+				);
+				expect(getLog()).to.deep.equal([]);
+
+				clearLog();
+			});
+		});
+	});
+
+	it('Should hydrate a fragment with multiple children correctly deeply', () => {
+		scratch.innerHTML =
+			'I am
Fragment Hello world
';
+		clearLog();
+
+		const [Lazy, resolve] = createLazy();
+		const [Lazy2, resolve2] = createLazy();
+		hydrate(
+			<>
+				
+					
+						
+							 
+					 
+				 
+				Hello world
+			>,
+			scratch
+		);
+		rerender(); // Flush rerender queue to mimic what preact will really do
+		expect(scratch.innerHTML).to.equal(
+			'I am
Fragment Hello world
'
+		);
+		expect(getLog()).to.deep.equal([]);
+		clearLog();
+
+		return resolve(p => p.children).then(() => {
+			rerender();
+			expect(scratch.innerHTML).to.equal(
+				'I am
Fragment Hello world
'
+			);
+			expect(getLog()).to.deep.equal([]);
+
+			clearLog();
+			return resolve2(() => (
+				<>
+					I am
+					Fragment 
+				>
+			)).then(() => {
+				rerender();
+				expect(scratch.innerHTML).to.equal(
+					'I am
Fragment Hello world
'
+				);
+				expect(getLog()).to.deep.equal([]);
+
+				clearLog();
+			});
+		});
+	});
 });
diff --git a/compat/test/browser/suspense-list.test.js b/compat/test/browser/suspense-list.test.js
deleted file mode 100644
index 9e733c98cb..0000000000
--- a/compat/test/browser/suspense-list.test.js
+++ /dev/null
@@ -1,589 +0,0 @@
-import { setupRerender } from 'preact/test-utils';
-import React, {
-	createElement,
-	render,
-	Component,
-	Suspense,
-	SuspenseList
-} from 'preact/compat';
-import { useState } from 'preact/hooks';
-import { setupScratch, teardown } from '../../../test/_util/helpers';
-
-const h = React.createElement;
-/* eslint-env browser, mocha */
-
-function getSuspendableComponent(text) {
-	let resolve;
-	let resolved = false;
-	const promise = new Promise(_resolve => {
-		resolve = () => {
-			resolved = true;
-			_resolve();
-			return promise;
-		};
-	});
-
-	class LifecycleSuspender extends Component {
-		render() {
-			if (!resolved) {
-				throw promise;
-			}
-			return {text} ;
-		}
-	}
-
-	LifecycleSuspender.resolve = () => {
-		resolve();
-	};
-
-	return LifecycleSuspender;
-}
-
-describe('suspense-list', () => {
-	/** @type {HTMLDivElement} */
-	let scratch,
-		rerender,
-		unhandledEvents = [];
-
-	function onUnhandledRejection(event) {
-		unhandledEvents.push(event);
-	}
-
-	function getSuspenseList(revealOrder) {
-		const A = getSuspendableComponent('A');
-		const B = getSuspendableComponent('B');
-		const C = getSuspendableComponent('C');
-		render(
-			
-				Loading...}>
-					 
-				Loading...}>
-					 
-				Loading...}>
-					 
-			 ,
-			scratch
-		); // Render initial state
-
-		return [A.resolve, B.resolve, C.resolve];
-	}
-
-	function getNestedSuspenseList(outerRevealOrder, innerRevealOrder) {
-		const A = getSuspendableComponent('A');
-		const B = getSuspendableComponent('B');
-		const C = getSuspendableComponent('C');
-		const D = getSuspendableComponent('D');
-
-		render(
-			
-				Loading...}>
-					 
-				
-					Loading...}>
-						 
-					Loading...}>
-						 
-				 
-				Loading...}>
-					 
-			 ,
-			scratch
-		);
-		return [A.resolve, B.resolve, C.resolve, D.resolve];
-	}
-
-	beforeEach(() => {
-		scratch = setupScratch();
-		rerender = setupRerender();
-		unhandledEvents = [];
-
-		if ('onunhandledrejection' in window) {
-			window.addEventListener('unhandledrejection', onUnhandledRejection);
-		}
-	});
-
-	afterEach(() => {
-		teardown(scratch);
-
-		if ('onunhandledrejection' in window) {
-			window.removeEventListener('unhandledrejection', onUnhandledRejection);
-
-			if (unhandledEvents.length) {
-				throw unhandledEvents[0].reason;
-			}
-		}
-	});
-
-	it('should work for single element', async () => {
-		const Component = getSuspendableComponent('A');
-		render(
-			
-				Loading...}>
-					 
-			 ,
-			scratch
-		); // Render initial state
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(`Loading... `);
-
-		await Component.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(`A `);
-	});
-
-	it('should let components appear backwards if no revealOrder is mentioned', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList();
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... B Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... B C `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should let components appear forwards if no revealOrder is mentioned', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList();
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A Loading... Loading... `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should let components appear in forwards if revealOrder=forwards and first one resolves before others', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList('forwards');
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A Loading... Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A Loading... Loading... `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should make components appear together if revealOrder=forwards and others resolves before first', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList('forwards');
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should let components appear backwards if revealOrder=backwards and others resolves before first', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList('backwards');
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... C `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... B C `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should make components appear together if revealOrder=backwards and first one resolves others', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList('backwards');
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... C `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should make components appear together if revealOrder=together and first one resolves others', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList('together');
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should make components appear together if revealOrder=together and second one resolves before others', async () => {
-		const [resolver1, resolver2, resolver3] = getSuspenseList('together');
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver2();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver1();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... `
-		);
-
-		await resolver3();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C `
-		);
-	});
-
-	it('should not do anything to non suspense elements', async () => {
-		const A = getSuspendableComponent('A');
-		const B = getSuspendableComponent('B');
-		render(
-			
-				Loading...}>
-					 
-				foo
-				Loading...}>
-					 
-				bar 
-			 ,
-			scratch
-		);
-
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... foo
Loading... bar `
-		);
-
-		await A.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A foo
Loading... bar `
-		);
-
-		await B.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A foo
B bar `
-		);
-	});
-
-	it('should make sure nested SuspenseList works with forwards', async () => {
-		const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList(
-			'forwards',
-			'forwards'
-		);
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveB();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveA();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B Loading... Loading... `
-		);
-
-		await resolveC();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C Loading... `
-		);
-
-		await resolveD();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C D `
-		);
-	});
-
-	it('should make sure nested SuspenseList works with backwards', async () => {
-		const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList(
-			'forwards',
-			'backwards'
-		);
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveA();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A Loading... Loading... Loading... `
-		);
-
-		await resolveC();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A Loading... C Loading... `
-		);
-
-		await resolveB();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C Loading... `
-		);
-
-		await resolveD();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C D `
-		);
-	});
-
-	it('should make sure nested SuspenseList works with together', async () => {
-		const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList(
-			'together',
-			'forwards'
-		);
-		rerender(); // Re-render with fallback cuz lazy threw
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveA();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveD();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveB();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`Loading... Loading... Loading... Loading... `
-		);
-
-		await resolveC();
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`A B C D `
-		);
-	});
-
-	it('should work with forwards even when a  child does not suspend', async () => {
-		const Component = getSuspendableComponent('A');
-
-		render(
-			
-				Loading...}>
-					
-				 
-				Loading...}>
-					 
-			 ,
-			scratch
-		); // Render initial state
-
-		rerender();
-		expect(scratch.innerHTML).to.eql(`
Loading... `);
-
-		await Component.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(`
A `);
-	});
-
-	it('should work with together even when a  child does not suspend', async () => {
-		const Component = getSuspendableComponent('A');
-
-		render(
-			
-				Loading...}>
-					
-				 
-				Loading...}>
-					 
-			 ,
-			scratch
-		); // Render initial state
-
-		rerender();
-		expect(scratch.innerHTML).to.eql(`
Loading... `);
-
-		await Component.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(`
A `);
-	});
-
-	it('should not suspend resolved children if a new suspense comes in between', async () => {
-		const ComponentA = getSuspendableComponent('A');
-		const ComponentB = getSuspendableComponent('B');
-
-		/** @type {(v) => void} */
-		let showB;
-		function Container() {
-			const [showHidden, setShowHidden] = useState(false);
-			showB = setShowHidden;
-			return (
-				
-					Loading...}>
-						
-					 
-					{showHidden && (
-						Loading...}>
-							 
-					)}
-					Loading...}>
-						 
-				 
-			);
-		}
-		render(
Loading... `);
-
-		await ComponentA.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(`
A `);
-
-		showB(true);
-		rerender();
-		expect(scratch.innerHTML).to.eql(
-			`
Loading... A `
-		);
-
-		await ComponentB.resolve();
-		rerender();
-		expect(scratch.innerHTML).to.eql(`
B A `);
-	});
-});
diff --git a/compat/test/browser/suspense.test.js b/compat/test/browser/suspense.test.js
index d18dc8104f..6b6024855f 100644
--- a/compat/test/browser/suspense.test.js
+++ b/compat/test/browser/suspense.test.js
@@ -272,7 +272,7 @@ describe('suspense', () => {
 	});
 
 	it('lazy should forward refs', () => {
-		const LazyComp = () => Hello from LazyComp
;
+		const LazyComp = props => 
;
 		let ref = {};
 
 		/** @type {() => Promise} */
@@ -298,7 +298,7 @@ describe('suspense', () => {
 
 		return resolve().then(() => {
 			rerender();
-			expect(ref.current.constructor).to.equal(LazyComp);
+			expect(ref.current).to.equal(scratch.firstChild);
 		});
 	});
 
@@ -1675,8 +1675,17 @@ describe('suspense', () => {
 
 		// eslint-disable-next-line react/require-render-return
 		class Suspender extends Component {
+			constructor(props) {
+				super(props);
+				this.state = { promise: new Promise(() => {}) };
+				if (typeof props.ref === 'function') {
+					props.ref(this);
+				} else if (props.ref) {
+					props.ref.current = this;
+				}
+			}
 			render() {
-				throw new Promise(() => {});
+				throw this.state.promise;
 			}
 		}
 
diff --git a/compat/test/browser/textarea.test.js b/compat/test/browser/textarea.test.js
index d4cb83bb2c..d7306a0a40 100644
--- a/compat/test/browser/textarea.test.js
+++ b/compat/test/browser/textarea.test.js
@@ -33,17 +33,10 @@ describe('Textarea', () => {
 		hydrate((null);
 
 	React.useImperativeHandle(ref, () => ({
diff --git a/compat/test/ts/index.tsx b/compat/test/ts/index.tsx
index 2e322931d9..51dbd60adc 100644
--- a/compat/test/ts/index.tsx
+++ b/compat/test/ts/index.tsx
@@ -34,6 +34,8 @@ class SimpleComponentWithContextAsProvider extends React.Component {
 	}
 }
 
+SimpleComponentWithContextAsProvider.defaultProps = { foo: 'default' };
+
 React.render(
 	
-			 
-	);
-}
-
 const Comp = () => Hello world
;
 
 const importComponent = async () => {
diff --git a/config/node-13-exports.js b/config/node-13-exports.js
deleted file mode 100644
index 9528d2aefa..0000000000
--- a/config/node-13-exports.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const fs = require('fs');
-
-const subRepositories = [
-	'compat',
-	'debug',
-	'devtools',
-	'hooks',
-	'jsx-runtime',
-	'test-utils'
-];
-const snakeCaseToCamelCase = str =>
-	str.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', ''));
-
-const copyPreact = () => {
-	// Copy .module.js --> .mjs for Node 13 compat.
-	fs.writeFileSync(
-		`${process.cwd()}/dist/preact.mjs`,
-		fs.readFileSync(`${process.cwd()}/dist/preact.module.js`)
-	);
-};
-
-const copy = name => {
-	// Copy .module.js --> .mjs for Node 13 compat.
-	const filename = name.includes('-') ? snakeCaseToCamelCase(name) : name;
-	fs.writeFileSync(
-		`${process.cwd()}/${name}/dist/${filename}.mjs`,
-		fs.readFileSync(`${process.cwd()}/${name}/dist/${filename}.module.js`)
-	);
-};
-
-copyPreact();
-subRepositories.forEach(copy);
diff --git a/debug/package.json b/debug/package.json
index 836b4b49f7..df80213a7a 100644
--- a/debug/package.json
+++ b/debug/package.json
@@ -5,7 +5,7 @@
   "private": true,
   "description": "Preact extensions for development",
   "main": "dist/debug.js",
-  "module": "dist/debug.module.js",
+  "module": "dist/debug.mjs",
   "umd:main": "dist/debug.umd.js",
   "source": "src/index.js",
   "license": "MIT",
@@ -18,7 +18,7 @@
   "exports": {
     ".": {
       "types": "./src/index.d.ts",
-      "browser": "./dist/debug.module.js",
+      "module": "./dist/debug.mjs",
       "umd": "./dist/debug.umd.js",
       "import": "./dist/debug.mjs",
       "require": "./dist/debug.js"
diff --git a/debug/src/debug.js b/debug/src/debug.js
index 8da063130f..1d8eb7c0e6 100644
--- a/debug/src/debug.js
+++ b/debug/src/debug.js
@@ -11,7 +11,7 @@ import {
 	getCurrentVNode,
 	getDisplayName
 } from './component-stack';
-import { assign, isNaN } from './util';
+import { isNaN } from './util';
 
 const isWeakMapSupported = typeof WeakMap == 'function';
 
@@ -229,15 +229,12 @@ export function initDebug() {
 				}
 			}
 
-			let values = vnode.props;
-			if (vnode.type._forwarded) {
-				values = assign({}, values);
-				delete values.ref;
-			}
+			/* eslint-disable-next-line */
+			const { ref: _ref, ...props } = vnode.props;
 
 			checkPropTypes(
 				vnode.type.propTypes,
-				values,
+				props,
 				'prop',
 				getDisplayName(vnode),
 				() => getOwnerStack(vnode)
diff --git a/debug/src/util.js b/debug/src/util.js
index be4228b9b6..2dddcea736 100644
--- a/debug/src/util.js
+++ b/debug/src/util.js
@@ -1,14 +1,4 @@
-/**
- * Assign properties from `props` to `obj`
- * @template O, P The obj and props types
- * @param {O} obj The object to copy properties to
- * @param {P} props The object to copy properties from
- * @returns {O & P}
- */
-export function assign(obj, props) {
-	for (let i in props) obj[i] = props[i];
-	return /** @type {O & P} */ (obj);
-}
+export const assign = Object.assign;
 
 export function isNaN(value) {
 	return value !== value;
diff --git a/devtools/package.json b/devtools/package.json
index c12ac730f0..353f2ad950 100644
--- a/devtools/package.json
+++ b/devtools/package.json
@@ -5,7 +5,7 @@
   "private": true,
   "description": "Preact bridge for Preact devtools",
   "main": "dist/devtools.js",
-  "module": "dist/devtools.module.js",
+  "module": "dist/devtools.mjs",
   "umd:main": "dist/devtools.umd.js",
   "source": "src/index.js",
   "license": "MIT",
@@ -16,7 +16,7 @@
   "exports": {
     ".": {
       "types": "./src/index.d.ts",
-      "browser": "./dist/devtools.module.js",
+      "module": "./dist/devtools.mjs",
       "umd": "./dist/devtools.umd.js",
       "import": "./dist/devtools.mjs",
       "require": "./dist/devtools.js"
diff --git a/hooks/package.json b/hooks/package.json
index 787927573e..ea32c3fe2f 100644
--- a/hooks/package.json
+++ b/hooks/package.json
@@ -5,7 +5,7 @@
   "private": true,
   "description": "Hook addon for Preact",
   "main": "dist/hooks.js",
-  "module": "dist/hooks.module.js",
+  "module": "dist/hooks.mjs",
   "umd:main": "dist/hooks.umd.js",
   "source": "src/index.js",
   "license": "MIT",
@@ -26,7 +26,7 @@
   "exports": {
     ".": {
       "types": "./src/index.d.ts",
-      "browser": "./dist/hooks.module.js",
+      "module": "./dist/hooks.mjs",
       "umd": "./dist/hooks.umd.js",
       "import": "./dist/hooks.mjs",
       "require": "./dist/hooks.js"
diff --git a/hooks/src/index.d.ts b/hooks/src/index.d.ts
index d7f77dbb49..22147a2bd4 100644
--- a/hooks/src/index.d.ts
+++ b/hooks/src/index.d.ts
@@ -52,13 +52,6 @@ export function useReducer(
 	init: (arg: I) => S
 ): [S, Dispatch];
 
-/** @deprecated Use the `Ref` type instead. */
-type PropRef = MutableRef;
-
-interface MutableRef {
-	current: T;
-}
-
 /**
  * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
  * (`initialValue`). The returned object will persist for the full lifetime of the component.
@@ -68,9 +61,9 @@ interface MutableRef {
  *
  * @param initialValue the initial value to store in the ref object
  */
-export function useRef(initialValue: T): MutableRef;
-export function useRef(initialValue: T | null): RefObject;
-export function useRef(): MutableRef;
+export function useRef(initialValue: T): RefObject;
+export function useRef(initialValue: T | null): RefObject;
+export function useRef(initialValue: T | undefined): RefObject;
 
 type EffectCallback = () => void | (() => void);
 /**
diff --git a/hooks/src/index.js b/hooks/src/index.js
index b98b1988ae..eb52ae8e51 100644
--- a/hooks/src/index.js
+++ b/hooks/src/index.js
@@ -1,4 +1,7 @@
 import { options as _options } from 'preact';
+import { SKIP_CHILDREN } from '../../src/constants';
+
+const ObjectIs = Object.is;
 
 /** @type {number} */
 let currentIndex;
@@ -24,6 +27,7 @@ let oldAfterDiff = options.diffed;
 let oldCommit = options._commit;
 let oldBeforeUnmount = options.unmount;
 let oldRoot = options._root;
+let oldAfterRender = options._afterRender;
 
 // We take the minimum timeout for requestAnimationFrame to ensure that
 // the callback is invoked after the next frame. 35ms is based on a 30hz
@@ -58,10 +62,7 @@ options._render = vnode => {
 			hooks._pendingEffects = [];
 			currentComponent._renderCallbacks = [];
 			hooks._list.forEach(hookItem => {
-				if (hookItem._nextValue) {
-					hookItem._value = hookItem._nextValue;
-				}
-				hookItem._pendingArgs = hookItem._nextValue = undefined;
+				hookItem._pendingArgs = undefined;
 			});
 		} else {
 			hooks._pendingEffects.forEach(invokeCleanup);
@@ -184,19 +185,13 @@ export function useReducer(reducer, initialState, init) {
 	const hookState = getHookState(currentIndex++, 2);
 	hookState._reducer = reducer;
 	if (!hookState._component) {
+		hookState._actions = [];
 		hookState._value = [
 			!init ? invokeOrReturn(undefined, initialState) : init(initialState),
 
 			action => {
-				const currentValue = hookState._nextValue
-					? hookState._nextValue[0]
-					: hookState._value[0];
-				const nextValue = hookState._reducer(currentValue, action);
-
-				if (currentValue !== nextValue) {
-					hookState._nextValue = [nextValue, hookState._value[1]];
-					hookState._component.setState({});
-				}
+				hookState._actions.push(action);
+				hookState._component.setState({});
 			}
 		];
 
@@ -205,74 +200,55 @@ export function useReducer(reducer, initialState, init) {
 		if (!currentComponent._hasScuFromHooks) {
 			currentComponent._hasScuFromHooks = true;
 			let prevScu = currentComponent.shouldComponentUpdate;
-			const prevCWU = currentComponent.componentWillUpdate;
-
-			// If we're dealing with a forced update `shouldComponentUpdate` will
-			// not be called. But we use that to update the hook values, so we
-			// need to call it.
-			currentComponent.componentWillUpdate = function (p, s, c) {
-				if (this._force) {
-					let tmp = prevScu;
-					// Clear to avoid other sCU hooks from being called
-					prevScu = undefined;
-					updateHookState(p, s, c);
-					prevScu = tmp;
-				}
-
-				if (prevCWU) prevCWU.call(this, p, s, c);
+
+			currentComponent.shouldComponentUpdate = function (p, s, c) {
+				return prevScu
+					? prevScu.call(this, p, s, c) || hookState._actions.length
+					: hookState._actions.length;
 			};
+		}
+	}
 
-			// This SCU has the purpose of bailing out after repeated updates
-			// to stateful hooks.
-			// we store the next value in _nextValue[0] and keep doing that for all
-			// state setters, if we have next states and
-			// all next states within a component end up being equal to their original state
-			// we are safe to bail out for this specific component.
-			/**
-			 *
-			 * @type {import('./internal').Component["shouldComponentUpdate"]}
-			 */
-			// @ts-ignore - We don't use TS to downtranspile
-			// eslint-disable-next-line no-inner-declarations
-			function updateHookState(p, s, c) {
-				if (!hookState._component.__hooks) return true;
-
-				/** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
-				const isStateHook = x => !!x._component;
-				const stateHooks =
-					hookState._component.__hooks._list.filter(isStateHook);
-
-				const allHooksEmpty = stateHooks.every(x => !x._nextValue);
-				// When we have no updated hooks in the component we invoke the previous SCU or
-				// traverse the VDOM tree further.
-				if (allHooksEmpty) {
-					return prevScu ? prevScu.call(this, p, s, c) : true;
-				}
-
-				// We check whether we have components with a nextValue set that
-				// have values that aren't equal to one another this pushes
-				// us to update further down the tree
-				let shouldUpdate = hookState._component.props !== p;
-				stateHooks.forEach(hookItem => {
-					if (hookItem._nextValue) {
-						const currentValue = hookItem._value[0];
-						hookItem._value = hookItem._nextValue;
-						hookItem._nextValue = undefined;
-						if (currentValue !== hookItem._value[0]) shouldUpdate = true;
-					}
-				});
+	if (hookState._actions.length) {
+		const initialValue = hookState._value[0];
+		hookState._actions.some(action => {
+			hookState._value[0] = hookState._reducer(hookState._value[0], action);
+		});
 
-				return prevScu
-					? prevScu.call(this, p, s, c) || shouldUpdate
-					: shouldUpdate;
-			}
+		hookState._didUpdate = !ObjectIs(initialValue, hookState._value[0]);
+		hookState._value = [hookState._value[0], hookState._value[1]];
+		hookState._didExecute = true;
+		hookState._actions = [];
+	}
 
-			currentComponent.shouldComponentUpdate = updateHookState;
+	return hookState._value;
+}
+
+options._afterRender = (newVNode, oldVNode) => {
+	if (newVNode._component && newVNode._component.__hooks) {
+		const hooks = newVNode._component.__hooks._list;
+		const stateHooksThatExecuted = hooks.filter(
+			/** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
+			// @ts-expect-error
+			x => x._component && x._didExecute
+		);
+
+		if (
+			stateHooksThatExecuted.length &&
+			!stateHooksThatExecuted.some(x => x._didUpdate) &&
+			oldVNode.props === newVNode.props
+		) {
+			newVNode._component.__hooks._pendingEffects = [];
+			newVNode._flags |= SKIP_CHILDREN;
 		}
+
+		stateHooksThatExecuted.some(hook => {
+			hook._didExecute = hook._didUpdate = false;
+		});
 	}
 
-	return hookState._nextValue || hookState._value;
-}
+	if (oldAfterRender) oldAfterRender(newVNode, oldVNode);
+};
 
 /**
  * @param {import('./internal').Effect} callback
@@ -540,7 +516,7 @@ function argsChanged(oldArgs, newArgs) {
 	return (
 		!oldArgs ||
 		oldArgs.length !== newArgs.length ||
-		newArgs.some((arg, index) => arg !== oldArgs[index])
+		newArgs.some((arg, index) => !ObjectIs(arg, oldArgs[index]))
 	);
 }
 
diff --git a/hooks/src/internal.d.ts b/hooks/src/internal.d.ts
index 76cd97812b..b5b6d66c99 100644
--- a/hooks/src/internal.d.ts
+++ b/hooks/src/internal.d.ts
@@ -4,7 +4,7 @@ import {
 	VNode as PreactVNode,
 	PreactContext,
 	HookType,
-	ErrorInfo,
+	ErrorInfo
 } from '../../src/internal';
 import { Reducer, StateUpdater } from '.';
 
@@ -32,7 +32,8 @@ export interface ComponentHooks {
 	_pendingEffects: EffectHookState[];
 }
 
-export interface Component extends Omit, '_renderCallbacks'> {
+export interface Component
+	extends Omit, '_renderCallbacks'> {
 	__hooks?: ComponentHooks;
 	// Extend to include HookStates
 	_renderCallbacks?: Array void)>;
@@ -54,8 +55,6 @@ export type HookState =
 
 interface BaseHookState {
 	_value?: unknown;
-	_nextValue?: unknown;
-	_pendingValue?: unknown;
 	_args?: unknown;
 	_pendingArgs?: unknown;
 	_component?: unknown;
@@ -74,7 +73,6 @@ export interface EffectHookState extends BaseHookState {
 
 export interface MemoHookState extends BaseHookState {
 	_value?: T;
-	_pendingValue?: T;
 	_args?: unknown[];
 	_pendingArgs?: unknown[];
 	_factory?: () => T;
@@ -82,10 +80,12 @@ export interface MemoHookState extends BaseHookState {
 
 export interface ReducerHookState
 	extends BaseHookState {
-	_nextValue?: [S, StateUpdater];
 	_value?: [S, StateUpdater];
+	_actions?: any[];
 	_component?: Component;
 	_reducer?: Reducer;
+	_didExecute?: boolean;
+	_didUpdate?: boolean;
 }
 
 export interface ContextHookState extends BaseHookState {
diff --git a/hooks/test/browser/useContext.test.js b/hooks/test/browser/useContext.test.js
index 629ab06714..36baaad45c 100644
--- a/hooks/test/browser/useContext.test.js
+++ b/hooks/test/browser/useContext.test.js
@@ -185,6 +185,7 @@ describe('useContext', () => {
 		let provider, subSpy;
 
 		function Comp() {
+			provider = this._vnode._parent._component;
 			const value = useContext(Context);
 			values.push(value);
 			return null;
@@ -193,7 +194,7 @@ describe('useContext', () => {
 		render( (provider = p)} value={42}>
+			
 				 ,
 			scratch
@@ -217,6 +218,7 @@ describe('useContext', () => {
 		let provider, subSpy;
 
 		function Comp() {
+			provider = this._vnode._parent._component;
 			const value = useContext(Context);
 			values.push(value);
 			return null;
@@ -225,7 +227,7 @@ describe('useContext', () => {
 		render( (provider = p)} value={42}>
+			
 				 ,
 			scratch
diff --git a/hooks/test/browser/useEffect.test.js b/hooks/test/browser/useEffect.test.js
index feb554192d..550fc41741 100644
--- a/hooks/test/browser/useEffect.test.js
+++ b/hooks/test/browser/useEffect.test.js
@@ -636,4 +636,26 @@ describe('useEffect', () => {
 		expect(calls.length).to.equal(1);
 		expect(calls).to.deep.equal(['doing effecthi']);
 	});
+
+	it('should not rerun when receiving NaN on subsequent renders', () => {
+		const calls = [];
+		const Component = ({ value }) => {
+			const [count, setCount] = useState(0);
+			useEffect(() => {
+				calls.push('doing effect' + count);
+				setCount(count + 1);
+				return () => {
+					calls.push('cleaning up' + count);
+				};
+			}, [value]);
+			return {count}
;
+		};
+		const App = () => hello world!!!
');
 	});
 
+	it('should exhaust renders when NaN state is set as a result of a props update', () => {
+		const calls = [];
+		const App = ({ i }) => {
+			calls.push('rendering' + i);
+			const [greeting, setGreeting] = useState(0);
+
+			if (i === 2) {
+				setGreeting(NaN);
+			}
+
+			return {greeting}
;
+		};
+
+		act(() => {
+			render({greeting}
;
+		};
+		const App = () => {
+			const [greeting, setGreeting] = useState(0);
+			set = setGreeting;
+
+			return {children} 
+				);
+			};
+
+			return context;
+		}
+
+		function useContextSelector(context) {
+			const contextValue = useContext(context);
+			const {
+				value: { current: value }
+			} = contextValue;
+			const [state, dispatch] = useReducer(
+				() => {
+					return {
+						value
+					};
+				},
+				{
+					value
+				}
+			);
+			useLayoutEffect(() => {
+				contextValue.listener = dispatch;
+			}, []);
+			return state.value;
+		}
+
+		const context = createContext2();
+		let set;
+
+		function Child() {
+			const [count, setState] = useContextSelector(context);
+			const [c, setC] = useState(0);
+			set = () => {
+				setC(s => s + 1);
+				setState(s => s + 1);
+			};
+			return (
+				
+					
Context count: {count}
+					
Local count: {c}
+				
+					 
+			);
+		}
+
+		act(() => {
+			render(} exprs
- * @returns {VNode}
+ * @param  {Array} exprs
+ * @returns {import('preact').VNode}
  */
 function jsxTemplate(templates, ...exprs) {
 	const vnode = createVNode(Fragment, { tpl: templates, exprs });
 	// Bypass render to string top level Fragment optimization
+	// @ts-ignore
 	vnode.key = vnode._vnode;
 	return vnode;
 }
@@ -121,16 +112,7 @@ function jsxAttr(name, value) {
 						: JS_TO_CSS[prop] ||
 							(JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase());
 
-				let suffix = ';';
-				if (
-					typeof val === 'number' &&
-					// Exclude custom-attributes
-					!name.startsWith('--') &&
-					!IS_NON_DIMENSIONAL.test(name)
-				) {
-					suffix = 'px;';
-				}
-				str = str + name + ':' + val + suffix;
+				str = str + name + ':' + val + ';';
 			}
 		}
 		return name + '="' + str + '"';
@@ -153,7 +135,7 @@ function jsxAttr(name, value) {
  * is not expected to be used directly, but rather through a
  * precompile JSX transform
  * @param {*} value
- * @returns {string | null | VNode | Array}
+ * @returns {string | null | import('preact').VNode | Array}
  */
 function jsxEscape(value) {
 	if (
diff --git a/jsx-runtime/test/browser/jsx-runtime.test.js b/jsx-runtime/test/browser/jsx-runtime.test.js
index a5ba0daaf0..d06f8da21d 100644
--- a/jsx-runtime/test/browser/jsx-runtime.test.js
+++ b/jsx-runtime/test/browser/jsx-runtime.test.js
@@ -51,46 +51,6 @@ describe('Babel jsx/jsxDEV', () => {
 		expect(vnode.key).to.equal('foo');
 	});
 
-	it('should apply defaultProps', () => {
-		class Foo extends Component {
-			render() {
-				return 
;
-			}
-		}
-
-		Foo.defaultProps = {
-			foo: 'bar'
-		};
-
-		const vnode = jsx(Foo, {}, null);
-		expect(vnode.props).to.deep.equal({
-			foo: 'bar'
-		});
-	});
-
-	it('should respect defaultProps when props are null', () => {
-		const Component = ({ children }) => children;
-		Component.defaultProps = { foo: 'bar' };
-		expect(jsx(Component, { foo: null }).props).to.deep.equal({ foo: null });
-	});
-
-	it('should keep props over defaultProps', () => {
-		class Foo extends Component {
-			render() {
-				return 
;
-			}
-		}
-
-		Foo.defaultProps = {
-			foo: 'bar'
-		};
-
-		const vnode = jsx(Foo, { foo: 'baz' }, null);
-		expect(vnode.props).to.deep.equal({
-			foo: 'baz'
-		});
-	});
-
 	it('should set __source and __self', () => {
 		const vnode = jsx('div', { class: 'foo' }, 'key', false, 'source', 'self');
 		expect(vnode.__source).to.equal('source');
@@ -173,7 +133,9 @@ describe('precompiled JSX', () => {
 		});
 
 		it('should serialize style object', () => {
-			expect(jsxAttr('style', { padding: 3 })).to.equal('style="padding:3px;"');
+			expect(jsxAttr('style', { padding: '3px' })).to.equal(
+				'style="padding:3px;"'
+			);
 		});
 	});
 
diff --git a/mangle.json b/mangle.json
index 2d15afef53..bfe7a0b10b 100644
--- a/mangle.json
+++ b/mangle.json
@@ -26,22 +26,25 @@
     "cname": 6,
     "props": {
       "$_hasScuFromHooks": "__f",
-      "$_listeners": "l",
+      "$_listeners": "__l",
       "$_cleanup": "__c",
       "$__hooks": "__H",
       "$_hydrationMismatch": "__m",
       "$_list": "__",
       "$_pendingEffects": "__h",
       "$_value": "__",
-      "$_nextValue": "__N",
+      "$_didExecute": "__N",
+      "$_didUpdate": "__U",
       "$_original": "__v",
       "$_args": "__H",
       "$_factory": "__h",
       "$_depth": "__b",
       "$_dirty": "__d",
+      "$_afterRender": "__d",
       "$_mask": "__m",
       "$_detachOnNextRender": "__b",
       "$_force": "__e",
+      "$_excess": "__z",
       "$_nextState": "__s",
       "$_renderCallbacks": "__h",
       "$_stateCallbacks": "_sb",
diff --git a/package.json b/package.json
index 864fe6ec7c..e92cf3dacd 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
   "amdName": "preact",
   "version": "10.26.8",
   "private": false,
-  "description": "Fast 3kb React-compatible Virtual DOM library.",
+  "description": "Fast 4kb React-compatible Virtual DOM library.",
   "main": "dist/preact.js",
-  "module": "dist/preact.module.js",
+  "module": "dist/preact.mjs",
   "umd:main": "dist/preact.umd.js",
   "unpkg": "dist/preact.min.js",
   "source": "src/index.js",
@@ -20,42 +20,42 @@
         "types": "./src/index-5.d.ts"
       },
       "types": "./src/index.d.ts",
-      "browser": "./dist/preact.module.js",
+      "module": "./dist/preact.mjs",
       "umd": "./dist/preact.umd.js",
       "import": "./dist/preact.mjs",
       "require": "./dist/preact.js"
     },
     "./compat": {
       "types": "./compat/src/index.d.ts",
-      "browser": "./compat/dist/compat.module.js",
+      "module": "./compat/dist/compat.mjs",
       "umd": "./compat/dist/compat.umd.js",
       "import": "./compat/dist/compat.mjs",
       "require": "./compat/dist/compat.js"
     },
     "./debug": {
       "types": "./debug/src/index.d.ts",
-      "browser": "./debug/dist/debug.module.js",
+      "module": "./debug/dist/debug.mjs",
       "umd": "./debug/dist/debug.umd.js",
       "import": "./debug/dist/debug.mjs",
       "require": "./debug/dist/debug.js"
     },
     "./devtools": {
       "types": "./devtools/src/index.d.ts",
-      "browser": "./devtools/dist/devtools.module.js",
+      "module": "./devtools/dist/devtools.mjs",
       "umd": "./devtools/dist/devtools.umd.js",
       "import": "./devtools/dist/devtools.mjs",
       "require": "./devtools/dist/devtools.js"
     },
     "./hooks": {
       "types": "./hooks/src/index.d.ts",
-      "browser": "./hooks/dist/hooks.module.js",
+      "module": "./hooks/dist/hooks.mjs",
       "umd": "./hooks/dist/hooks.umd.js",
       "import": "./hooks/dist/hooks.mjs",
       "require": "./hooks/dist/hooks.js"
     },
     "./test-utils": {
       "types": "./test-utils/src/index.d.ts",
-      "browser": "./test-utils/dist/testUtils.module.js",
+      "module": "./test-utils/dist/testUtils.mjs",
       "umd": "./test-utils/dist/testUtils.umd.js",
       "import": "./test-utils/dist/testUtils.mjs",
       "require": "./test-utils/dist/testUtils.js"
@@ -69,14 +69,14 @@
     },
     "./jsx-runtime": {
       "types": "./jsx-runtime/src/index.d.ts",
-      "browser": "./jsx-runtime/dist/jsxRuntime.module.js",
+      "module": "./jsx-runtime/dist/jsxRuntime.mjs",
       "umd": "./jsx-runtime/dist/jsxRuntime.umd.js",
       "import": "./jsx-runtime/dist/jsxRuntime.mjs",
       "require": "./jsx-runtime/dist/jsxRuntime.js"
     },
     "./jsx-dev-runtime": {
       "types": "./jsx-runtime/src/index.d.ts",
-      "browser": "./jsx-runtime/dist/jsxRuntime.module.js",
+      "module": "./jsx-runtime/dist/jsxRuntime.mjs",
       "umd": "./jsx-runtime/dist/jsxRuntime.umd.js",
       "import": "./jsx-runtime/dist/jsxRuntime.mjs",
       "require": "./jsx-runtime/dist/jsxRuntime.js"
@@ -123,14 +123,13 @@
     "prepare": "husky && run-s build",
     "build": "npm-run-all --parallel 'build:*'",
     "build:core": "microbundle build --raw --no-generateTypes -f cjs,esm,umd",
-    "build:core-min": "microbundle build --raw --no-generateTypes -f cjs,esm,umd,iife src/cjs.js -o dist/preact.min.js",
     "build:debug": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd debug",
     "build:devtools": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd devtools",
     "build:hooks": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd hooks",
     "build:test-utils": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd test-utils",
     "build:compat": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd compat --globals 'preact/hooks=preactHooks'",
     "build:jsx": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd jsx-runtime",
-    "postbuild": "node ./config/node-13-exports.js && node ./config/compat-entries.js",
+    "postbuild": "node ./config/compat-entries.js",
     "dev": "microbundle watch --raw --no-generateTypes --format cjs",
     "dev:hooks": "microbundle watch --raw --no-generateTypes --format cjs --cwd hooks",
     "dev:compat": "microbundle watch --raw --no-generateTypes --format cjs --cwd compat --globals 'preact/hooks=preactHooks'",
diff --git a/src/cjs.js b/src/cjs.js
deleted file mode 100644
index b4721b1d44..0000000000
--- a/src/cjs.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import * as preact from './index.js';
-if (typeof module < 'u') module.exports = preact;
-else self.preact = preact;
diff --git a/src/clone-element.js b/src/clone-element.js
index 671eb4e63f..b2f6e118cf 100644
--- a/src/clone-element.js
+++ b/src/clone-element.js
@@ -1,6 +1,6 @@
 import { assign, slice } from './util';
 import { createVNode } from './create-element';
-import { NULL, UNDEFINED } from './constants';
+import { NULL } from './constants';
 
 /**
  * Clones the given VNode, optionally adding attributes/props and replacing its
@@ -17,20 +17,10 @@ export function cloneElement(vnode, props, children) {
 		ref,
 		i;
 
-	let defaultProps;
-
-	if (vnode.type && vnode.type.defaultProps) {
-		defaultProps = vnode.type.defaultProps;
-	}
-
 	for (i in props) {
 		if (i == 'key') key = props[i];
-		else if (i == 'ref') ref = props[i];
-		else if (props[i] === UNDEFINED && defaultProps != UNDEFINED) {
-			normalizedProps[i] = defaultProps[i];
-		} else {
-			normalizedProps[i] = props[i];
-		}
+		else if (i == 'ref' && typeof vnode.type != 'function') ref = props[i];
+		else normalizedProps[i] = props[i];
 	}
 
 	if (arguments.length > 2) {
diff --git a/src/component.js b/src/component.js
index b233d8c7c0..9b1327629b 100644
--- a/src/component.js
+++ b/src/component.js
@@ -159,11 +159,11 @@ function renderComponent(component) {
  */
 function updateParentDomPointers(vnode) {
 	if ((vnode = vnode._parent) != NULL && vnode._component != NULL) {
-		vnode._dom = vnode._component.base = NULL;
+		vnode._dom = NULL;
 		for (let i = 0; i < vnode._children.length; i++) {
 			let child = vnode._children[i];
 			if (child != NULL && child._dom != NULL) {
-				vnode._dom = vnode._component.base = child._dom;
+				vnode._dom = child._dom;
 				break;
 			}
 		}
@@ -189,11 +189,6 @@ let rerenderQueue = [];
 
 let prevDebounce;
 
-const defer =
-	typeof Promise == 'function'
-		? Promise.prototype.then.bind(Promise.resolve())
-		: setTimeout;
-
 /**
  * Enqueue a rerender of a component
  * @param {import('./internal').Component} c The component to rerender
@@ -207,7 +202,7 @@ export function enqueueRender(c) {
 		prevDebounce != options.debounceRendering
 	) {
 		prevDebounce = options.debounceRendering;
-		(prevDebounce || defer)(process);
+		(prevDebounce || queueMicrotask)(process);
 	}
 }
 
diff --git a/src/constants.js b/src/constants.js
index c60df07b93..57d56d98c6 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -6,6 +6,8 @@ export const MODE_SUSPENDED = 1 << 7;
 export const INSERT_VNODE = 1 << 2;
 /** Indicates a VNode has been matched with another VNode in the diff */
 export const MATCHED = 1 << 1;
+/** Indicates that children should not be diffed */
+export const SKIP_CHILDREN = 1 << 3;
 
 /** Reset all mode flags */
 export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED);
@@ -18,5 +20,3 @@ export const NULL = null;
 export const UNDEFINED = undefined;
 export const EMPTY_OBJ = /** @type {any} */ ({});
 export const EMPTY_ARR = [];
-export const IS_NON_DIMENSIONAL =
-	/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
diff --git a/src/create-element.js b/src/create-element.js
index d4ea2dd935..0c0990ff60 100644
--- a/src/create-element.js
+++ b/src/create-element.js
@@ -20,7 +20,7 @@ export function createElement(type, props, children) {
 		i;
 	for (i in props) {
 		if (i == 'key') key = props[i];
-		else if (i == 'ref') ref = props[i];
+		else if (i == 'ref' && typeof type != 'function') ref = props[i];
 		else normalizedProps[i] = props[i];
 	}
 
@@ -29,16 +29,6 @@ export function createElement(type, props, children) {
 			arguments.length > 3 ? slice.call(arguments, 2) : children;
 	}
 
-	// If a Component VNode, check for and apply defaultProps
-	// Note: type may be undefined in development, must never error here.
-	if (typeof type == 'function' && type.defaultProps != NULL) {
-		for (i in type.defaultProps) {
-			if (normalizedProps[i] === UNDEFINED) {
-				normalizedProps[i] = type.defaultProps[i];
-			}
-		}
-	}
-
 	return createVNode(type, normalizedProps, key, ref, NULL);
 }
 
diff --git a/src/diff/children.js b/src/diff/children.js
index 25ebcd3660..7f401a0373 100644
--- a/src/diff/children.js
+++ b/src/diff/children.js
@@ -358,7 +358,7 @@ function insert(parentVNode, oldDom, parentDom) {
 
 		return oldDom;
 	} else if (parentVNode._dom != oldDom) {
-		if (oldDom && parentVNode.type && !parentDom.contains(oldDom)) {
+		if (oldDom && parentVNode.type && !oldDom.parentNode) {
 			oldDom = getDomSibling(parentVNode);
 		}
 		parentDom.insertBefore(parentVNode._dom, oldDom || NULL);
diff --git a/src/diff/index.js b/src/diff/index.js
index 5603586ae6..d9a62cbae5 100644
--- a/src/diff/index.js
+++ b/src/diff/index.js
@@ -5,6 +5,7 @@ import {
 	MODE_SUSPENDED,
 	NULL,
 	RESET_MODE,
+	SKIP_CHILDREN,
 	SVG_NAMESPACE,
 	UNDEFINED,
 	XHTML_NAMESPACE
@@ -69,8 +70,11 @@ export function diff(
 	// If the previous diff bailed out, resume creating/hydrating.
 	if (oldVNode._flags & MODE_SUSPENDED) {
 		isHydrating = !!(oldVNode._flags & MODE_HYDRATE);
-		oldDom = newVNode._dom = oldVNode._dom;
-		excessDomChildren = [oldDom];
+		if (oldVNode._component._excess) {
+			excessDomChildren = oldVNode._component._excess;
+			oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0];
+			oldVNode._component._excess = null;
+		}
 	}
 
 	if ((tmp = options._diff)) tmp(newVNode);
@@ -220,6 +224,7 @@ export function diff(
 			c._force = false;
 
 			let renderHook = options._render,
+				afterRender = options._afterRender,
 				count = 0;
 			if (isClassComponent) {
 				c.state = c._nextState;
@@ -228,6 +233,7 @@ export function diff(
 				if (renderHook) renderHook(newVNode);
 
 				tmp = c.render(c.props, c.state, c.context);
+				if (afterRender) afterRender(newVNode, oldVNode);
 
 				for (let i = 0; i < c._stateCallbacks.length; i++) {
 					c._renderCallbacks.push(c._stateCallbacks[i]);
@@ -239,6 +245,18 @@ export function diff(
 					if (renderHook) renderHook(newVNode);
 
 					tmp = c.render(c.props, c.state, c.context);
+					if (afterRender) afterRender(newVNode, oldVNode);
+
+					if (newVNode._flags & SKIP_CHILDREN) {
+						c._dirty = false;
+						c._renderCallbacks = [];
+						newVNode._dom = oldVNode._dom;
+						newVNode._children = oldVNode._children;
+						newVNode._children.some(vnode => {
+							if (vnode) vnode._parent = newVNode;
+						});
+						break outer;
+					}
 
 					// Handle setState called in render, see #2553
 					c.state = c._nextState;
@@ -249,7 +267,7 @@ export function diff(
 			c.state = c._nextState;
 
 			if (c.getChildContext != NULL) {
-				globalContext = assign(assign({}, globalContext), c.getChildContext());
+				globalContext = assign({}, globalContext, c.getChildContext());
 			}
 
 			if (isClassComponent && !isNew && c.getSnapshotBeforeUpdate != NULL) {
@@ -278,8 +296,6 @@ export function diff(
 				refQueue
 			);
 
-			c.base = newVNode._dom;
-
 			// We successfully rendered this VNode, unset any stored hydration/bailout state:
 			newVNode._flags &= RESET_MODE;
 
@@ -295,15 +311,54 @@ export function diff(
 			// if hydrating or creating initial tree, bailout preserves DOM:
 			if (isHydrating || excessDomChildren != NULL) {
 				if (e.then) {
+					let commentMarkersToFind = 0,
+						done = false;
+
 					newVNode._flags |= isHydrating
 						? MODE_HYDRATE | MODE_SUSPENDED
 						: MODE_SUSPENDED;
 
-					while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) {
-						oldDom = oldDom.nextSibling;
+					newVNode._component._excess = [];
+					for (let i = 0; i < excessDomChildren.length; i++) {
+						let child = excessDomChildren[i];
+						if (child == NULL || done) continue;
+
+						// When we encounter a boundary with $s we are opening
+						// a boundary, this implies that we need to bump
+						// the amount of markers we need to find before closing
+						// the outer boundary.
+						// We exclude the open and closing marker from
+						// the future excessDomChildren but any nested one
+						// needs to be included for future suspensions.
+						if (child.nodeType == 8 && child.data == '$s') {
+							if (commentMarkersToFind > 0) {
+								newVNode._component._excess.push(child);
+							}
+							commentMarkersToFind++;
+							excessDomChildren[i] = NULL;
+						} else if (child.nodeType == 8 && child.data == '/$s') {
+							commentMarkersToFind--;
+							if (commentMarkersToFind > 0) {
+								newVNode._component._excess.push(child);
+							}
+							done = commentMarkersToFind === 0;
+							oldDom = excessDomChildren[i];
+							excessDomChildren[i] = NULL;
+						} else if (commentMarkersToFind > 0) {
+							newVNode._component._excess.push(child);
+							excessDomChildren[i] = NULL;
+						}
+					}
+
+					if (!done) {
+						while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) {
+							oldDom = oldDom.nextSibling;
+						}
+
+						excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL;
+						newVNode._component._excess = [oldDom];
 					}
 
-					excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL;
 					newVNode._dom = oldDom;
 				} else {
 					for (let i = excessDomChildren.length; i--; ) {
@@ -316,12 +371,6 @@ export function diff(
 			}
 			options._catchError(e, newVNode, oldVNode);
 		}
-	} else if (
-		excessDomChildren == NULL &&
-		newVNode._original == oldVNode._original
-	) {
-		newVNode._children = oldVNode._children;
-		newVNode._dom = oldVNode._dom;
 	} else {
 		oldDom = newVNode._dom = diffElementNodes(
 			oldVNode._dom,
@@ -580,12 +629,7 @@ function diffElementNodes(
 				// despite the attribute not being present. When the attribute
 				// is missing the progress bar is treated as indeterminate.
 				// To fix that we'll always update it when it is 0 for progress elements
-				(inputValue !== dom[i] ||
-					(nodeType == 'progress' && !inputValue) ||
-					// This is only for IE 11 to fix  value not being updated.
-					// To avoid a stale select value we need to set the option.value
-					// again, which triggers IE11 to re-evaluate the select value
-					(nodeType == 'option' && inputValue != oldProps[i]))
+				(inputValue !== dom[i] || (nodeType === 'progress' && !inputValue))
 			) {
 				setProperty(dom, i, inputValue, oldProps[i], namespace);
 			}
@@ -653,7 +697,7 @@ export function unmount(vnode, parentVNode, skipRemove) {
 			}
 		}
 
-		r.base = r._parentDom = NULL;
+		r._parentDom = NULL;
 	}
 
 	if ((r = vnode._children)) {
diff --git a/src/diff/props.js b/src/diff/props.js
index 31bbb5daf5..ed17f763fa 100644
--- a/src/diff/props.js
+++ b/src/diff/props.js
@@ -1,4 +1,4 @@
-import { IS_NON_DIMENSIONAL, NULL, SVG_NAMESPACE } from '../constants';
+import { NULL, SVG_NAMESPACE } from '../constants';
 import options from '../options';
 
 function setStyle(style, key, value) {
@@ -6,10 +6,8 @@ function setStyle(style, key, value) {
 		style.setProperty(key, value == NULL ? '' : value);
 	} else if (value == NULL) {
 		style[key] = '';
-	} else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) {
-		style[key] = value;
 	} else {
-		style[key] = value + 'px';
+		style[key] = value;
 	}
 }
 
@@ -67,11 +65,9 @@ export function setProperty(dom, name, value, oldValue, namespace) {
 	// Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6
 	else if (name[0] == 'o' && name[1] == 'n') {
 		useCapture = name != (name = name.replace(CAPTURE_REGEX, '$1'));
-		const lowerCaseName = name.toLowerCase();
 
 		// Infer correct casing for DOM built-in events:
-		if (lowerCaseName in dom || name == 'onFocusOut' || name == 'onFocusIn')
-			name = lowerCaseName.slice(2);
+		if (name[2].toLowerCase() != name[2]) name = name.toLowerCase().slice(2);
 		else name = name.slice(2);
 
 		if (!dom._listeners) dom._listeners = {};
diff --git a/src/index-5.d.ts b/src/index-5.d.ts
index 6c7431cb00..1431b01f30 100644
--- a/src/index-5.d.ts
+++ b/src/index-5.d.ts
@@ -89,14 +89,12 @@ export type ComponentProps<
 export interface FunctionComponent {
 	(props: RenderableProps
, context?: any): VNode | null;
 	displayName?: string;
-	defaultProps?: Partial
 | undefined;
 }
 export interface FunctionalComponent
 extends FunctionComponent
 {}
 
 export interface ComponentClass
 {
 	new (props: P, context?: any): Component
;
 	displayName?: string;
-	defaultProps?: Partial
;
 	contextType?: Context;
 	getDerivedStateFromProps?(
 		props: Readonly,
@@ -141,7 +139,6 @@ export abstract class Component
 {
 	constructor(props?: P, context?: any);
 
 	static displayName?: string;
-	static defaultProps?: any;
 	static contextType?: Context;
 
 	// Static members cannot reference class type parameters. This is not
@@ -293,7 +290,6 @@ interface ContainerNode {
 	readonly firstChild: ContainerNode | null;
 	readonly childNodes: ArrayLike;
 
-	contains(other: ContainerNode | null): boolean;
 	insertBefore(node: ContainerNode, child: ContainerNode | null): ContainerNode;
 	appendChild(node: ContainerNode): ContainerNode;
 	removeChild(child: ContainerNode): ContainerNode;
@@ -348,6 +344,7 @@ export interface Options {
 	debounceRendering?(cb: () => void): void;
 	useDebugValue?(value: string | number): void;
 	_addHookName?(name: string | number): void;
+	_afterRender?(newVNode: VNode, oldVNode: VNode): void;
 	__suspenseDidResolve?(vnode: VNode, cb: () => void): void;
 	// __canSuspenseResolve?(vnode: VNode, cb: () => void): void;
 
diff --git a/src/index.d.ts b/src/index.d.ts
index 379bbebaaf..e6b8fd549e 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -37,9 +37,9 @@ export interface VNode {
 
 export type Key = string | number | any;
 
-export type RefObject = { current: T | null };
+export type RefObject = { current: T };
 export type RefCallback = (instance: T | null) => void;
-export type Ref = RefObject | RefCallback | null;
+export type Ref = RefCallback | RefObject | null;
 
 export type ComponentChild =
 	| VNode
@@ -89,14 +89,12 @@ export type ComponentProps<
 export interface FunctionComponent {
 	(props: RenderableProps
, context?: any): ComponentChildren;
 	displayName?: string;
-	defaultProps?: Partial
 | undefined;
 }
 export interface FunctionalComponent
 extends FunctionComponent
 {}
 
 export interface ComponentClass
 {
 	new (props: P, context?: any): Component
;
 	displayName?: string;
-	defaultProps?: Partial
;
 	contextType?: Context;
 	getDerivedStateFromProps?(
 		props: Readonly,
@@ -141,7 +139,6 @@ export abstract class Component
 {
 	constructor(props?: P, context?: any);
 
 	static displayName?: string;
-	static defaultProps?: any;
 	static contextType?: Context;
 
 	// Static members cannot reference class type parameters. This is not
@@ -159,7 +156,6 @@ export abstract class Component {
 	state: Readonly;
 	props: RenderableProps 
;
 	context: any;
-	base?: Element | Text;
 
 	// From https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/react/index.d.ts#L402
 	// // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
@@ -293,23 +289,12 @@ interface ContainerNode {
 	readonly firstChild: ContainerNode | null;
 	readonly childNodes: ArrayLike;
 
-	contains(other: ContainerNode | null): boolean;
 	insertBefore(node: ContainerNode, child: ContainerNode | null): ContainerNode;
 	appendChild(node: ContainerNode): ContainerNode;
 	removeChild(child: ContainerNode): ContainerNode;
 }
 
 export function render(vnode: ComponentChild, parent: ContainerNode): void;
-/**
- * @deprecated Will be removed in v11.
- *
- * Replacement Preact 10+ implementation can be found here: https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
- */
-export function render(
-	vnode: ComponentChild,
-	parent: ContainerNode,
-	replaceNode?: Element | Text
-): void;
 export function hydrate(vnode: ComponentChild, parent: ContainerNode): void;
 export function cloneElement(
 	vnode: VNode,
diff --git a/src/internal.d.ts b/src/internal.d.ts
index 85e704be65..403fe37dbb 100644
--- a/src/internal.d.ts
+++ b/src/internal.d.ts
@@ -27,6 +27,8 @@ export interface ErrorInfo {
 }
 
 export interface Options extends preact.Options {
+	/** Attach a hook that is invoked after a vnode has rendered. */
+	_afterRender?(vnode: VNode, oldVNode: VNode): void;
 	/** Attach a hook that is invoked before render, mainly to check the arguments. */
 	_root?(vnode: ComponentChild, parent: preact.ContainerNode): void;
 	/** Attach a hook that is invoked before a vnode is diffed. */
@@ -62,8 +64,7 @@ export type ComponentChild =
 	| undefined;
 export type ComponentChildren = ComponentChild[] | ComponentChild;
 
-export interface FunctionComponent
-	extends preact.FunctionComponent
 {
+export interface FunctionComponent
 extends preact.FunctionComponent
 {
 	// Internally, createContext uses `contextType` on a Function component to
 	// implement the Consumer component
 	contextType?: PreactContext;
@@ -95,6 +96,7 @@ export interface PreactElement extends preact.ContainerNode {
 	data?: CharacterData['data'];
 	// Property to set __dangerouslySetInnerHTML
 	innerHTML?: Element['innerHTML'];
+	remove?: Element['remove'];
 
 	// Attribute reading and setting
 	readonly attributes?: Element['attributes'];
@@ -139,9 +141,7 @@ type RefCallback = {
 export type Ref = RefObject | RefCallback;
 
 export interface VNode extends preact.VNode
 {
-	// Redefine type here using our internal ComponentType type, and specify
-	// string has an undefined `defaultProps` property to make TS happy
-	type: (string & { defaultProps: undefined }) | ComponentType
;
+	type: string | ComponentType
;
 	props: P & { children: ComponentChildren };
 	ref?: Ref | null;
 	_children: Array> | null;
@@ -158,12 +158,13 @@ export interface VNode extends preact.VNode
 {
 	_flags: number;
 }
 
-export interface Component
 extends Omit, 'base'> {
+export interface Component
+	extends Omit, 'base'> {
 	// When component is functional component, this is reset to functional component
 	constructor: ComponentType;
 	state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks
-	base?: PreactElement;
 
+	_excess?: PreactElement[];
 	_dirty: boolean;
 	_force?: boolean;
 	_renderCallbacks: Array<() => void>; // Only class components
diff --git a/src/jsx.d.ts b/src/jsx.d.ts
index 0cf3c6a79b..ad17309c2d 100644
--- a/src/jsx.d.ts
+++ b/src/jsx.d.ts
@@ -1552,6 +1552,8 @@ export namespace JSXInternal {
 		translate?: Signalish;
 
 		// WAI-ARIA Attributes
+		// Most elements only allow a subset of roles and so this
+		// is overwritten in many of the per-element interfaces below
 		role?: Signalish;
 
 		// Non-standard Attributes
@@ -1601,10 +1603,9 @@ export namespace JSXInternal {
 		| '_top'
 		| (string & {});
 
-	interface AnchorHTMLAttributes
+	interface PartialAnchorHTMLAttributes
 		extends HTMLAttributes {
 		download?: Signalish;
-		href?: Signalish;
 		hreflang?: Signalish;
 		hrefLang?: Signalish;
 		media?: Signalish;
@@ -1616,12 +1617,44 @@ export namespace JSXInternal {
 		referrerPolicy?: Signalish;
 	}
 
-	interface AreaHTMLAttributes
+	type AnchorAriaRoles =
+		| {
+				href: Signalish;
+				role?: Signalish<
+					| 'link'
+					| 'button'
+					| 'checkbox'
+					| 'menuitem'
+					| 'menuitemcheckbox'
+					| 'menuitemradio'
+					| 'option'
+					| 'radio'
+					| 'switch'
+					| 'tab'
+					| 'treeitem'
+					| 'doc-backlink'
+					| 'doc-biblioref'
+					| 'doc-glossref'
+					| 'doc-noteref'
+					| undefined
+				>;
+		  }
+		| {
+				href?: never;
+				role?: Signalish;
+		  };
+
+	type AnchorHTMLAttributes = Omit<
+		PartialAnchorHTMLAttributes,
+		'role'
+	> &
+		AnchorAriaRoles;
+
+	interface PartialAreaHTMLAttributes
 		extends HTMLAttributes {
 		alt?: Signalish;
 		coords?: Signalish;
 		download?: Signalish;
-		href?: Signalish;
 		hreflang?: Signalish;
 		hrefLang?: Signalish;
 		media?: Signalish;
@@ -1632,12 +1665,66 @@ export namespace JSXInternal {
 		target?: Signalish;
 	}
 
+	type AreaAriaRoles =
+		| {
+				href: Signalish;
+				role?: Signalish<'link' | undefined>;
+		  }
+		| {
+				href?: never;
+				role?: Signalish<'button' | 'link' | undefined>;
+		  };
+
+	type AreaHTMLAttributes = Omit<
+		PartialAreaHTMLAttributes,
+		'role'
+	> &
+		AreaAriaRoles;
+
+	interface ArticleHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<
+			| 'article'
+			| 'application'
+			| 'document'
+			| 'feed'
+			| 'main'
+			| 'none'
+			| 'presentation'
+			| 'region'
+			| undefined
+		>;
+	}
+
+	interface AsideHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<
+			| 'complementary'
+			| 'feed'
+			| 'none'
+			| 'note'
+			| 'presentation'
+			| 'region'
+			| 'search'
+			| 'doc-dedication'
+			| 'doc-example'
+			| 'doc-footnote'
+			| 'doc-glossary'
+			| 'doc-pullquote'
+			| 'doc-tip'
+			| undefined
+		>;
+	}
+
 	interface AudioHTMLAttributes
-		extends MediaHTMLAttributes {}
+		extends MediaHTMLAttributes {
+		role?: Signalish<'application' | undefined>;
+	}
 
 	interface BaseHTMLAttributes
 		extends HTMLAttributes {
 		href?: Signalish;
+		role?: never;
 		target?: Signalish;
 	}
 
@@ -1646,6 +1733,11 @@ export namespace JSXInternal {
 		cite?: Signalish;
 	}
 
+	interface BrHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'none' | 'presentation' | undefined>;
+	}
+
 	interface ButtonHTMLAttributes
 		extends HTMLAttributes {
 		command?: Signalish;
@@ -1668,6 +1760,24 @@ export namespace JSXInternal {
 		popoverTarget?: Signalish;
 		popovertargetaction?: Signalish<'hide' | 'show' | 'toggle' | undefined>;
 		popoverTargetAction?: Signalish<'hide' | 'show' | 'toggle' | undefined>;
+		role?: Signalish<
+			| 'button'
+			| 'checkbox'
+			| 'combobox'
+			| 'gridcell'
+			| 'link'
+			| 'menuitem'
+			| 'menuitemcheckbox'
+			| 'menuitemradio'
+			| 'option'
+			| 'radio'
+			| 'separator'
+			| 'slider'
+			| 'switch'
+			| 'tab'
+			| 'treeitem'
+			| undefined
+		>;
 		type?: Signalish<'submit' | 'reset' | 'button' | undefined>;
 		value?: Signalish;
 	}
@@ -1678,14 +1788,21 @@ export namespace JSXInternal {
 		width?: Signalish;
 	}
 
+	interface CaptionHTMLAttributes
+		extends HTMLAttributes {
+		role?: 'caption';
+	}
+
 	interface ColHTMLAttributes
 		extends HTMLAttributes {
+		role?: never;
 		span?: Signalish;
 		width?: Signalish;
 	}
 
 	interface ColgroupHTMLAttributes
 		extends HTMLAttributes {
+		role?: never;
 		span?: Signalish;
 	}
 
@@ -1694,6 +1811,16 @@ export namespace JSXInternal {
 		value?: Signalish;
 	}
 
+	interface DataListHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'listbox' | undefined>;
+	}
+
+	interface DdHTMLAttributes
+		extends HTMLAttributes {
+		role?: never;
+	}
+
 	interface DelHTMLAttributes
 		extends HTMLAttributes {
 		cite?: Signalish;
@@ -1705,6 +1832,7 @@ export namespace JSXInternal {
 		extends HTMLAttributes {
 		name?: Signalish;
 		open?: Signalish;
+		role?: Signalish<'group' | undefined>;
 	}
 
 	interface DialogHTMLAttributes
@@ -1714,11 +1842,25 @@ export namespace JSXInternal {
 		open?: Signalish;
 		closedby?: Signalish<'none' | 'closerequest' | 'any' | undefined>;
 		closedBy?: Signalish<'none' | 'closerequest' | 'any' | undefined>;
+		role?: Signalish<'dialog' | 'alertdialog' | undefined>;
+	}
+
+	interface DlHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'group' | 'list' | 'none' | 'presentation' | undefined>;
+	}
+
+	interface DtHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'listitem' | undefined>;
 	}
 
 	interface EmbedHTMLAttributes
 		extends HTMLAttributes {
 		height?: Signalish;
+		role?: Signalish<
+			'application' | 'document' | 'img' | 'none' | 'presentation' | undefined
+		>;
 		src?: Signalish;
 		type?: Signalish;
 		width?: Signalish;
@@ -1729,6 +1871,26 @@ export namespace JSXInternal {
 		disabled?: Signalish;
 		form?: Signalish;
 		name?: Signalish;
+		role?: Signalish<
+			'group' | 'none' | 'presentation' | 'radiogroup' | undefined
+		>;
+	}
+
+	interface FigcaptionHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'group' | 'none' | 'presentation' | undefined>;
+	}
+
+	interface FooterHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<
+			| 'contentinfo'
+			| 'group'
+			| 'none'
+			| 'presentation'
+			| 'doc-footnote'
+			| undefined
+		>;
 	}
 
 	interface FormHTMLAttributes
@@ -1745,9 +1907,39 @@ export namespace JSXInternal {
 		novalidate?: Signalish;
 		noValidate?: Signalish;
 		rel?: Signalish;
+		role?: Signalish<'form' | 'none' | 'presentation' | 'search' | undefined>;
 		target?: Signalish;
 	}
 
+	interface HeadingHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<
+			'heading' | 'none' | 'presentation' | 'tab' | 'doc-subtitle' | undefined
+		>;
+	}
+
+	interface HeadHTMLAttributes
+		extends HTMLAttributes {
+		role?: never;
+	}
+
+	interface HeaderHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'banner' | 'group' | 'none' | 'presentation' | undefined>;
+	}
+
+	interface HrHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<
+			'separator' | 'none' | 'presentation' | 'doc-pagebreak' | undefined
+		>;
+	}
+
+	interface HtmlHTMLAttributes
+		extends HTMLAttributes {
+		role?: Signalish<'document' | undefined>;
+	}
+
 	interface IframeHTMLAttributes
 		extends HTMLAttributes {
 		allow?: Signalish;
@@ -1766,6 +1958,9 @@ export namespace JSXInternal {
 		name?: Signalish;
 		referrerpolicy?: Signalish;
 		referrerPolicy?: Signalish;
+		role?: Signalish<
+			'application' | 'document' | 'img' | 'none' | 'presentation' | undefined
+		>;
 		sandbox?: Signalish;
 		/** @deprecated */
 		scrolling?: Signalish;
@@ -1778,9 +1973,8 @@ export namespace JSXInternal {
 
 	type HTMLAttributeCrossOrigin = 'anonymous' | 'use-credentials';
 
-	interface ImgHTMLAttributes
+	interface PartialImgHTMLAttributes