Repository: preactjs/preact-devtools
Pull request #191 (2020-05-10 07:31:15)

Profiler refactor Part 2
This is a follow-up to #187 .

- Improve render performance by avoiding unnecessary allocations
- More Profiler e2e tests
- Basic support for static sub-trees
- Extract diagrams into separate flamegraph + ranked component


Patch:
diff --git a/src/view/components/Devtools.css b/src/view/components/Devtools.css
index 8f4d2c1..75795c9 100644
--- a/src/view/components/Devtools.css
+++ b/src/view/components/Devtools.css
@@ -187,7 +187,7 @@ input {
 	--color-props-vnode: #dedede;
 
 	/* Profiling */
-	--color-bystander-text: #e7e7e7;
+	--color-bystander-text: #d8d8d8;
 	--color-record-active: #ff5722;
 	/* --color-profiler-gradient-0: #00b397;
 	--color-profiler-gradient-1: #00b397;
diff --git a/src/view/components/elements/TreeBar.tsx b/src/view/components/elements/TreeBar.tsx
index 5316414..630afb7 100644
--- a/src/view/components/elements/TreeBar.tsx
+++ b/src/view/components/elements/TreeBar.tsx
@@ -108,6 +108,7 @@ export function TreeBar() {
 					<IconBtn
 						title="Filter Components"
 						active={filterVisible}
+						testId="filter-menu-button"
 						onClick={() => setFilterVisible(!filterVisible)}
 					>
 						<FilterList />
@@ -126,7 +127,7 @@ export function FilterPopup() {
 	const filterFragment = useObserver(() => store.filter.filterFragment.$);
 
 	return (
-		<div class={s.filter}>
+		<div class={s.filter} data-testid="filter-popup">
 			<form
 				onSubmit={e => {
 					e.preventDefault();
@@ -202,6 +203,7 @@ export function FilterPopup() {
 					<IconBtn
 						styling="secondary"
 						title="Add new filter"
+						testId="add-filter"
 						onClick={() => store.filter.add()}
 					>
 						<span class={s.filterCheck}>
@@ -209,7 +211,11 @@ export function FilterPopup() {
 						</span>
 						Add filter
 					</IconBtn>
-					<button type="submit" class={s.filterSubmitBtn}>
+					<button
+						type="submit"
+						class={s.filterSubmitBtn}
+						data-testid="filter-update"
+					>
 						Update
 					</button>
 				</div>
diff --git a/src/view/components/profiler/data/commits.ts b/src/view/components/profiler/data/commits.ts
index f54f366..db5029b 100644
--- a/src/view/components/profiler/data/commits.ts
+++ b/src/view/components/profiler/data/commits.ts
@@ -94,6 +94,15 @@ export function createProfiler(): ProfilerState {
 			: null;
 	});
 
+	// Flamegraph
+	const flamegraphType = valoo(FlamegraphType.FLAMEGRAPH);
+	flamegraphType.on(type => {
+		selectedNodeId.$ =
+			type === FlamegraphType.FLAMEGRAPH && activeCommit.$
+				? activeCommit.$.rootId
+				: -1;
+	});
+
 	// Recording
 	const isRecording = valoo(false);
 	isRecording.on(v => {
@@ -107,20 +116,14 @@ export function createProfiler(): ProfilerState {
 			// Reset selection when recording stopped
 			// and new profiling data was collected.
 			if (commits.$.length > 0) {
-				selectedNodeId.$ = commits.$[0].rootId;
+				selectedNodeId.$ =
+					flamegraphType.$ === FlamegraphType.FLAMEGRAPH
+						? commits.$[0].rootId
+						: commits.$[0].commitRootId;
 			}
 		}
 	});
 
-	// Flamegraph
-	const flamegraphType = valoo(FlamegraphType.FLAMEGRAPH);
-	flamegraphType.on(type => {
-		selectedNodeId.$ =
-			type === FlamegraphType.FLAMEGRAPH && activeCommit.$
-				? activeCommit.$.rootId
-				: -1;
-	});
-
 	// Render reasons
 	const activeReason = watch(() => {
 		if (activeCommit.$ !== null) {
diff --git a/src/view/components/profiler/data/gradient.ts b/src/view/components/profiler/data/gradient.ts
index cfa95bf..46cfcdc 100644
--- a/src/view/components/profiler/data/gradient.ts
+++ b/src/view/components/profiler/data/gradient.ts
@@ -10,5 +10,7 @@ export function getGradient(max: number, n: number) {
 		}
 	}
 
+	// i can be NaN at this point
+	if (isNaN(i)) return 0;
 	return Math.max(i, 0);
 }
diff --git a/src/view/components/profiler/flamegraph/FlameGraph.css b/src/view/components/profiler/flamegraph/FlameGraph.css
index 52fbd76..07b1ecf 100644
--- a/src/view/components/profiler/flamegraph/FlameGraph.css
+++ b/src/view/components/profiler/flamegraph/FlameGraph.css
@@ -55,17 +55,17 @@
 	opacity: 1;
 }
 
-.node[data-weight="-2"]:not([data-overflow]) {
+.node[data-weight="-1"]:not([data-commit-parent]) {
 	color: var(--color-bystander-text);
 }
-.node[data-weight="-2"]::after {
+.node[data-weight="-1"]:not([data-commit-parent])::after {
 	/* inline-pattern */
 	background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0IDQiPgo8ZGVmcz4KICA8cGF0dGVybgogICAgaWQ9ImRpYWdvbmFsLWhhdGNoIgogICAgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIKICAgIHdpZHRoPSI0IgogICAgaGVpZ2h0PSI0IgogID4KICAgIDxwYXRoCiAgICAgIGQ9Ik0tMSwxIGwyLC0yCk0wLDQgbDQsLTQKTTMsNSBsMiwtMiIKc3Ryb2tlPSIjZGVkZWRlIgpzdHJva2Utd2lkdGg9IjEiCiAgICAvPgogIDwvcGF0dGVybj4KICA8L2RlZnM+CiAgPHJlY3QKICAgIHg9IjAiCiAgICB5PSIwIgogICAgd2lkdGg9IjQiCiAgICBoZWlnaHQ9IjQiCiAgICBmaWxsPSJ1cmwoI2RpYWdvbmFsLWhhdGNoKSIKICA+PC9yZWN0Pgo8L3N2Zz4=");
 	background-repeat: repeat;
 	background-color: transparent;
 	background-size: 0.25rem 0.25rem;
 }
-:global(.dark) .node[data-weight="-2"]::after {
+:global(.dark) .node[data-weight="-1"]:not([data-commit-parent])::after {
 	opacity: 0.5;
 }
 
@@ -73,9 +73,6 @@
 	opacity: 0.5;
 }
 
-.node[data-weight="-1"]::after {
-	background: var(--color-profiler-old);
-}
 .node[data-weight="0"]::after {
 	background: var(--color-profiler-gradient-0);
 }
diff --git a/src/view/components/profiler/flamegraph/FlameGraph.tsx b/src/view/components/profiler/flamegraph/FlameGraph.tsx
index 341567e..8e78c16 100644
--- a/src/view/components/profiler/flamegraph/FlameGraph.tsx
+++ b/src/view/components/profiler/flamegraph/FlameGraph.tsx
@@ -1,49 +1,33 @@
 import { h } from "preact";
 import s from "./FlameGraph.css";
-import {
-	useStore,
-	useObserver,
-	WindowCtx,
-} from "../../../store/react-bindings";
-import { useRef, useCallback, useContext } from "preact/hooks";
-import { formatTime } from "../util";
-import { FlamegraphType, ProfilerNode } from "../data/commits";
-import { createFlameGraphStore } from "./FlamegraphStore";
-import { useInstance, useResize } from "../../utils";
-import { FlameNode } from "./FlameNode";
-import { ID } from "../../../store/types";
+import { useStore, useObserver } from "../../../store/react-bindings";
+import { useRef, useCallback, useState, useEffect } from "preact/hooks";
+import { FlamegraphType } from "../data/commits";
+import { useResize } from "../../utils";
+import { RankedLayout } from "./ranked/RankedLayout";
+import { EMPTY } from "./placeNodes";
+import { FlamegraphLayout } from "./modes/FlamegraphLayout";
 
 export function FlameGraph() {
 	const store = useStore();
+	const [canvasWidth, setCanvasWidth] = useState(600);
 
 	const displayType = useObserver(() => store.profiler.flamegraphType.$);
-	const selectedId = useObserver(() => store.profiler.selectedNodeId.$);
+	const selected = useObserver(() => store.profiler.selectedNode.$ || EMPTY);
 	const commit = useObserver(() => store.profiler.activeCommit.$);
-	const nodes = commit ? commit.nodes : new Map<ID, ProfilerNode>();
-
-	const ref2 = useInstance(() => createFlameGraphStore(store.profiler));
-	const positionData = useObserver(() => {
-		return !store.profiler.isRecording.$ ? ref2.nodes.$ : [];
-	});
-
-	const win = useContext(WindowCtx) || window;
-	if (process.env.DEBUG) {
-		(win as any).flamegraph = ref2;
-	}
+	const isRecording = useObserver(() => store.profiler.isRecording.$);
 
 	const ref = useRef<HTMLDivElement>();
-	useResize(
-		() => {
-			if (ref.current) {
-				const width = ref.current.clientWidth;
-				if (ref2.canvasWidth.$ !== width) {
-					ref2.canvasWidth.$ = width;
-				}
-			}
-		},
-		[positionData.length],
-		true,
-	);
+	useEffect(() => {
+		if (ref.current) {
+			setCanvasWidth(ref.current.clientWidth);
+		}
+	}, [isRecording || !commit]);
+	useResize(() => {
+		if (ref.current) {
+			setCanvasWidth(ref.current.clientWidth);
+		}
+	}, []);
 
 	const onSelect = useCallback(
 		(id: number) => {
@@ -53,26 +37,25 @@ export function FlameGraph() {
 		[store],
 	);
 
+	if (isRecording || !commit) return null;
+
 	return (
 		<div class={s.root} ref={ref} data-type={displayType.toLowerCase()}>
-			{positionData.map(pos => {
-				const node = nodes.get(pos.id)!;
-				return (
-					<FlameNode
-						key={pos.id}
-						parentId={node.parent}
-						node={pos}
-						commitRootId={commit ? commit.commitRootId : -1}
-						onClick={() => onSelect(pos.id)}
-						selected={selectedId === pos.id}
-					>
-						{node.name} ({formatTime(node.selfDuration)}
-						{displayType === FlamegraphType.FLAMEGRAPH &&
-							" of " + formatTime(node.treeEndTime - node.treeStartTime)}
-						)
-					</FlameNode>
-				);
-			})}
+			{displayType === FlamegraphType.RANKED ? (
+				<RankedLayout
+					canvasWidth={canvasWidth}
+					commit={commit!}
+					onSelect={onSelect}
+					selected={selected}
+				/>
+			) : (
+				<FlamegraphLayout
+					canvasWidth={canvasWidth}
+					commit={commit!}
+					onSelect={onSelect}
+					selected={selected}
+				/>
+			)}
 		</div>
 	);
 }
diff --git a/src/view/components/profiler/flamegraph/FlameNode.tsx b/src/view/components/profiler/flamegraph/FlameNode.tsx
index f6bd7a9..05f9d76 100644
--- a/src/view/components/profiler/flamegraph/FlameNode.tsx
+++ b/src/view/components/profiler/flamegraph/FlameNode.tsx
@@ -1,14 +1,15 @@
 import { h } from "preact";
-import { useRef } from "preact/hooks";
+import { useRef, useCallback } from "preact/hooks";
 import s from "./FlameGraph.css";
-import { NodeTransform } from "./transform/focusNode";
+import { NodeTransform } from "./transform/shared";
+import { ID } from "../../../store/types";
 
 export interface Props {
 	selected: boolean;
 	children: any;
 	parentId: number;
 	commitRootId: number;
-	onClick: () => void;
+	onClick: (id: ID) => void;
 	node: NodeTransform;
 }
 
@@ -20,6 +21,8 @@ export function FlameNode(props: Props) {
 	const transform = useRef("");
 	const widthCss = useRef("");
 
+	const onRawClick = useCallback(() => onClick(node.id), [node.id]);
+
 	const { visible, width, x } = node;
 
 	if (visible) {
@@ -31,18 +34,22 @@ export function FlameNode(props: Props) {
 	return (
 		<div
 			class={s.node}
-			onClick={onClick}
+			onClick={onRawClick}
 			data-id={node.id}
 			data-commit-root={props.commitRootId}
+			data-active-commit-root={node.id === props.commitRootId}
 			data-parent-id={props.parentId}
 			data-visible={visible}
 			data-weight={node.weight}
+			data-commit-parent={node.commitParent}
 			data-maximized={node.maximized}
 			data-selected={selected}
 			data-overflow={width < MIN_TEXT_WIDTH}
-			style={`height: ${ROW_HEIGHT}px; ${transform.current} ${widthCss.current};`}
+			style={`height: ${ROW_HEIGHT}px; ${transform.current} ${widthCss.current}`}
 		>
-			<span class={s.text}>{props.children}</span>
+			<span class={s.text} style={widthCss.current}>
+				{props.children}
+			</span>
 		</div>
 	);
 }
diff --git a/src/view/components/profiler/flamegraph/FlamegraphStore.ts b/src/view/components/profiler/flamegraph/FlamegraphStore.ts
index 2f4d18f..79ca4d4 100644
--- a/src/view/components/profiler/flamegraph/FlamegraphStore.ts
+++ b/src/view/components/profiler/flamegraph/FlamegraphStore.ts
@@ -1,20 +1,5 @@
-import { valoo } from "../../../valoo";
-import { ProfilerState, ProfilerNode } from "../data/commits";
+import { ProfilerNode } from "../data/commits";
 import { ID, Tree } from "../../../store/types";
-import { placeNodes } from "./placeNodes";
-
-export function createFlameGraphStore(profiler: ProfilerState) {
-	const canvasWidth = valoo(600);
-
-	const nodes = placeNodes(
-		profiler.activeCommit,
-		profiler.flamegraphType,
-		profiler.selectedNodeId,
-		canvasWidth,
-	);
-
-	return { nodes, canvasWidth };
-}
 
 export function getRoot(tree: Tree, id: ID) {
 	let item = tree.get(id);
diff --git a/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx b/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx
new file mode 100644
index 0000000..f33bf9e
--- /dev/null
+++ b/src/view/components/profiler/flamegraph/modes/FlamegraphLayout.tsx
@@ -0,0 +1,58 @@
+import { h, Fragment } from "preact";
+import { CommitData, ProfilerNode } from "../../data/commits";
+import { ID } from "../../../../store/types";
+import { FlameNode } from "../FlameNode";
+import { useMemo } from "preact/hooks";
+import { toTransform, placeFlamegraph } from "./flamegraph-utils";
+import { formatTime } from "../../util";
+
+export interface FlamegraphLayoutProps {
+	commit: CommitData;
+	selected: ProfilerNode;
+	canvasWidth: number;
+	onSelect: (id: ID) => void;
+}
+
+export function FlamegraphLayout({
+	commit,
+	selected,
+	canvasWidth,
+	onSelect,
+}: FlamegraphLayoutProps) {
+	const data = useMemo(() => toTransform(commit), [commit]);
+
+	const placed = useMemo(
+		() =>
+			placeFlamegraph(commit.nodes, data, commit.rootId, selected, canvasWidth),
+		[commit, data, selected, canvasWidth],
+	);
+
+	// Cache text content to avoid calling `Intl`-API repeatedly
+	const texts = useMemo(() => {
+		return Array.from(data.values()).map(pos => {
+			const node = commit.nodes.get(pos.id)!;
+			const self = formatTime(node.selfDuration);
+			const total = formatTime(node.treeEndTime - node.treeStartTime);
+			return `${node.name} (${self} of ${total})`;
+		});
+	}, [commit, data]);
+
+	return (
+		<Fragment>
+			{placed.map((pos, i) => {
+				return (
+					<FlameNode
+						key={pos.id}
+						commitRootId={commit.commitRootId}
+						node={pos}
+						selected={pos.id === selected.id}
+						parentId={commit.nodes.get(pos.id)!.parent}
+						onClick={onSelect}
+					>
+						{texts[i]}
+					</FlameNode>
+				);
+			})}
+		</Fragment>
+	);
+}
diff --git a/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts b/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts
new file mode 100644
index 0000000..6d40467
--- /dev/null
+++ b/src/view/components/profiler/flamegraph/modes/flamegraph-utils.ts
@@ -0,0 +1,105 @@
+import { ProfilerNode, CommitData } from "../../data/commits";
+import { NodeTransform } from "../transform/shared";
+import { getGradient } from "../../data/gradient";
+import { flattenNodeTree } from "../placeNodes";
+import { ID } from "../../../../store/types";
+
+/**
+ * Convert tree to position nodes.
+ */
+export function toTransform(commit: CommitData): Map<ID, NodeTransform> {
+	const root = commit.nodes.get(commit.commitRootId)!;
+
+	const commitParents = new Set<ID>();
+	let item: ProfilerNode | undefined = commit.nodes.get(commit.commitRootId)!;
+	while ((item = commit.nodes.get(item.parent))) {
+		commitParents.add(item.id);
+	}
+
+	const out = new Map<ID, NodeTransform>();
+	flattenNodeTree(commit.nodes, commit.rootId).forEach(node => {
+		out.set(node.id, {
+			id: node.id,
+			width: node.selfDuration,
+			x: 0,
+			row: node.depth,
+			maximized: false,
+			weight:
+				node.startTime < root.startTime
+					? -1
+					: getGradient(commit.maxSelfDuration, node.selfDuration),
+			visible: true,
+			commitParent: commitParents.has(node.id),
+		});
+	});
+
+	return out;
+}
+
+/**
+ * The default layout that's typical for a flamegraph chart.
+ */
+export function placeFlamegraph(
+	tree: Map<ID, ProfilerNode>,
+	transforms: Map<ID, NodeTransform>,
+	rootId: ID,
+	selected: ProfilerNode,
+	canvasWidth: number,
+): NodeTransform[] {
+	// Collect parents upfront, so that we already know which
+	// nodes should be maximized in the next traversal
+	const parents = new Set<ID>([selected.id]);
+	let item: ProfilerNode | undefined = selected;
+	while ((item = tree.get(item.parent))) {
+		parents.add(item.id);
+	}
+
+	const root = tree.get(rootId)!;
+	const scale =
+		(canvasWidth || 1) /
+		(selected.treeEndTime - selected.treeStartTime || 0.01);
+	const offset = selected.treeStartTime;
+
+	// Use this value to keep track of when we enter the current
+	// sub-tree that we're zoomed in into. Once we processed all
+	// nodes of that we need to reset this value.
+	let visitedSelectedRow = -1;
+
+	const stack = [root];
+	while ((item = stack.pop())) {
+		// Track when we enter and leave the currently focused sub-tree
+		if (item.id === selected.id) {
+			visitedSelectedRow = item.depth;
+		} else if (item.depth <= visitedSelectedRow) {
+			visitedSelectedRow = -1;
+		}
+
+		const pos = transforms.get(item.id)!;
+		// If we are a parent node of the selected node or the selected
+		// node itself, we need to maximize it by stretching it over
+		// the whole canvas.
+		if (parents.has(item.id)) {
+			pos.maximized = true;
+			pos.width = canvasWidth;
+			pos.x = 0;
+			pos.visible = true;
+		}
+		// At this point the node isn't maximized. If we're not inside
+		// the currently zoomed in sub-tree, we must be a node that
+		// should be hidden.
+		else if (visitedSelectedRow === -1) {
+			pos.visible = false;
+		}
+		// Looks like the node is visible and we have to position it
+		// inside the visible canvas.
+		else {
+			pos.maximized = false;
+			pos.visible = true;
+			pos.x = (item.treeStartTime - offset) * scale;
+			pos.width = (item.treeEndTime - item.treeStartTime) * scale;
+		}
+		stack.push(...item.children.map(id => tree.get(id)!));
+	}
+
+	return Array.from(transforms.values());
+}
diff --git a/src/view/components/profiler/flamegraph/modes/flamegraph.ts b/src/view/components/profiler/flamegraph/modes/flamegraph.ts
deleted file mode 100644
index 96f7166..0000000
--- a/src/view/components/profiler/flamegraph/modes/flamegraph.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { ProfilerNode } from "../../data/commits";
-import { NodeTransform } from "../transform/focusNode";
-import { getGradient } from "../../data/gradient";
-
-/**
- * The default layout that's typical for a flamegraph chart.
- */
-export function layoutTimeline(
-	nodes: ProfilerNode[],
-	root: ProfilerNode,
-	selected: ProfilerNode,
-	maxSelfDuration: number,
-): NodeTransform[] {
-	const offset = selected.treeStartTime;
-
-	return nodes
-		.sort((a, b) => a.treeStartTime - b.treeStartTime)
-		.map(node => {
-			let weight = -1;
-			if (
-				node.treeEndTime <= root.treeStartTime ||
-				node.treeStartTime > root.treeEndTime
-			) {
-				weight = -2;
-			} else if (root.depth > node.depth) {
-				weight = -5;
-			} else {
-				weight = getGradient(maxSelfDuration, node.selfDuration);
-			}
-
-			return {
-				id: node.id,
-				// Normalize x coordinate
-				x: node.treeStartTime - offset,
-				row: node.depth,
-				width: node.treeEndTime - node.treeStartTime,
-				weight,
-				maximized: false,
-				visible: true,
-			};
-		});
-}
diff --git a/src/view/components/profiler/flamegraph/modes/ranked.ts b/src/view/components/profiler/flamegraph/modes/ranked.ts
deleted file mode 100644
index 543e8f5..0000000
--- a/src/view/components/profiler/flamegraph/modes/ranked.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NodeTransform } from "../transform/focusNode";
-import { ProfilerNode } from "../../data/commits";
-import { getGradient } from "../../data/gradient";
-
-/**
- * Layout nodes in a flat list from top to bottom
- * in descending order by the time it took a nod
- * to render.
- */
-export function layoutRanked(
-	nodes: ProfilerNode[],
-	root: ProfilerNode,
-	maxSelfDuration: number,
-): NodeTransform[] {
-	return nodes
-		.filter(
-			node => node.startTime >= root.startTime && node.endTime <= root.endTime,
-		)
-		.sort((a, b) => b.selfDuration - a.selfDuration)
-		.map((node, i) => {
-			return {
-				id: node.id,
-				// Ensure nodes are always visible
-				width: Math.max(node.selfDuration, 0.01),
-				x: 0,
-				row: i,
-				maximized: false,
-				weight: getGradient(maxSelfDuration, node.selfDuration),
-				visible: true,
-			};
-		});
-}
diff --git a/src/view/components/profiler/flamegraph/placeNodes.ts b/src/view/components/profiler/flamegraph/placeNodes.ts
index c73f0c9..80732cc 100644
--- a/src/view/components/profiler/flamegraph/placeNodes.ts
+++ b/src/view/components/profiler/flamegraph/placeNodes.ts
@@ -1,10 +1,4 @@
-import { ID } from "../../../store/types";
-import { CommitData, FlamegraphType, ProfilerNode } from "../data/commits";
-import { watch, Observable } from "../../../valoo";
-import { layoutTimeline } from "./modes/flamegraph";
-import { layoutRanked } from "./modes/ranked";
-import { focusNode } from "./transform/focusNode";
-import { padNodes } from "./transform/pad";
+import { CommitData, ProfilerNode } from "../data/commits";
 
 /**
  * Flatten profiler node tree into a flat list
@@ -37,7 +31,16 @@ export function flattenNodeTree<K, T extends { id: K; children: K[] }>(
 	return out;
 }
 
-const EMPTY: ProfilerNode = {
+export const EMPTY_COMMIT: CommitData = {
+	commitRootId: -1,
+	duration: -1,
+	maxDepth: 1,
+	maxSelfDuration: 1,
+	nodes: new Map(),
+	rootId: -1,
+};
+
+export const EMPTY: ProfilerNode = {
 	children: [],
 	depth: 0,
 	duration: 0,
@@ -59,66 +62,3 @@ const EMPTY: ProfilerNode = {
  * that would be smaller than <1px because of zooming
  */
 export const MIN_WIDTH = 6;
-
-/**
- * This function is responsible for creating the diagram layout
- * for the profiler. It's divided into steps so that we can reuse
- * as much as possible from the previous calculations on each user
- * action.
- * @param commit Commit data
- * @param viewMode The diagram mode "flamegraph" | "ranked"
- * @param selectedId The currently selected id
- * @param canvasWidth The width of the canvas
- */
-export function placeNodes(
-	commit: Observable<CommitData | null>,
-	viewMode: Observable<FlamegraphType>,
-	selectedId: Observable<ID>,
-	canvasWidth: Observable<number>,
-) {
-	const sorted = watch(() => {
-		if (!commit.$) return [];
-
-		// 1. Preparation
-		const nodes = flattenNodeTree(commit.$.nodes, commit.$.rootId);
-		const root = commit.$.nodes.get(commit.$.commitRootId) || EMPTY;
-		const maxDuration = commit.$.maxSelfDuration;
-
-		const selected = commit.$.nodes.get(selectedId.$) || root;
-
-		// 2. Sorting (view mode) + coloring
-		const prepared =
-			viewMode.$ === FlamegraphType.FLAMEGRAPH
-				? layoutTimeline(nodes, root, selected, maxDuration)
-				: layoutRanked(nodes, root, maxDuration);
-
-		return padNodes(prepared, 0.1);
-	});
-
-	// 3. Focus
-	const focused = watch(() => {
-		return focusNode(sorted.$, selectedId.$);
-	});
-
-	// 4. Resize to available canvas space
-	const resized = watch(() => {
-		if (!focused.$.length) return [];
-
-		const scale = (canvasWidth.$ || 1) / focused.$[0].width;
-
-		const out = focused.$.map(node => {
-			const x = node.x * scale;
-			const width = node.width * scale;
-			const visible =
-				node.maximized ||
-				(x >= 0 && x <= canvasWidth.$) ||
-				(x + width >= 0 && x + width <= canvasWidth.$);
-			return { ...node, x, width, visible };
-		});
-
-		// 5. Pad min-width
-		return padNodes(out, MIN_WIDTH);
-	});
-
-	return resized;
-}
diff --git a/src/view/components/profiler/flamegraph/ranked/RankedLayout.tsx b/src/view/components/profiler/flamegraph/ranked/RankedLayout.tsx
new file mode 100644
index 0000000..a96394d
--- /dev/null
+++ b/src/view/components/profiler/flamegraph/ranked/RankedLayout.tsx
@@ -0,0 +1,62 @@
+import { h, Fragment } from "preact";
+import { useMemo } from "preact/hooks";
+import { placeRanked, toTransform } from "./ranked-utils";
+import { ProfilerNode, CommitData } from "../../data/commits";
+import { ID } from "../../../../store/types";
+import { FlameNode } from "../FlameNode";
+import { formatTime } from "../../util";
+
+export interface RankedLayoutProps {
+	commit: CommitData;
+	selected: ProfilerNode;
+	canvasWidth: number;
+	onSelect: (id: ID) => void;
+}
+
+/**
+ * Layout nodes in a flat list from top to bottom
+ * in descending order by the time it took a node
+ * to render.
+ */
+export function RankedLayout({
+	canvasWidth,
+	commit,
+	selected,
+	onSelect,
+}: RankedLayoutProps) {
+	// Convert node tree to position data
+	const data = useMemo(() => toTransform(commit), [commit]);
+
+	// Update node positions and mutate `data` to avoid allocations
+	const placed = useMemo(
+		() => placeRanked(commit.nodes, data, selected, canvasWidth),
+		[canvasWidth, selected, commit, data],
+	);
+
+	// Cache text content to avoid calling `Intl`-API repeatedly
+	const texts = useMemo(() => {
+		return data.map(pos => {
+			const node = commit.nodes.get(pos.id)!;
+			return `${node.name} (${formatTime(node.selfDuration)})`;
+		});
+	}, [commit, data]);
+
+	return (
+		<Fragment>
+			{placed.map((pos, i) => {
+				return (
+					<FlameNode
+						key={pos.id}
+						commitRootId={commit.commitRootId}
+						node={pos}
+						selected={pos.id === selected.id}
+						parentId={selected.parent}
+						onClick={onSelect}
+					>
+						{texts[i]}
+					</FlameNode>
+				);
+			})}
+		</Fragment>
+	);
+}
diff --git a/src/view/components/profiler/flamegraph/ranked/ranked-utils.ts b/src/view/components/profiler/flamegraph/ranked/ranked-utils.ts
new file mode 100644
index 0000000..e0ecae6
--- /dev/null
+++ b/src/view/components/profiler/flamegraph/ranked/ranked-utils.ts
@@ -0,0 +1,66 @@
+import { NodeTransform } from "../transform/shared";
+import { ProfilerNode, CommitData } from "../../data/commits";
+import { getGradient } from "../../data/gradient";
+import { ID } from "../../../../store/types";
+import { flattenNodeTree, EMPTY } from "../placeNodes";
+
+const MIN_WIDTH = 4;
+
+/**
+ * Convert commit data into an array of position data to operate on.
+ */
+export function toTransform(commit: CommitData): NodeTransform[] {
+	const nodes = flattenNodeTree(commit.nodes, commit.rootId);
+	const root = commit.nodes.get(commit.commitRootId) || EMPTY;
+
+	return nodes
+		.filter(
+			node => node.startTime >= root.startTime && node.endTime <= root.endTime,
+		)
+		.sort((a, b) => b.selfDuration - a.selfDuration)
+		.map((node, i) => {
+			return {
+				id: node.id,
+				width: node.selfDuration,
+				x: 0,
+				row: i,
+				maximized: false,
+				weight: getGradient(commit.maxSelfDuration, node.selfDuration),
+				visible: true,
+				commitParent: false,
+			};
+		});
+}
+
+/**
+ * Place a pre-sorted array of nodes in a linear top to bottom list.
+ * MUTATES position nodes to avoid many allocations for each node.
+ */
+export function placeRanked(
+	tree: Map<ID, ProfilerNode>,
+	sorted: NodeTransform[],
+	selected: ProfilerNode,
+	canvasWidth: number,
+) {
+	const scale = (canvasWidth || 1) / Math.max(selected.selfDuration, 0.01);
+	let maximized = true;
+
+	sorted.forEach(pos => {
+		const node = tree.get(pos.id);
+		if (!node) return;
+
+		const selfDuration = node.selfDuration;
+
+		// Ensure nodes are always visible
+		pos.width = maximized
+			? canvasWidth
+			: Math.max(Math.max(selfDuration, 0.01) * scale, MIN_WIDTH);
+		pos.maximized = maximized;
+
+		if (pos.id === selected.id) {
+			maximized = false;
+		}
+	});
+
+	return sorted.slice();
+}
diff --git a/src/view/components/profiler/flamegraph/transform/focusNode.ts b/src/view/components/profiler/flamegraph/transform/focusNode.ts
deleted file mode 100644
index 0d840af..0000000
--- a/src/view/components/profiler/flamegraph/transform/focusNode.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { ID } from "../../../../store/types";
-import { deepClone } from "./util";
-
-export interface NodeTransform {
-	id: ID;
-	x: number;
-	row: number;
-	width: number;
-	weight: number;
-	maximized: boolean;
-	visible: boolean;
-}
-
-/**
- * Focus a particular node by maximizing it. All parents
- * will be maximized too.
- */
-export function focusNode(nodes: NodeTransform[], id: ID) {
-	nodes = deepClone(nodes);
-
-	// Populate caches
-	const byId = new Map<ID, NodeTransform>();
-	nodes.forEach(node => byId.set(node.id, node));
-
-	// Bail out if the focused node is not available
-	const target = byId.get(id);
-	if (!target) {
-		return nodes;
-	}
-
-	// Sort nodes by `row` and `x` coordinate
-	nodes.sort((a, b) => {
-		const res = a.row - b.row;
-		return res === 0 ? a.x - b.x : res;
-	});
-
-	// Separate nodes by row
-	const rows: NodeTransform[][] = [];
-
-	// list of parents of the focused node
-	const parents: NodeTransform[] = [];
-
-	nodes.forEach(node => {
-		if (!rows[node.row]) {
-			rows[node.row] = [];
-		}
-
-		if (
-			target.row >= node.row &&
-			target.x >= node.x &&
-			target.x + target.width <= node.x + node.width
-		) {
-			node.maximized = true;
-			parents.push(node);
-		} else {
-			node.maximized = false;
-		}
-
-		rows[node.row].push(node);
-	});
-
-	// Assumes that there is a single root node for
-	// each flamegraph
-	const maxWidth = nodes[0].width;
-
-	// Scale factor of the row that's focused
-	let scale = 1;
-
-	// x-axis offset for the current row
-	let offset = 0;
-
-	// Focus each row to the parent or target node
-	rows.forEach((row, i) => {
-		// Get scale factor for the current row.
-		// We'll keep using the one of the row with
-		// the focused node, for all succeeding rows.
-		const maximized = i < parents.length ? parents[i] : null;
-		if (maximized !== null) {
-			scale = maxWidth / maximized.width;
-			offset = maximized.x;
-		}
-
-		// Apply offset + scale factor to each node of the current row
-		row.forEach(node => {
-			node.x = (node.x - offset) * scale;
-			node.width = node.width * scale;
-		});
-	});
-
-	return nodes;
-}
diff --git a/src/view/components/profiler/flamegraph/transform/pad.ts b/src/view/components/profiler/flamegraph/transform/pad.ts
deleted file mode 100644
index 030e752..0000000
--- a/src/view/components/profiler/flamegraph/transform/pad.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { NodeTransform } from "./focusNode";
-
-/**
- * Ensures that nodes with a width of 0 are visible.
- * This is done by applying a minimum-width and
- * padding parents by that amount.
- */
-export function padNodes(
-	nodes: NodeTransform[],
-	factor: number,
-): NodeTransform[] {
-	nodes = nodes.slice();
-
-	// Ensure that every node is visible
-	const needsPad: NodeTransform[] = [];
-	nodes.forEach(node => {
-		if (node.width <= 0) {
-			needsPad.push(node);
-		}
-	});
-
-	needsPad.forEach(pad => {
-		pad.width += factor;
-
-		nodes.forEach(node => {
-			// Enlarge width of parents
-			if (
-				node.x <= pad.x &&
-				(node.x + node.width >= pad.x + pad.width ||
-					(node.x + node.width > pad.x &&
-						node.x + node.width < pad.x + pad.width))
-			) {
-				if (node.id !== pad.id) {
-					node.width += factor;
-				}
-			} else if (node.id !== pad.id && node.x >= pad.x) {
-				// Move nodes to the right by factor
-				node.x += factor;
-			}
-		});
-	});
-
-	return nodes;
-}
diff --git a/src/view/components/profiler/flamegraph/transform/patchTree.ts b/src/view/components/profiler/flamegraph/transform/patchTree.ts
index 650d239..8815071 100644
--- a/src/view/components/profiler/flamegraph/transform/patchTree.ts
+++ b/src/view/components/profiler/flamegraph/transform/patchTree.ts
@@ -1,6 +1,93 @@
 import { ID, Tree } from "../../../../store/types";
-import { mapChildren, adjustNodesToRight, deepClone } from "./util";
+import { adjustNodesToRight, deepClone } from "./util";
 
+/**
+ * Merge a static (memoized) sub-tree into the current tree. We'll try
+ * to fit it into the remaining space of the parent. It's not much,
+ * but it's honest work!
+ */
+function placeStaticTree(tree: Tree, rootId: ID) {
+	const root = tree.get(rootId)!;
+
+	let offset = 0;
+	let scale = 1;
+
+	// Measure available space to fit the sub-tree into
+	let spaceStart = 0;
+	let spaceEnd = 0;
+
+	const parent = tree.get(root.parent)!;
+	const childIdx = parent.children.indexOf(root.id);
+
+	if (childIdx + 1 < parent.children.length) {
+		const nextChild = tree.get(parent.children[childIdx + 1])!;
+		spaceEnd = nextChild.treeStartTime;
+	} else {
+		spaceEnd = parent.treeEndTime;
+	}
+
+	if (childIdx > 0) {
+		// If the previous child is a static root, we'll continue to
+		// traverse until we find the first sibling that is not static.
+		// We do this to get the total free space and to distribute sibling
+		// static trees accordingly.
+		let staticWidth = root.treeEndTime - root.treeStartTime;
+		let idx = childIdx;
+		let prevChild;
+		while ((prevChild = tree.get(parent.children[--idx])!)) {
+			if (prevChild.startTime >= parent.startTime) {
+				break;
+			}
+			staticWidth += prevChild.treeEndTime - prevChild.treeStartTime;
+		}
+
+		spaceStart = prevChild ? prevChild.treeEndTime : parent.treeStartTime;
+		const available = spaceEnd - spaceStart;
+
+		scale = staticWidth < available ? 1 : available / staticWidth;
+		spaceStart =
+			parent.treeStartTime +
+			(staticWidth - (root.treeEndTime - root.treeStartTime));
+		offset = spaceStart - root.treeStartTime * scale;
+	} else {
+		// If our node was the first child we can just take the
+		// current tree start time of the parent
+		spaceStart = parent.treeStartTime;
+
+		if (spaceEnd - spaceStart < root.treeEndTime - root.treeStartTime) {
+			scale =
+				(spaceEnd - spaceStart) /
+				// This can converge to 0, leading to wonky results
+				(root.treeEndTime - root.treeStartTime || 0.01);
+		}
+
+		offset = spaceStart - root.treeStartTime * scale;
+	}
+
+	const stack = [rootId];
+	let item;
+	while ((item = stack.pop())) {
+		const node = tree.get(item);
+		if (!node) continue;
+
+		tree.set(node.id, {
+			...deepClone(node),
+			// We need to scale down our tree first before moving it to
+			// the new position
+			treeStartTime: node.treeStartTime * scale + offset,
+			treeEndTime: node.treeEndTime * scale + offset,
+		});
+
+		stack.push(...node.children);
+	}
+}
+
+/**
+ * Merge the newly rendered tree into the existing one. If the new
+ * tree took longer to render than the previous version, we need to
+ * expand all parents and push all right-hand nodes further to the
+ * right to make room for our new tree.
+ */
 export function patchTree(old: Tree, next: Tree, rootId: ID): Tree {
 	const out: Tree = new Map(old);
 	const oldRoot = old.get(rootId);
@@ -38,17 +125,39 @@ export function patchTree(old: Tree, next: Tree, rootId: ID): Tree {
 	});
 
 	// Move children of newly committed sub-tree
-	mapChildren(next, root.id, child => {
-		out.set(child.id, {
-			...deepClone(child),
-			// TODO: Check for stale children
-			treeStartTime: child.startTime + deltaStart,
-			treeEndTime: child.endTime + deltaStart,
+	const stack = [root.id];
+
+	// We need to process all "active" commit nodes before processing
+	// static sub-trees to know how much space we have to place the
+	// sub-tree into. These sub-trees are ususally the result of
+	// memoization via `useMemo` or `memo()`.
+	const staticTrees: ID[] = [];
+
+	let item;
+	while ((item = stack.pop()) !== undefined) {
+		const node = next.get(item);
+		if (!node) continue;
+
+		// Check if we have a static sub-tree from a previous render.
+		// We'll process those later.
+		if (node.startTime < root.startTime) {
+			staticTrees.push(node.id);
+			continue;
+		}
+
+		out.set(node.id, {
+			...deepClone(node),
+			treeStartTime: node.startTime + deltaStart,
+			treeEndTime: node.endTime + deltaStart,
 		});
-	});
+		stack.push(...node.children);
+	}
 
 	// Enlarge parents and move children
 	adjustNodesToRight(out, root.id, -deltaEnd);
 
+	// Process any found static sub-trees
+	staticTrees.forEach(rootId => placeStaticTree(out, rootId));
+
 	return out;
 }
diff --git a/src/view/components/profiler/flamegraph/transform/shared.ts b/src/view/components/profiler/flamegraph/transform/shared.ts
new file mode 100644
index 0000000..2722f89
--- /dev/null
+++ b/src/view/components/profiler/flamegraph/transform/shared.ts
@@ -0,0 +1,12 @@
+import { ID } from "../../../../store/types";
+
+export interface NodeTransform {
+	id: ID;
+	x: number;
+	row: number;
+	width: number;
+	weight: number;
+	maximized: boolean;
+	visible: boolean;
+	commitParent: boolean;
+}


Test patch:
diff --git a/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts b/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts
deleted file mode 100644
index fad248e..0000000
--- a/src/view/components/profiler/flamegraph/FlamegraphStore.test.ts
+++ /dev/null
@@ -1,289 +0,0 @@
-import { expect } from "chai";
-import { createProfiler, FlamegraphType } from "../data/commits";
-import { createFlameGraphStore } from "./FlamegraphStore";
-import { flames } from "./testHelpers";
-
-describe("FlameGraphStore", () => {
-	let profiler: ReturnType<typeof createProfiler>;
-	let flame: ReturnType<typeof createFlameGraphStore>;
-
-	beforeEach(() => {
-		profiler = createProfiler();
-		flame = createFlameGraphStore(profiler);
-		profiler.activeCommitIdx.$ = 0;
-	});
-
-	it("should be empty when no commits", () => {
-		expect(flame.nodes.$).to.deep.equal([]);
-	});
-
-	describe("ranked mode", () => {
-		beforeEach(() => {
-			profiler.flamegraphType.$ = FlamegraphType.RANKED;
-			profiler.commits.on(commits => {
-				if (commits.length > 0) {
-					const list = Array.from(commits[0].nodes.values());
-					let max = list[0]!;
-					for (let i = 0; i < list.length; i++) {
-						const item = list[i];
-						if (max.selfDuration < item.selfDuration) {
-							max = item;
-						}
-					}
-					profiler.selectedNodeId.$ = max.id;
-				}
-			});
-		});
-
-		it("should always set x = 0", () => {
-			const tree = flames`
-				App ***************
-				  Foo **   Bar ***
-			`;
-			profiler.commits.$ = [tree.commit];
-			expect(flame.nodes.$.map(x => x.x)).to.deep.equal([0, 0, 0]);
-		});
-
-		it("should rank nodes by selfDuration", () => {
-			const tree = flames`
-				App *****************
-				  Foo **   Bar ***
-			`;
-			profiler.commits.$ = [tree.commit];
-			expect(flame.nodes.$.map(x => tree.idMap.get(x.id)!.name)).to.deep.equal([
-				"App",
-				"Bar",
-				"Foo",
-			]);
-		});
-
-		it("should calculate width", () => {
-			const tree = flames`
-					App *****************
-						Foo **   Bar ***
-				`;
-			profiler.commits.$ = [tree.commit];
-			expect(flame.nodes.$.map(x => x.width)).to.deep.equal([600, 525, 450]);
-		});
-
-		it("should rank nodes by selfDuration #2", () => {
-			const tree = flames`
-				App *****************
-				  Foo **   Bar ******
-			`;
-			profiler.commits.$ = [tree.commit];
-			expect(flame.nodes.$.map(x => tree.idMap.get(x.id)!.name)).to.deep.equal([
-				"Bar",
-				"Foo",
-				"App",
-			]);
-		});
-
-		it("should calculate widths", () => {
-			const tree = flames`
-				App ********************
-				  Foo ****   Bar ******
-			`;
-			profiler.commits.$ = [tree.commit];
-
-			expect(flame.nodes.$.map(x => x.width)).to.deep.equal([600, 480, 360]);
-		});
-
-		it("should support maximizing nodes", () => {
-			const tree = flames`
-				App *****************
-				  Foo **   Bar ******
-			`;
-			profiler.commits.$ = [tree.commit];
-			profiler.selectedNodeId.$ = 2;
-
-			expect(flame.nodes.$.map(x => tree.idMap.get(x.id)!.name)).to.deep.equal([
-				"Bar",
-				"Foo",
-				"App",
-			]);
-
-			expect(flame.nodes.$.map(x => x.width)).to.deep.equal([
-				600,
-				600,
-				500.00000000000006,
-			]);
-		});
-	});
-
-	describe("timeline mode", () => {
-		beforeEach(() => {
-			profiler.flamegraphType.$ = FlamegraphType.FLAMEGRAPH;
-			profiler.selectedNodeId.$ = 1;
-		});
-
-		it("should position nodes", () => {
-			const tree = flames`
-				App ********
-				  Foo ****
-				    Bar *
-			`;
-			profiler.commits.$ = [tree.commit];
-
-			const actual = flame.nodes.$.map(x => {
-				return {
-					...x,
-					name: tree.idMap.get(x.id)!.name,
-				};
-			});
-
-			expect(actual).to.deep.equal([
-				{
-					id: 1,
-					name: "App",
-					x: 0,
-					row: 0,
-					width: 600,
-					visible: true,
-					maximized: true,
-					weight: 7,
-				},
-				{
-					id: 2,
-					name: "Foo",
-					x: 100,
-					row: 1,
-					width: 400,
-					visible: true,
-					maximized: false,
-					weight: 5,
-				},
-				{
-					id: 3,
-					name: "Bar",
-					x: 200,
-					row: 2,
-					width: 250,
-					visible: true,
-					maximized: false,
-					weight: 9,
-				},
-			]);
-		});
-
-		it("should position with maximized nodes", () => {
-			const tree = flames`
-				App ********
-				  Foo ****
-				    Bar *
-			`;
-			profiler.commits.$ = [tree.commit];
-			profiler.selectedNodeId.$ = 2;
-
-			const actual = flame.nodes.$.map(x => {
-				return {
-					...x,
-					name: tree.idMap.get(x.id)!.name,
-				};
-			});
-
-			expect(actual).to.deep.equal([
-				{
-					id: 1,
-					name: "App",
-					x: 0,
-					row: 0,
-					width: 600,
-					visible: true,
-					maximized: true,
-					weight: 7,
-				},
-				{
-					id: 2,
-					name: "Foo",
-					x: 0,
-					row: 1,
-					width: 600,
-					visible: true,
-					maximized: true,
-					weight: 5,
-				},
-				{
-					id: 3,
-					name: "Bar",
-					x: 150,
-					row: 2,
-					width: 375,
-					visible: true,
-					maximized: false,
-					weight: 9,
-				},
-			]);
-		});
-
-		it("should position outside node when maximized", () => {
-			const tree = flames`
-				App ***************************
-				  Foo ****  Bob **   Boof ***
-				    Bar *
-			`;
-			profiler.commits.$ = [tree.commit];
-			profiler.selectedNodeId.$ = tree.byName("Bob")!.id;
-
-			const actual = flame.nodes.$.map(x => {
-				return {
-					...x,
-					name: tree.idMap.get(x.id)!.name,
-				};
-			});
-
-			expect(actual).to.deep.equal([
-				{
-					id: 1,
-					name: "App",
-					x: 0,
-					row: 0,
-					width: 600,
-					maximized: true,
-					weight: 9,
-					visible: true,
-				},
-				{
-					id: 2,
-					name: "Foo",
-					x: -1000.0000000000002,
-					row: 1,
-					width: 800.0000000000001,
-					visible: false,
-					maximized: false,
-					weight: 3,
-				},
-				{
-					id: 4,
-					name: "Bob",
-					x: 0,
-					row: 1,
-					width: 600,
-					weight: 6,
-					visible: true,
-					maximized: true,
-				},
-				{
-					id: 5,
-					name: "Boof",
-					x: 900,
-					row: 1,
-					width: 800.0000000000001,
-					weight: 8,
-					visible: false,
-					maximized: false,
-				},
-				{
-					id: 3,
-					name: "Bar",
-					x: -800.0000000000001,
-					row: 2,
-					width: 500.0000000000001,
-					visible: false,
-					maximized: false,
-					weight: 5,
-				},
-			]);
-		});
-	});
-});
diff --git a/src/view/components/profiler/flamegraph/modes/flamegraph-utils.test.ts b/src/view/components/profiler/flamegraph/modes/flamegraph-utils.test.ts
new file mode 100644
index 0000000..bc210f7
--- /dev/null
+++ b/src/view/components/profiler/flamegraph/modes/flamegraph-utils.test.ts
@@ -0,0 +1,39 @@
+import { expect } from "chai";
+import { flames } from "../testHelpers";
+import { toTransform } from "./flamegraph-utils";
+
+const required = {
+	visible: true,
+	maximized: false,
+	commitParent: false,
+};
+
+describe("toTransform", () => {
+	it("should display simple flamegraph", () => {
+		const tree = flames`
+        App **********
+          Foo ****
+            Bar *
+      `;
+		expect(Array.from(toTransform(tree.commit).values())).to.deep.equal([
+			{ ...required, id: 1, width: 60, x: 0, row: 0, weight: 9 },
+			{ ...required, id: 2, width: 30, x: 0, row: 1, weight: 5 },
+			{ ...required, id: 3, width: 50, x: 0, row: 2, weight: 8 },
+		]);
+	});
+
+	it("should display simple complex flamegraph", () => {
+		const tree = flames`
+        App *********************
+          Foo ****   Baz ******
+            Bar *      Bob **
+      `;
+		expect(Array.from(toTransform(tree.commit).values())).to.deep.equal([
+			{ ...required, id: 1, width: 70, x: 0, row: 0, weight: 9 },
+			{ ...required, id: 2, width: 30, x: 0, row: 1, weight: 4 },
+			{ ...required, id: 3, width: 50, x: 0, row: 2, weight: 6 },
+			{ ...required, id: 4, width: 40, x: 0, row: 1, weight: 5 },
+			{ ...required, id: 5, width: 60, x: 0, row: 2, weight: 8 },
+		]);
+	});
+});
diff --git a/src/view/components/profiler/flamegraph/modes/flamegraph.test.ts b/src/view/components/profiler/flamegraph/modes/flamegraph.test.ts
deleted file mode 100644
index a41027c..0000000
--- a/src/view/components/profiler/flamegraph/modes/flamegraph.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { expect } from "chai";
-import { flames } from "../testHelpers";
-import { layoutTimeline } from "./flamegraph";
-
-const required = {
-	visible: true,
-	maximized: false,
-};
-
-describe("layoutTimeline", () => {
-	it("should display simple flamegraph", () => {
-		const tree = flames`
-        App **********
-          Foo ****
-            Bar *
-      `;
-		expect(
-			layoutTimeline(tree.nodes, tree.root, tree.root, tree.root.selfDuration),
-		).to.deep.equal([
-			{ ...required, id: 1, width: 140, x: 0, row: 0, weight: 9 },
-			{ ...required, id: 2, width: 80, x: 20, row: 1, weight: 5 },
-			{ ...required, id: 3, width: 50, x: 40, row: 2, weight: 8 },
-		]);
-	});
-
-	it("should display simple complex flamegraph", () => {
-		const tree = flames`
-        App *********************
-          Foo ****   Baz ******
-            Bar *      Bob **
-      `;
-		expect(
-			layoutTimeline(tree.nodes, tree.root, tree.root, tree.root.selfDuration),
-		).to.deep.equal([
-			{ ...required, id: 1, width: 250, x: 0, row: 0, weight: 9 },
-			{ ...required, id: 2, width: 80, x: 20, row: 1, weight: 4 },
-			{ ...required, id: 3, width: 50, x: 40, row: 2, weight: 6 },
-			{ ...required, id: 4, width: 100, x: 130, row: 1, weight: 5 },
-			{ ...required, id: 5, width: 60, x: 150, row: 2, weight: 8 },
-		]);
-	});
-});
diff --git a/src/view/components/profiler/flamegraph/modes/ranked.test.ts b/src/view/components/profiler/flamegraph/ranked/ranked-utils.test.ts
similarity index 50%
rename from src/view/components/profiler/flamegraph/modes/ranked.test.ts
rename to src/view/components/profiler/flamegraph/ranked/ranked-utils.test.ts
index f08ee24..02e32ac 100644
--- a/src/view/components/profiler/flamegraph/modes/ranked.test.ts
+++ b/src/view/components/profiler/flamegraph/ranked/ranked-utils.test.ts
@@ -1,23 +1,23 @@
 import { expect } from "chai";
-import { layoutRanked } from "./ranked";
+import { toTransform } from "./ranked-utils";
 import { flames } from "../testHelpers";
-import { NodeTransform } from "../transform/focusNode";
+import { NodeTransform } from "../transform/shared";
 
 const required: Partial<NodeTransform> = {
 	visible: true,
 	maximized: false,
+	commitParent: false,
 };
 
-describe("layoutRanked", () => {
+describe("toTransform", () => {
 	it("should order nodes by selfDuration", () => {
 		const tree = flames`
         App **********
           Foo ****
             Bar *
-      `;
-		expect(
-			layoutRanked(tree.nodes, tree.root, tree.root.selfDuration),
-		).to.deep.equal([
+			`;
+
+		expect(toTransform(tree.commit)).to.deep.equal([
 			{ ...required, id: 1, width: 60, x: 0, row: 0, weight: 9 },
 			{ ...required, id: 3, width: 50, x: 0, row: 1, weight: 8 },
 			{ ...required, id: 2, width: 30, x: 0, row: 2, weight: 5 },
@@ -30,31 +30,11 @@ describe("layoutRanked", () => {
           Foo ****  Baz **
             Bar *
       `;
-		expect(
-			layoutRanked(tree.nodes, tree.root, tree.root.selfDuration),
-		).to.deep.equal([
+		expect(toTransform(tree.commit)).to.deep.equal([
 			{ ...required, id: 1, width: 80, x: 0, row: 0, weight: 9 },
 			{ ...required, id: 4, width: 60, x: 0, row: 1, weight: 7 },
 			{ ...required, id: 3, width: 50, x: 0, row: 2, weight: 6 },
 			{ ...required, id: 2, width: 30, x: 0, row: 3, weight: 3 },
 		]);
 	});
-
-	it("should pad nodes with a width of 0", () => {
-		const tree = flames`
-        App ******************
-          Foo ****  Baz **
-            Bar *
-      `;
-		tree.nodes[3].endTime = 0;
-		tree.nodes[3].selfDuration = 0;
-		expect(
-			layoutRanked(tree.nodes, tree.root, tree.root.selfDuration),
-		).to.deep.equal([
-			{ ...required, id: 1, width: 80, x: 0, row: 0, weight: 9 },
-			{ ...required, id: 3, width: 50, x: 0, row: 1, weight: 6 },
-			{ ...required, id: 2, width: 30, x: 0, row: 2, weight: 3 },
-			{ ...required, id: 4, width: 0.01, x: 0, row: 3, weight: 0 },
-		]);
-	});
 });
diff --git a/src/view/components/profiler/flamegraph/testHelpers.ts b/src/view/components/profiler/flamegraph/testHelpers.ts
index 430f4d6..3efec5b 100644
--- a/src/view/components/profiler/flamegraph/testHelpers.ts
+++ b/src/view/components/profiler/flamegraph/testHelpers.ts
@@ -1,7 +1,7 @@
 import { ID, DevNodeType, Tree } from "../../../store/types";
 import { ProfilerNode, CommitData } from "../data/commits";
 import { sortTimeline } from "./FlamegraphStore";
-import { NodeTransform } from "./transform/focusNode";
+import { NodeTransform } from "./transform/shared";
 
 /**
  * Parse a visual flamegraph DSL into a `ProfilerNode` tree. Each
diff --git a/src/view/components/profiler/flamegraph/transform/focusNode.test.ts b/src/view/components/profiler/flamegraph/transform/focusNode.test.ts
deleted file mode 100644
index 5b99667..0000000
--- a/src/view/components/profiler/flamegraph/transform/focusNode.test.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { expect } from "chai";
-import { focusNode, NodeTransform } from "./focusNode";
-
-const required = {
-	weight: -1,
-	maximized: true,
-	visible: true,
-};
-
-describe("focusNode", () => {
-	it("should focus flat tree", () => {
-		const nodes: NodeTransform[] = [
-			{ ...required, id: 1, row: 0, width: 100, x: 0 },
-			{ ...required, id: 2, row: 1, width: 50, x: 25 },
-			{ ...required, id: 3, row: 2, width: 25, x: 40 },
-		];
-
-		expect(focusNode(nodes, 2)).to.deep.equal([
-			{ ...required, id: 1, row: 0, width: 100, x: 0, maximized: true },
-			{ ...required, id: 2, row: 1, width: 100, x: 0, maximized: true },
-			{ ...required, id: 3, row: 2, width: 50, x: 30, maximized: false },
-		]);
-	});
-
-	it("should focus flat tree #2", () => {
-		const nodes: NodeTransform[] = [
-			{ ...required, id: 1, row: 0, width: 100, x: 0 },
-			{ ...required, id: 2, row: 1, width: 75, x: 25 },
-			{ ...required, id: 3, row: 2, width: 60, x: 40 },
-		];
-
-		expect(focusNode(nodes, 2)).to.deep.equal([
-			{ ...required, id: 1, row: 0, width: 100, x: 0, maximized: true },
-			{ ...required, id: 2, row: 1, width: 100, x: 0, maximized: true },
-			{ ...required, id: 3, row: 2, width: 80, x: 20, maximized: false },
-		]);
-	});
-
-	it("should focus sibling tree", () => {
-		const nodes: NodeTransform[] = [
-			{ ...required, id: 1, row: 0, width: 100, x: 0 },
-			{ ...required, id: 2, row: 1, width: 50, x: 25 },
-			{ ...required, id: 3, row: 2, width: 25, x: 40 },
-			{ ...required, id: 4, row: 1, width: 25, x: 75 },
-		];
-
-		expect(focusNode(nodes, 2)).to.deep.equal([
-			{ ...required, id: 1, row: 0, width: 100, x: 0, maximized: true },
-			{ ...required, id: 2, row: 1, width: 100, x: 0, maximized: true },
-			{ ...required, id: 4, row: 1, width: 50, x: 100, maximized: false },
-			{ ...required, id: 3, row: 2, width: 50, x: 30, maximized: false },
-		]);
-	});
-
-	it("should focus tree", () => {
-		const nodes: NodeTransform[] = [
-			{
-				...required,
-				id: 1,
-				x: 0,
-				row: 0,
-				width: 5.02,
-			},
-			{
-				...required,
-				id: 2,
-				x: 1,
-				row: 1,
-				width: 2,
-			},
-			{
-				...required,
-				id: 3,
-				x: 3,
-				row: 1,
-				width: 2.0199999999999996,
-			},
-			{
-				...required,
-				id: 4,
-				x: 5,
-				row: 2,
-				width: 0.009999999999999787,
-			},
-			{
-				...required,
-				id: 5,
-				x: 5.01,
-				row: 2,
-				width: 0.009999999999999787,
-			},
-		];
-
-		expect(focusNode(nodes, 2)).to.deep.equal([
-			{ ...required, id: 1, row: 0, width: 5.02, x: 0, maximized: true },
-			{ ...required, id: 2, row: 1, width: 5.02, x: 0, maximized: true },
-			{
-				...required,
-				id: 3,
-				row: 1,
-				width: 5.070199999999999,
-				x: 5.02,
-				maximized: false,
-			},
-			{
-				...required,
-				id: 4,
-				row: 2,
-				width: 0.025099999999999463,
-				x: 10.04,
-				maximized: false,
-			},
-			{
-				...required,
-				id: 5,
-				row: 2,
-				width: 0.025099999999999463,
-				x: 10.0651,
-				maximized: false,
-			},
-		]);
-	});
-});
diff --git a/src/view/components/profiler/flamegraph/transform/pad.test.ts b/src/view/components/profiler/flamegraph/transform/pad.test.ts
deleted file mode 100644
index 4b6e436..0000000
--- a/src/view/components/profiler/flamegraph/transform/pad.test.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { expect } from "chai";
-import { padNodes } from "./pad";
-import { NodeTransform } from "./focusNode";
-
-const required = {
-	weight: -1,
-	visible: true,
-	maximized: false,
-};
-
-describe("FlameGraph", () => {
-	describe("padNodes", () => {
-		it("should pad flat tree", () => {
-			const nodes: NodeTransform[] = [
-				{ ...required, id: 1, row: 0, width: 100, x: 0 },
-				{ ...required, id: 2, row: 1, width: 0, x: 75 },
-			];
-			expect(padNodes(nodes, 10)).to.deep.equal([
-				{ ...required, id: 1, row: 0, width: 110, x: 0 },
-				{ ...required, id: 2, row: 1, width: 10, x: 75 },
-			]);
-		});
-
-		it("should pad following nodes", () => {
-			const nodes: NodeTransform[] = [
-				{ ...required, id: 1, row: 0, width: 100, x: 0 },
-				{ ...required, id: 2, row: 1, width: 0, x: 25 },
-				{ ...required, id: 3, row: 1, width: 10, x: 50 },
-			];
-			expect(padNodes(nodes, 10)).to.deep.equal([
-				{ ...required, id: 1, row: 0, width: 110, x: 0 },
-				{ ...required, id: 2, row: 1, width: 10, x: 25 },
-				{ ...required, id: 3, row: 1, width: 10, x: 60 },
-			]);
-		});
-
-		it("should pad parent following nodes", () => {
-			const nodes: NodeTransform[] = [
-				{ ...required, id: 1, row: 0, width: 100, x: 0 },
-				{ ...required, id: 2, row: 1, width: 40, x: 25 },
-				{ ...required, id: 3, row: 2, width: 0, x: 40 },
-				{ ...required, id: 4, row: 1, width: 20, x: 70 },
-			];
-			expect(padNodes(nodes, 10)).to.deep.equal([
-				{ ...required, id: 1, row: 0, width: 110, x: 0 },
-				{ ...required, id: 2, row: 1, width: 50, x: 25 },
-				{ ...required, id: 3, row: 2, width: 10, x: 40 },
-				{ ...required, id: 4, row: 1, width: 20, x: 80 },
-			]);
-		});
-
-		it("should pad zeroed nodes on the same location", () => {
-			const nodes: NodeTransform[] = [
-				{ ...required, id: 1, row: 0, width: 100, x: 0 },
-				{ ...required, id: 2, row: 1, width: 0, x: 75 },
-				{ ...required, id: 3, row: 1, width: 0, x: 75 },
-			];
-			expect(padNodes(nodes, 10)).to.deep.equal([
-				{ ...required, id: 1, row: 0, width: 120, x: 0 },
-				{ ...required, id: 2, row: 1, width: 10, x: 75 },
-				{ ...required, id: 3, row: 1, width: 10, x: 85 },
-			]);
-		});
-
-		it("should pad zeroed nodes on the same location #2", () => {
-			const nodes: NodeTransform[] = [
-				{ ...required, id: 1, row: 0, width: 100, x: 0 },
-				{ ...required, id: 2, row: 1, width: 0, x: 75 },
-				{ ...required, id: 3, row: 1, width: 0, x: 75 },
-				{ ...required, id: 4, row: 1, width: 0, x: 75 },
-				{ ...required, id: 5, row: 1, width: 15, x: 85 },
-			];
-			expect(padNodes(nodes, 10)).to.deep.equal([
-				{ ...required, id: 1, row: 0, width: 130, x: 0 },
-				{ ...required, id: 2, row: 1, width: 10, x: 75 },
-				{ ...required, id: 3, row: 1, width: 10, x: 85 },
-				{ ...required, id: 4, row: 1, width: 10, x: 95 },
-				{ ...required, id: 5, row: 1, width: 15, x: 115 },
-			]);
-		});
-	});
-});
diff --git a/test-e2e/index.html b/test-e2e/index.html
index 261594e..0f638c9 100644
--- a/test-e2e/index.html
+++ b/test-e2e/index.html
@@ -53,6 +53,7 @@
 		</div>
 		<script src="vendor/preact.js"></script>
 		<script src="vendor/preactHooks.js"></script>
+		<script src="vendor/preactCompat.js"></script>
 		<script src="vendor/preactDevtools.js"></script>
 		<script src="vendor/htm.js"></script>
 		<script>
diff --git a/test-e2e/test-utils.ts b/test-e2e/test-utils.ts
index 047089f..fcf9993 100644
--- a/test-e2e/test-utils.ts
+++ b/test-e2e/test-utils.ts
@@ -65,6 +65,11 @@ export async function newTestPage(
 				"preact.js",
 				`./vendor/preact/${preactVersion}/preact.js`,
 			) &&
+			!mockResponse(
+				req,
+				"preactCompat.js",
+				`./vendor/preact/${preactVersion}/preactCompat.js`,
+			) &&
 			!mockResponse(
 				req,
 				"preactHooks.js",
@@ -254,7 +259,12 @@ export async function clickTab(
 	page: Page,
 	tab: "ELEMENTS" | "PROFILER" | "SETTINGS",
 ) {
-	await click(page, `[name="root-panel"][value="${tab}"]`);
+	await page.evaluate(name => {
+		return document
+			.querySelector(`[name="root-panel"][value="${name}"]`)!
+			.closest("label")!
+			.click();
+	}, tab);
 }
 
 // This injects a box into the page that moves with the mouse;
diff --git a/test-e2e/tests/fixtures/memo.js b/test-e2e/tests/fixtures/memo.js
new file mode 100644
index 0000000..d5c2986
--- /dev/null
+++ b/test-e2e/tests/fixtures/memo.js
@@ -0,0 +1,32 @@
+const { h, render } = preact;
+const { useState } = preactHooks;
+const { memo } = preactCompat;
+
+function Value(props) {
+	return html`<p>${props.children}</p>`;
+}
+
+function Display(props) {
+	return html`
+		<div data-testid="result">
+			Counter: <br />
+			<${Value}>${props.value}<//>,
+			<${Value}>${props.value}<//>
+		</div>
+	`;
+}
+
+const MemoDisplay = memo(Display);
+
+function Counter() {
+	const [v, set] = useState(0);
+
+	return html`
+		<div style="padding: 2rem;">
+			<${MemoDisplay} value="42" />
+			<button onClick=${() => set(v + 1)}>Increment</button>
+		</div>
+	`;
+}
+
+render(h(Counter), document.getElementById("app"));
diff --git a/test-e2e/tests/fixtures/memo2.js b/test-e2e/tests/fixtures/memo2.js
new file mode 100644
index 0000000..16ed557
--- /dev/null
+++ b/test-e2e/tests/fixtures/memo2.js
@@ -0,0 +1,34 @@
+const { h, render } = preact;
+const { useState } = preactHooks;
+const { memo } = preactCompat;
+
+function Value1(props) {
+	return html`<p>${props.children}</p>`;
+}
+function Value2(props) {
+	return html`<p>${props.children}</p>`;
+}
+
+function Display(props) {
+	return html`
+		<div data-testid="result">
+			Counter: <br />
+			<${Value1}>${props.value}<//>, <${Value2}>${props.value}<//>,
+		</div>
+	`;
+}
+
+const MemoDisplay = memo(Display);
+
+function Counter() {
+	const [v, set] = useState(0);
+
+	return html`
+		<div style="padding: 2rem;">
+			<${MemoDisplay} value="42" />
+			<button onClick=${() => set(v + 1)}>Increment</button>
+		</div>
+	`;
+}
+
+render(h(Counter), document.getElementById("app"));
diff --git a/test-e2e/tests/fixtures/profiler-3.js b/test-e2e/tests/fixtures/profiler-3.js
new file mode 100644
index 0000000..e1916e2
--- /dev/null
+++ b/test-e2e/tests/fixtures/profiler-3.js
@@ -0,0 +1,35 @@
+const { h, render } = preact;
+const { useState } = preactHooks;
+
+function Value(props) {
+	return html`<p>${props.children}</p>`;
+}
+
+function Display(props) {
+	return html`
+		<div data-testid="result">Counter: <${Value}>${props.value}<//></div>
+	`;
+}
+
+let i = 1;
+function Counter() {
+	const [v, set] = useState(0);
+
+	return html`
+		<div style="padding: 2rem;">
+			<${Display} value=${v} />
+			<button id="counter-1${i++}" onClick=${() => set(v + 1)}>
+				Increment
+			</button>
+		</div>
+	`;
+}
+
+function Foo() {
+	return html`
+		<div>foo</div>
+		<${Counter} />
+	`;
+}
+
+render(h(Foo), document.getElementById("app"));
diff --git a/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts b/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts
new file mode 100644
index 0000000..954f068
--- /dev/null
+++ b/test-e2e/tests/profiler/flamegraph/memo-sibling.test.ts
@@ -0,0 +1,95 @@
+import {
+	newTestPage,
+	click,
+	waitForAttribute,
+	getSize,
+	typeText,
+	clickTab,
+} from "../../../test-utils";
+import { expect } from "chai";
+import { closePage, clickTestId, waitForTestId } from "pentf/browser_utils";
+import { Page } from "puppeteer";
+
+async function getFlameNodes(page: Page) {
+	return await page.$$eval('[data-type="flamegraph"] > *', els => {
+		return els.map(el => {
+			const text = el.textContent!;
+			return {
+				maximized: el.hasAttribute("data-maximized"),
+				name: text.slice(0, text.lastIndexOf("(") - 1),
+				visible: el.hasAttribute("data-visible"),
+			};
+		});
+	});
+}
+
+export const description = "Correctly position memoized sibling sub-trees";
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "memo2");
+
+	await clickTestId(devtools, "filter-menu-button");
+	await waitForTestId(devtools, "filter-popup");
+	await clickTestId(devtools, "add-filter");
+	await typeText(
+		devtools,
+		'[data-testid="filter-popup"] input[type="text"]',
+		"Display",
+	);
+	await click(
+		devtools,
+		'[data-testid="filter-popup"] input[type="checkbox"]:not(:checked)',
+	);
+	await clickTestId(devtools, "filter-update");
+	await clickTestId(devtools, "filter-menu-button");
+
+	await devtools.waitForFunction(
+		() => {
+			return document.querySelector('[data-testid="filter-popup"]') === null;
+		},
+		{ timeout: 2000 },
+	);
+
+	await clickTab(devtools, "PROFILER");
+
+	const recordBtn = '[data-testid="record-btn"]';
+	await click(devtools, recordBtn);
+
+	await waitForAttribute(devtools, recordBtn, "title", /Stop Recording/);
+
+	await click(page, "button");
+
+	await click(devtools, recordBtn);
+
+	const nodes = await getFlameNodes(devtools);
+	expect(nodes).to.deep.equal([
+		{ maximized: true, name: "Fragment", visible: true },
+		{ maximized: false, name: "Counter", visible: true },
+		{ maximized: false, name: "Value1", visible: true },
+		{ maximized: false, name: "Value2", visible: true },
+	]);
+
+	const memoSize = await getSize(
+		devtools,
+		'[data-type="flamegraph"] *:nth-child(2)',
+	);
+	const staticSize1 = await getSize(
+		devtools,
+		'[data-type="flamegraph"] *:nth-child(3)',
+	);
+	const staticSize2 = await getSize(
+		devtools,
+		'[data-type="flamegraph"] *:nth-child(4)',
+	);
+
+	expect(memoSize.x <= staticSize1.x).to.equal(true);
+	expect(
+		memoSize.x + memoSize.width >= staticSize1.x + staticSize1.width,
+	).to.equal(true);
+	expect(memoSize.x <= staticSize2.x).to.equal(true);
+	expect(
+		memoSize.x + memoSize.width >= staticSize2.x + staticSize2.width,
+	).to.equal(true);
+
+	await closePage(page);
+}
diff --git a/test-e2e/tests/profiler/flamegraph/memo.test.ts b/test-e2e/tests/profiler/flamegraph/memo.test.ts
new file mode 100644
index 0000000..1cf9e4e
--- /dev/null
+++ b/test-e2e/tests/profiler/flamegraph/memo.test.ts
@@ -0,0 +1,65 @@
+import {
+	newTestPage,
+	click,
+	waitForAttribute,
+	getSize,
+} from "../../../test-utils";
+import { expect } from "chai";
+import { closePage } from "pentf/browser_utils";
+import { Page } from "puppeteer";
+
+async function getFlameNodes(page: Page) {
+	return await page.$$eval('[data-type="flamegraph"] > *', els => {
+		return els.map(el => {
+			const text = el.textContent!;
+			return {
+				maximized: el.hasAttribute("data-maximized"),
+				name: text.slice(0, text.lastIndexOf("(") - 1),
+				visible: el.hasAttribute("data-visible"),
+			};
+		});
+	});
+}
+
+export const description = "Correctly position memoized sub-trees";
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "memo");
+
+	await click(devtools, '[name="root-panel"][value="PROFILER"]');
+
+	const recordBtn = '[data-testid="record-btn"]';
+	await click(devtools, recordBtn);
+
+	await waitForAttribute(devtools, recordBtn, "title", /Stop Recording/);
+
+	await click(page, "button");
+
+	await click(devtools, recordBtn);
+
+	const nodes = await getFlameNodes(devtools);
+	expect(nodes).to.deep.equal([
+		{ maximized: true, name: "Fragment", visible: true },
+		{ maximized: false, name: "Counter", visible: true },
+		{ maximized: false, name: "Memo(Display)", visible: true },
+		{ maximized: false, name: "Display", visible: true },
+		{ maximized: false, name: "Value", visible: true },
+		{ maximized: false, name: "Value", visible: true },
+	]);
+
+	const memoSize = await getSize(
+		devtools,
+		'[data-type="flamegraph"] *:nth-child(3)',
+	);
+	const staticSize = await getSize(
+		devtools,
+		'[data-type="flamegraph"] *:nth-child(4)',
+	);
+
+	expect(memoSize.x <= staticSize.x).to.equal(true);
+	expect(
+		memoSize.x + memoSize.width >= staticSize.x + staticSize.width,
+	).to.equal(true);
+
+	await closePage(page);
+}
diff --git a/test-e2e/tests/profiler/flamegraph/profiler-flamegraph-focus.test.ts b/test-e2e/tests/profiler/flamegraph/profiler-flamegraph-focus.test.ts
new file mode 100644
index 0000000..20f2d14
--- /dev/null
+++ b/test-e2e/tests/profiler/flamegraph/profiler-flamegraph-focus.test.ts
@@ -0,0 +1,70 @@
+import {
+	newTestPage,
+	click,
+	waitForAttribute,
+	clickNestedText,
+} from "../../../test-utils";
+import { expect } from "chai";
+import { closePage } from "pentf/browser_utils";
+import { Page } from "puppeteer";
+
+async function getFlameNodes(page: Page) {
+	return await page.$$eval('[data-type="flamegraph"] > *', els => {
+		return els.map(el => {
+			const text = el.textContent!;
+			return {
+				maximized: el.hasAttribute("data-maximized"),
+				name: text.slice(0, text.indexOf("(") - 1),
+			};
+		});
+	});
+}
+
+export const description = "Focus nodes in flamegraph layout";
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "profiler-3");
+
+	await click(devtools, '[name="root-panel"][value="PROFILER"]');
+
+	const recordBtn = '[data-testid="record-btn"]';
+	await click(devtools, recordBtn);
+
+	await waitForAttribute(devtools, recordBtn, "title", /Stop Recording/);
+
+	await click(page, "button");
+	await click(page, "button");
+
+	await click(devtools, recordBtn);
+
+	// Initially only the top node should be focused.
+	expect(await getFlameNodes(devtools)).to.deep.equal([
+		{ maximized: true, name: "Fragment" },
+		{ maximized: false, name: "Foo" },
+		{ maximized: false, name: "Counter" },
+		{ maximized: false, name: "Display" },
+		{ maximized: false, name: "Value" },
+	]);
+
+	// Focus 2nd node manually
+	await clickNestedText(devtools, "Counter");
+	expect(await getFlameNodes(devtools)).to.deep.equal([
+		{ maximized: true, name: "Fragment" },
+		{ maximized: true, name: "Foo" },
+		{ maximized: true, name: "Counter" },
+		{ maximized: false, name: "Display" },
+		{ maximized: false, name: "Value" },
+	]);
+
+	// Focus 1st node again
+	await clickNestedText(devtools, "Fragment");
+	expect(await getFlameNodes(devtools)).to.deep.equal([
+		{ maximized: true, name: "Fragment" },
+		{ maximized: false, name: "Foo" },
+		{ maximized: false, name: "Counter" },
+		{ maximized: false, name: "Display" },
+		{ maximized: false, name: "Value" },
+	]);
+
+	await closePage(page);
+}
diff --git a/test-e2e/tests/profiler/profiler-unaffected.test.ts b/test-e2e/tests/profiler/flamegraph/profiler-unaffected.test.ts
similarity index 66%
rename from test-e2e/tests/profiler/profiler-unaffected.test.ts
rename to test-e2e/tests/profiler/flamegraph/profiler-unaffected.test.ts
index 9b4225f..f98966c 100644
--- a/test-e2e/tests/profiler/profiler-unaffected.test.ts
+++ b/test-e2e/tests/profiler/flamegraph/profiler-unaffected.test.ts
@@ -3,7 +3,7 @@ import {
 	click,
 	waitForAttribute,
 	getText$$,
-} from "../../test-utils";
+} from "../../../test-utils";
 import { expect } from "chai";
 import { closePage } from "pentf/browser_utils";
 
@@ -24,16 +24,16 @@ export async function run(config: any) {
 
 	await click(devtools, recordBtn);
 
-	const nodes = await getText$$(
+	let nodes = await getText$$(
 		devtools,
-		'[data-type="flamegraph"] > *:not([data-weight="-2"])',
+		'[data-type="flamegraph"] > *:not([data-weight="-1"])',
 	);
-	expect(nodes.map(n => n.split(" ")[0])).to.deep.equal([
-		"Fragment",
-		"App",
-		"Counter",
-		"Display",
-	]);
+	expect(nodes.map(n => n.split(" ")[0])).to.deep.equal(["Counter", "Display"]);
+	nodes = await getText$$(
+		devtools,
+		'[data-type="flamegraph"] > [data-active-parent]',
+	);
+	expect(nodes.map(n => n.split(" ")[0])).to.deep.equal(["Fragment"]);
 
 	await closePage(page);
 }
diff --git a/test-e2e/tests/profiler/ranked/profiler-ranked-focus.test.ts b/test-e2e/tests/profiler/ranked/profiler-ranked-focus.test.ts
new file mode 100644
index 0000000..5122bb1
--- /dev/null
+++ b/test-e2e/tests/profiler/ranked/profiler-ranked-focus.test.ts
@@ -0,0 +1,65 @@
+import {
+	newTestPage,
+	click,
+	waitForAttribute,
+	clickNestedText,
+} from "../../../test-utils";
+import { expect } from "chai";
+import { closePage } from "pentf/browser_utils";
+import { Page } from "puppeteer";
+
+async function getFlameNodes(page: Page) {
+	return await page.$$eval('[data-type="ranked"] > *', els => {
+		return els.map(el => {
+			const text = el.textContent!;
+			return {
+				maximized: el.hasAttribute("data-maximized"),
+				name: text.slice(0, text.indexOf("(") - 1),
+			};
+		});
+	});
+}
+
+export const description = "Focus nodes in ranked layout";
+
+export async function run(config: any) {
+	const { page, devtools } = await newTestPage(config, "profiler-3");
+
+	await click(devtools, '[name="root-panel"][value="PROFILER"]');
+	await click(devtools, '[name="flamegraph_mode"][value="RANKED"]');
+
+	const recordBtn = '[data-testid="record-btn"]';
+	await click(devtools, recordBtn);
+
+	await waitForAttribute(devtools, recordBtn, "title", /Stop Recording/);
+
+	await click(page, "button");
+	await click(page, "button");
+
+	await click(devtools, recordBtn);
+
+	// Initially only the top node should be focused.
+	expect(await getFlameNodes(devtools)).to.deep.equal([
+		{ maximized: true, name: "Counter" },
+		{ maximized: false, name: "Display" },
+		{ maximized: false, name: "Value" },
+	]);
+
+	// Focus 2nd node manually
+	await clickNestedText(devtools, "Display");
+	expect(await getFlameNodes(devtools)).to.deep.equal([
+		{ maximized: true, name: "Counter" },
+		{ maximized: true, name: "Display" },
+		{ maximized: false, name: "Value" },
+	]);
+
+	// Focus 1st node again
+	await clickNestedText(devtools, "Counter");
+	expect(await getFlameNodes(devtools)).to.deep.equal([
+		{ maximized: true, name: "Counter" },
+		{ maximized: false, name: "Display" },
+		{ maximized: false, name: "Value" },
+	]);
+
+	await closePage(page);
+}
diff --git a/test-e2e/tests/profiler/profiler-ranked.test.ts b/test-e2e/tests/profiler/ranked/profiler-ranked.test.ts
similarity index 97%
rename from test-e2e/tests/profiler/profiler-ranked.test.ts
rename to test-e2e/tests/profiler/ranked/profiler-ranked.test.ts
index 6eb2354..0e36e75 100644
--- a/test-e2e/tests/profiler/profiler-ranked.test.ts
+++ b/test-e2e/tests/profiler/ranked/profiler-ranked.test.ts
@@ -3,7 +3,7 @@ import {
 	click,
 	waitForAttribute,
 	getText,
-} from "../../test-utils";
+} from "../../../test-utils";
 import { expect } from "chai";
 import { closePage } from "pentf/browser_utils";
 
diff --git a/test-e2e/vendor/preact/10.0.0-rc.0/preactCompat.js b/test-e2e/vendor/preact/10.0.0-rc.0/preactCompat.js
new file mode 100644
index 0000000..2d1549b
--- /dev/null
+++ b/test-e2e/vendor/preact/10.0.0-rc.0/preactCompat.js
@@ -0,0 +1,347 @@
+!(function (n, t) {
+	"object" == typeof exports && "undefined" != typeof module
+		? t(exports, require("preact/hooks"), require("preact"))
+		: "function" == typeof define && define.amd
+		? define(["exports", "preact/hooks", "preact"], t)
+		: t((n.preactCompat = {}), n.preactHooks, n.preact);
+})(this, function (n, t, e) {
+	function r(n, t) {
+		for (var e in t) n[e] = t[e];
+		return n;
+	}
+	function o(n) {
+		var t = n.parentNode;
+		t && t.removeChild(n);
+	}
+	var i = e.options.__e;
+	function u() {
+		this.t = [];
+	}
+	function f(n) {
+		var t, r, o;
+		function i(i) {
+			if (
+				(t ||
+					(t = n()).then(
+						function (n) {
+							r = n.default;
+						},
+						function (n) {
+							o = n;
+						},
+					),
+				o)
+			)
+				throw o;
+			if (!r) throw t;
+			return e.createElement(r, i);
+		}
+		return (i.displayName = "Lazy"), i;
+	}
+	(e.options.__e = function (n, t, e) {
+		if (n.then && e)
+			for (var r, o = t; (o = o.__p); )
+				if ((r = o.__c) && r.o)
+					return e && ((t.__e = e.__e), (t.__k = e.__k)), void r.o(n);
+		i(n, t, e);
+	}),
+		((u.prototype = new e.Component()).o = function (n) {
+			var t = this;
+			t.t.push(n);
+			var r = function () {
+				(t.t[t.t.indexOf(n)] = t.t[t.t.length - 1]),
+					t.t.pop(),
+					0 == t.t.length &&
+						(e._e(t.props.fallback),
+						(t.__v.__e = null),
+						(t.__v.__k = t.state.i),
+						t.setState({ i: null }));
+			};
+			null == t.state.i &&
+				(t.setState({ i: t.__v.__k }),
+				(function n(t) {
+					for (var e = 0; e < t.length; e++) {
+						var r = t[e];
+						null != r &&
+							("function" != typeof r.type && r.__e
+								? o(r.__e)
+								: r.__k && n(r.__k));
+					}
+				})(t.__v.__k),
+				(t.__v.__k = [])),
+				n.then(r, r);
+		}),
+		(u.prototype.render = function (n, t) {
+			return t.i ? n.fallback : n.children;
+		});
+	var c =
+			("undefined" != typeof Symbol &&
+				Symbol.for &&
+				Symbol.for("react.element")) ||
+			60103,
+		a = /^(?:accent|alignment|arabic|baseline|cap|clip|color|fill|flood|font|glyph|horiz|marker|overline|paint|stop|strikethrough|stroke|text|underline|unicode|units|v|vector|vert|word|writing|x)[A-Z]/,
+		l = e.options.event;
+	function d(n) {
+		return b.bind(null, n);
+	}
+	function s(n, t, r) {
+		for (; t.firstChild; ) o(t.firstChild);
+		return e.render(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	e.options.event = function (n) {
+		return l && (n = l(n)), (n.persist = function () {}), (n.nativeEvent = n);
+	};
+	var v = function () {};
+	function p(n) {
+		var t = this,
+			r = n.container,
+			o = e.h(v, { context: t.context }, n.vnode);
+		return (
+			t.u &&
+				t.u !== r &&
+				(t.l.parentNode && t.u.removeChild(t.l), e._e(t.s), (t.v = !1)),
+			n.vnode
+				? t.v
+					? s(o, r)
+					: ((t.l = document.createTextNode("")),
+					  e.hydrate("", r),
+					  r.insertBefore(t.l, r.firstChild),
+					  (t.v = !0),
+					  (t.u = r),
+					  e.render(o, r, t.l))
+				: t.v && (t.l.parentNode && t.u.removeChild(t.l), e._e(t.s)),
+			(t.s = o),
+			(t.componentWillUnmount = function () {
+				t.l.parentNode && t.u.removeChild(t.l), e._e(t.s);
+			}),
+			null
+		);
+	}
+	function h(n, t) {
+		return e.h(p, { vnode: n, container: t });
+	}
+	(v.prototype.getChildContext = function () {
+		return this.props.context;
+	}),
+		(v.prototype.render = function (n) {
+			return n.children;
+		});
+	var m = function (n, t) {
+			return n ? e.toChildArray(n).map(t) : null;
+		},
+		y = {
+			map: m,
+			forEach: m,
+			count: function (n) {
+				return n ? e.toChildArray(n).length : 0;
+			},
+			only: function (n) {
+				if (1 !== (n = e.toChildArray(n)).length)
+					throw new Error("Children.only() expects only one child.");
+				return n[0];
+			},
+			toArray: e.toChildArray,
+		};
+	function b() {
+		for (var n = [], t = arguments.length; t--; ) n[t] = arguments[t];
+		var r = e.h.apply(void 0, n),
+			o = r.type,
+			i = r.props;
+		return (
+			"function" != typeof o &&
+				(i.defaultValue &&
+					(i.value || 0 === i.value || (i.value = i.defaultValue),
+					delete i.defaultValue),
+				Array.isArray(i.value) &&
+					i.multiple &&
+					"select" === o &&
+					(e.toChildArray(i.children).forEach(function (n) {
+						-1 != i.value.indexOf(n.props.value) && (n.props.selected = !0);
+					}),
+					delete i.value),
+				(function (n, t) {
+					var e, r, o;
+					for (o in t) if ((e = a.test(o))) break;
+					if (e)
+						for (o in ((r = n.props = {}), t))
+							r[a.test(o) ? o.replace(/([A-Z0-9])/, "-$1").toLowerCase() : o] =
+								t[o];
+				})(r, i)),
+			(r.preactCompatNormalized = !1),
+			g(r)
+		);
+	}
+	function g(n) {
+		return (
+			(n.preactCompatNormalized = !0),
+			(function (n) {
+				var t = n.props;
+				(t.class || t.className) &&
+					((C.enumerable = "className" in t),
+					t.className && (t.class = t.className),
+					Object.defineProperty(t, "className", C));
+			})(n),
+			n
+		);
+	}
+	function w(n) {
+		return x(n) ? g(e.cloneElement.apply(null, arguments)) : n;
+	}
+	function x(n) {
+		return !!n && n.$$typeof === c;
+	}
+	function k(n) {
+		return !!n.__k && (e.render(null, n), !0);
+	}
+	var C = {
+		configurable: !0,
+		get: function () {
+			return this.class;
+		},
+	};
+	function A(n, t) {
+		for (var e in n) if (!(e in t)) return !0;
+		for (var r in t) if (n[r] !== t[r]) return !0;
+		return !1;
+	}
+	function E(n) {
+		return (n && (n.base || (1 === n.nodeType && n))) || null;
+	}
+	var j = (function (n) {
+		function t(t) {
+			n.call(this, t), (this.isPureReactComponent = !0);
+		}
+		return (
+			n && (t.__proto__ = n),
+			((t.prototype = Object.create(n && n.prototype)).constructor = t),
+			(t.prototype.shouldComponentUpdate = function (n, t) {
+				return A(this.props, n) || A(this.state, t);
+			}),
+			t
+		);
+	})(e.Component);
+	function N(n, t) {
+		function o(n) {
+			var e = this.props.ref,
+				r = e == n.ref;
+			return (
+				r || (e.call ? e(null) : (e.current = null)),
+				(t ? !t(this.props, n) : A(this.props, n)) || !r
+			);
+		}
+		function i(t) {
+			return (this.shouldComponentUpdate = o), e.h(n, r({}, t));
+		}
+		return (
+			(i.displayName = "Memo(" + (n.displayName || n.name) + ")"), (i.p = !0), i
+		);
+	}
+	function O(n, t) {
+		Object.defineProperty(n.prototype, "UNSAFE_" + t, {
+			configurable: !0,
+			get: function () {
+				return this[t];
+			},
+			set: function (n) {
+				this[t] = n;
+			},
+		});
+	}
+	function S(n) {
+		function t(t) {
+			var e = t.ref;
+			return delete t.ref, n(t, e);
+		}
+		return (
+			(t.p = !0),
+			(t.displayName = "ForwardRef(" + (n.displayName || n.name) + ")"),
+			t
+		);
+	}
+	(e.Component.prototype.isReactComponent = {}),
+		O(e.Component, "componentWillMount"),
+		O(e.Component, "componentWillReceiveProps"),
+		O(e.Component, "componentWillUpdate");
+	var F = e.options.vnode;
+	e.options.vnode = function (n) {
+		(n.$$typeof = c),
+			(function (t) {
+				var e = n.type,
+					r = n.props;
+				if (r && "string" == typeof e) {
+					var o = {};
+					for (var i in r) o[i.toLowerCase()] = i;
+					if (
+						(o.ondoubleclick &&
+							((r.ondblclick = r[o.ondoubleclick]), delete r[o.ondoubleclick]),
+						o.onbeforeinput &&
+							((r.onbeforeinput = r[o.onbeforeinput]),
+							delete r[o.onbeforeinput]),
+						o.onchange &&
+							("textarea" === e ||
+								("input" === e.toLowerCase() && !/^fil|che|rad/i.test(r.type))))
+					) {
+						var u = o.oninput || "oninput";
+						r[u] || ((r[u] = r[o.onchange]), delete r[o.onchange]);
+					}
+				}
+			})();
+		var t = n.type;
+		t && t.p && n.ref && ((n.props.ref = n.ref), (n.ref = null)), F && F(n);
+	};
+	var R = function (n, t) {
+			return n(t);
+		},
+		_ = r(
+			{
+				version: "16.8.0",
+				Children: y,
+				render: s,
+				hydrate: s,
+				unmountComponentAtNode: k,
+				createPortal: h,
+				createElement: b,
+				createContext: e.createContext,
+				createFactory: d,
+				cloneElement: w,
+				createRef: e.createRef,
+				Fragment: e.Fragment,
+				isValidElement: x,
+				findDOMNode: E,
+				Component: e.Component,
+				PureComponent: j,
+				memo: N,
+				forwardRef: S,
+				unstable_batchedUpdates: R,
+				Suspense: u,
+				lazy: f,
+			},
+			t,
+		);
+	Object.keys(t).forEach(function (e) {
+		n[e] = t[e];
+	}),
+		(n.createContext = e.createContext),
+		(n.createRef = e.createRef),
+		(n.Fragment = e.Fragment),
+		(n.Component = e.Component),
+		(n.version = "16.8.0"),
+		(n.Children = y),
+		(n.render = s),
+		(n.hydrate = s),
+		(n.unmountComponentAtNode = k),
+		(n.createPortal = h),
+		(n.createElement = b),
+		(n.createFactory = d),
+		(n.cloneElement = w),
+		(n.isValidElement = x),
+		(n.findDOMNode = E),
+		(n.PureComponent = j),
+		(n.memo = N),
+		(n.forwardRef = S),
+		(n.unstable_batchedUpdates = R),
+		(n.Suspense = u),
+		(n.lazy = f),
+		(n.default = _);
+});
+//# sourceMappingURL=compat.umd.js.map
diff --git a/test-e2e/vendor/preact/10.3.4/preactCompat.js b/test-e2e/vendor/preact/10.3.4/preactCompat.js
new file mode 100644
index 0000000..c98a3fc
--- /dev/null
+++ b/test-e2e/vendor/preact/10.3.4/preactCompat.js
@@ -0,0 +1,442 @@
+!(function (n, t) {
+	"object" == typeof exports && "undefined" != typeof module
+		? t(exports, require("preact/hooks"), require("preact"))
+		: "function" == typeof define && define.amd
+		? define(["exports", "preact/hooks", "preact"], t)
+		: t((n.preactCompat = {}), n.preactHooks, n.preact);
+})(this, function (n, t, e) {
+	function r(n, t) {
+		for (var e in t) n[e] = t[e];
+		return n;
+	}
+	function u(n, t) {
+		for (var e in n) if ("__source" !== e && !(e in t)) return !0;
+		for (var r in t) if ("__source" !== r && n[r] !== t[r]) return !0;
+		return !1;
+	}
+	var i = (function (n) {
+		var t, e;
+		function r(t) {
+			var e;
+			return ((e = n.call(this, t) || this).isPureReactComponent = !0), e;
+		}
+		return (
+			(e = n),
+			((t = r).prototype = Object.create(e.prototype)),
+			(t.prototype.constructor = t),
+			(t.__proto__ = e),
+			(r.prototype.shouldComponentUpdate = function (n, t) {
+				return u(this.props, n) || u(this.state, t);
+			}),
+			r
+		);
+	})(e.Component);
+	function o(n, t) {
+		function i(n) {
+			var e = this.props.ref,
+				r = e == n.ref;
+			return (
+				!r && e && (e.call ? e(null) : (e.current = null)),
+				t ? !t(this.props, n) || !r : u(this.props, n)
+			);
+		}
+		function o(t) {
+			return (this.shouldComponentUpdate = i), e.createElement(n, r({}, t));
+		}
+		return (
+			(o.prototype.isReactComponent = !0),
+			(o.displayName = "Memo(" + (n.displayName || n.name) + ")"),
+			(o.t = !0),
+			o
+		);
+	}
+	var f = e.options.__b;
+	function c(n) {
+		function t(t) {
+			var e = r({}, t);
+			return delete e.ref, n(e, t.ref);
+		}
+		return (
+			(t.prototype.isReactComponent = t.t = !0),
+			(t.displayName = "ForwardRef(" + (n.displayName || n.name) + ")"),
+			t
+		);
+	}
+	e.options.__b = function (n) {
+		n.type && n.type.t && n.ref && ((n.props.ref = n.ref), (n.ref = null)),
+			f && f(n);
+	};
+	var l = function (n, t) {
+			return n
+				? e.toChildArray(n).reduce(function (n, e, r) {
+						return n.concat(t(e, r));
+				  }, [])
+				: null;
+		},
+		a = {
+			map: l,
+			forEach: l,
+			count: function (n) {
+				return n ? e.toChildArray(n).length : 0;
+			},
+			only: function (n) {
+				if (1 !== (n = e.toChildArray(n)).length)
+					throw new Error("Children.only() expects only one child.");
+				return n[0];
+			},
+			toArray: e.toChildArray,
+		},
+		s = e.options.__e;
+	function h(n) {
+		return (
+			n && (((n = r({}, n)).__c = null), (n.__k = n.__k && n.__k.map(h))), n
+		);
+	}
+	function v(n) {
+		(this.__u = 0), (this.u = null), (this.__b = null);
+	}
+	function d(n) {
+		var t = n.__.__c;
+		return t && t.i && t.i(n);
+	}
+	function p(n) {
+		var t, r, u;
+		function i(i) {
+			if (
+				(t ||
+					(t = n()).then(
+						function (n) {
+							r = n.default || n;
+						},
+						function (n) {
+							u = n;
+						},
+					),
+				u)
+			)
+				throw u;
+			if (!r) throw t;
+			return e.createElement(r, i);
+		}
+		return (i.displayName = "Lazy"), (i.t = !0), i;
+	}
+	function m() {
+		(this.o = null), (this.l = null);
+	}
+	(e.options.__e = function (n, t, e) {
+		if (n.then)
+			for (var r, u = t; (u = u.__); )
+				if ((r = u.__c) && r.__c) return r.__c(n, t.__c);
+		s(n, t, e);
+	}),
+		((v.prototype = new e.Component()).__c = function (n, t) {
+			var e = this;
+			null == e.u && (e.u = []), e.u.push(t);
+			var r = d(e.__v),
+				u = !1,
+				i = function () {
+					u || ((u = !0), r ? r(o) : o());
+				};
+			(t.__c = t.componentWillUnmount),
+				(t.componentWillUnmount = function () {
+					i(), t.__c && t.__c();
+				});
+			var o = function () {
+				var n;
+				if (!--e.__u)
+					for (
+						e.__v.__k[0] = e.state.i, e.setState({ i: (e.__b = null) });
+						(n = e.u.pop());
+
+					)
+						n.forceUpdate();
+			};
+			e.__u++ || e.setState({ i: (e.__b = e.__v.__k[0]) }), n.then(i, i);
+		}),
+		(v.prototype.render = function (n, t) {
+			return (
+				this.__b && ((this.__v.__k[0] = h(this.__b)), (this.__b = null)),
+				[
+					e.createElement(e.Component, null, t.i ? null : n.children),
+					t.i && n.fallback,
+				]
+			);
+		});
+	var y = function (n, t, e) {
+		if (
+			(++e[1] === e[0] && n.l.delete(t),
+			n.props.revealOrder && ("t" !== n.props.revealOrder[0] || !n.l.size))
+		)
+			for (e = n.o; e; ) {
+				for (; e.length > 3; ) e.pop()();
+				if (e[1] < e[0]) break;
+				n.o = e = e[2];
+			}
+	};
+	((m.prototype = new e.Component()).i = function (n) {
+		var t = this,
+			e = d(t.__v),
+			r = t.l.get(n);
+		return (
+			r[0]++,
+			function (u) {
+				var i = function () {
+					t.props.revealOrder ? (r.push(u), y(t, n, r)) : u();
+				};
+				e ? e(i) : i();
+			}
+		);
+	}),
+		(m.prototype.render = function (n) {
+			(this.o = null), (this.l = new Map());
+			var t = e.toChildArray(n.children);
+			n.revealOrder && "b" === n.revealOrder[0] && t.reverse();
+			for (var r = t.length; r--; ) this.l.set(t[r], (this.o = [1, 0, this.o]));
+			return n.children;
+		}),
+		(m.prototype.componentDidUpdate = m.prototype.componentDidMount = function () {
+			var n = this;
+			n.l.forEach(function (t, e) {
+				y(n, e, t);
+			});
+		});
+	var b = (function () {
+		function n() {}
+		var t = n.prototype;
+		return (
+			(t.getChildContext = function () {
+				return this.props.context;
+			}),
+			(t.render = function (n) {
+				return n.children;
+			}),
+			n
+		);
+	})();
+	function g(n) {
+		var t = this,
+			r = n.container,
+			u = e.createElement(b, { context: t.context }, n.vnode);
+		return (
+			t.s &&
+				t.s !== r &&
+				(t.h.parentNode && t.s.removeChild(t.h), e._e(t.v), (t.p = !1)),
+			n.vnode
+				? t.p
+					? ((r.__k = t.__k), e.render(u, r), (t.__k = r.__k))
+					: ((t.h = document.createTextNode("")),
+					  e.hydrate("", r),
+					  r.appendChild(t.h),
+					  (t.p = !0),
+					  (t.s = r),
+					  e.render(u, r, t.h),
+					  (t.__k = t.h.__k))
+				: t.p && (t.h.parentNode && t.s.removeChild(t.h), e._e(t.v)),
+			(t.v = u),
+			(t.componentWillUnmount = function () {
+				t.h.parentNode && t.s.removeChild(t.h), e._e(t.v);
+			}),
+			null
+		);
+	}
+	function w(n, t) {
+		return e.createElement(g, { vnode: n, container: t });
+	}
+	var x = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
+	e.Component.prototype.isReactComponent = {};
+	var E =
+		("undefined" != typeof Symbol &&
+			Symbol.for &&
+			Symbol.for("react.element")) ||
+		60103;
+	function _(n, t, r) {
+		if (null == t.__k) for (; t.firstChild; ) t.removeChild(t.firstChild);
+		return e.render(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	var A = e.options.event;
+	function C(n, t) {
+		n["UNSAFE_" + t] &&
+			!n[t] &&
+			Object.defineProperty(n, t, {
+				configurable: !1,
+				get: function () {
+					return this["UNSAFE_" + t];
+				},
+				set: function (n) {
+					this["UNSAFE_" + t] = n;
+				},
+			});
+	}
+	e.options.event = function (n) {
+		A && (n = A(n)), (n.persist = function () {});
+		var t = !1,
+			e = !1,
+			r = n.stopPropagation;
+		n.stopPropagation = function () {
+			r.call(n), (t = !0);
+		};
+		var u = n.preventDefault;
+		return (
+			(n.preventDefault = function () {
+				u.call(n), (e = !0);
+			}),
+			(n.isPropagationStopped = function () {
+				return t;
+			}),
+			(n.isDefaultPrevented = function () {
+				return e;
+			}),
+			(n.nativeEvent = n)
+		);
+	};
+	var S = {
+			configurable: !0,
+			get: function () {
+				return this.class;
+			},
+		},
+		k = e.options.vnode;
+	function N(n) {
+		return e.createElement.bind(null, n);
+	}
+	function R(n) {
+		return !!n && n.$$typeof === E;
+	}
+	function U(n) {
+		return R(n) ? e.cloneElement.apply(null, arguments) : n;
+	}
+	function F(n) {
+		return !!n.__k && (e.render(null, n), !0);
+	}
+	function j(n) {
+		return (n && (n.base || (1 === n.nodeType && n))) || null;
+	}
+	e.options.vnode = function (n) {
+		n.$$typeof = E;
+		var t = n.type,
+			r = n.props;
+		if (
+			(r.class != r.className &&
+				((S.enumerable = "className" in r),
+				null != r.className && (r.class = r.className),
+				Object.defineProperty(r, "className", S)),
+			"function" != typeof t)
+		) {
+			var u, i, o;
+			for (o in (r.defaultValue &&
+				void 0 !== r.value &&
+				(r.value || 0 === r.value || (r.value = r.defaultValue),
+				delete r.defaultValue),
+			Array.isArray(r.value) &&
+				r.multiple &&
+				"select" === t &&
+				(e.toChildArray(r.children).forEach(function (n) {
+					-1 != r.value.indexOf(n.props.value) && (n.props.selected = !0);
+				}),
+				delete r.value),
+			r))
+				if ((u = x.test(o))) break;
+			if (u)
+				for (o in ((i = n.props = {}), r))
+					i[x.test(o) ? o.replace(/([A-Z0-9])/, "-$1").toLowerCase() : o] =
+						r[o];
+		}
+		!(function (t) {
+			var e = n.type,
+				r = n.props;
+			if (r && "string" == typeof e) {
+				var u = {};
+				for (var i in r)
+					/^on(Ani|Tra|Tou)/.test(i) &&
+						((r[i.toLowerCase()] = r[i]), delete r[i]),
+						(u[i.toLowerCase()] = i);
+				if (
+					(u.ondoubleclick &&
+						((r.ondblclick = r[u.ondoubleclick]), delete r[u.ondoubleclick]),
+					u.onbeforeinput &&
+						((r.onbeforeinput = r[u.onbeforeinput]), delete r[u.onbeforeinput]),
+					u.onchange &&
+						("textarea" === e ||
+							("input" === e.toLowerCase() && !/^fil|che|ra/i.test(r.type))))
+				) {
+					var o = u.oninput || "oninput";
+					r[o] || ((r[o] = r[u.onchange]), delete r[u.onchange]);
+				}
+			}
+		})(),
+			"function" == typeof t &&
+				!t.m &&
+				t.prototype &&
+				(C(t.prototype, "componentWillMount"),
+				C(t.prototype, "componentWillReceiveProps"),
+				C(t.prototype, "componentWillUpdate"),
+				(t.m = !0)),
+			k && k(n);
+	};
+	var M = function (n, t) {
+			return n(t);
+		},
+		O = {
+			useState: t.useState,
+			useReducer: t.useReducer,
+			useEffect: t.useEffect,
+			useLayoutEffect: t.useLayoutEffect,
+			useRef: t.useRef,
+			useImperativeHandle: t.useImperativeHandle,
+			useMemo: t.useMemo,
+			useCallback: t.useCallback,
+			useContext: t.useContext,
+			useDebugValue: t.useDebugValue,
+			version: "16.8.0",
+			Children: a,
+			render: _,
+			hydrate: _,
+			unmountComponentAtNode: F,
+			createPortal: w,
+			createElement: e.createElement,
+			createContext: e.createContext,
+			createFactory: N,
+			cloneElement: U,
+			createRef: e.createRef,
+			Fragment: e.Fragment,
+			isValidElement: R,
+			findDOMNode: j,
+			Component: e.Component,
+			PureComponent: i,
+			memo: o,
+			forwardRef: c,
+			unstable_batchedUpdates: M,
+			Suspense: v,
+			SuspenseList: m,
+			lazy: p,
+		};
+	Object.keys(t).forEach(function (e) {
+		n[e] = t[e];
+	}),
+		(n.createElement = e.createElement),
+		(n.createContext = e.createContext),
+		(n.createRef = e.createRef),
+		(n.Fragment = e.Fragment),
+		(n.Component = e.Component),
+		(n.version = "16.8.0"),
+		(n.Children = a),
+		(n.render = _),
+		(n.hydrate = function (n, t, r) {
+			return e.hydrate(n, t), "function" == typeof r && r(), n ? n.__c : null;
+		}),
+		(n.unmountComponentAtNode = F),
+		(n.createPortal = w),
+		(n.createFactory = N),
+		(n.cloneElement = U),
+		(n.isValidElement = R),
+		(n.findDOMNode = j),
+		(n.PureComponent = i),
+		(n.memo = o),
+		(n.forwardRef = c),
+		(n.unstable_batchedUpdates = M),
+		(n.Suspense = v),
+		(n.SuspenseList = m),
+		(n.lazy = p),
+		(n.default = O);
+});
+//# sourceMappingURL=compat.umd.js.map
diff --git a/test-e2e/vendor/preact/10.4.1/preactCompat.js b/test-e2e/vendor/preact/10.4.1/preactCompat.js
new file mode 100644
index 0000000..f1f4d30
--- /dev/null
+++ b/test-e2e/vendor/preact/10.4.1/preactCompat.js
@@ -0,0 +1,446 @@
+!(function (n, t) {
+	"object" == typeof exports && "undefined" != typeof module
+		? t(exports, require("preact/hooks"), require("preact"))
+		: "function" == typeof define && define.amd
+		? define(["exports", "preact/hooks", "preact"], t)
+		: t((n.preactCompat = {}), n.preactHooks, n.preact);
+})(this, function (n, t, e) {
+	function r(n, t) {
+		for (var e in t) n[e] = t[e];
+		return n;
+	}
+	function u(n, t) {
+		for (var e in n) if ("__source" !== e && !(e in t)) return !0;
+		for (var r in t) if ("__source" !== r && n[r] !== t[r]) return !0;
+		return !1;
+	}
+	var i = (function (n) {
+		var t, e;
+		function r(t) {
+			var e;
+			return ((e = n.call(this, t) || this).isPureReactComponent = !0), e;
+		}
+		return (
+			(e = n),
+			((t = r).prototype = Object.create(e.prototype)),
+			(t.prototype.constructor = t),
+			(t.__proto__ = e),
+			(r.prototype.shouldComponentUpdate = function (n, t) {
+				return u(this.props, n) || u(this.state, t);
+			}),
+			r
+		);
+	})(e.Component);
+	function o(n, t) {
+		function i(n) {
+			var e = this.props.ref,
+				r = e == n.ref;
+			return (
+				!r && e && (e.call ? e(null) : (e.current = null)),
+				t ? !t(this.props, n) || !r : u(this.props, n)
+			);
+		}
+		function o(t) {
+			return (this.shouldComponentUpdate = i), e.createElement(n, r({}, t));
+		}
+		return (
+			(o.prototype.isReactComponent = !0),
+			(o.displayName = "Memo(" + (n.displayName || n.name) + ")"),
+			(o.t = !0),
+			o
+		);
+	}
+	var f = e.options.__b;
+	function c(n) {
+		function t(t) {
+			var e = r({}, t);
+			return delete e.ref, n(e, t.ref);
+		}
+		return (
+			(t.prototype.isReactComponent = t.t = !0),
+			(t.displayName = "ForwardRef(" + (n.displayName || n.name) + ")"),
+			t
+		);
+	}
+	e.options.__b = function (n) {
+		n.type && n.type.t && n.ref && ((n.props.ref = n.ref), (n.ref = null)),
+			f && f(n);
+	};
+	var l = function (n, t) {
+			return n
+				? e.toChildArray(n).reduce(function (n, e, r) {
+						return n.concat(t(e, r));
+				  }, [])
+				: null;
+		},
+		a = {
+			map: l,
+			forEach: l,
+			count: function (n) {
+				return n ? e.toChildArray(n).length : 0;
+			},
+			only: function (n) {
+				if (1 !== (n = e.toChildArray(n)).length)
+					throw new Error("Children.only() expects only one child.");
+				return n[0];
+			},
+			toArray: e.toChildArray,
+		},
+		s = e.options.__e;
+	function h(n) {
+		return (
+			n && (((n = r({}, n)).__c = null), (n.__k = n.__k && n.__k.map(h))), n
+		);
+	}
+	function v() {
+		(this.__u = 0), (this.u = null), (this.__b = null);
+	}
+	function d(n) {
+		var t = n.__.__c;
+		return t && t.i && t.i(n);
+	}
+	function p(n) {
+		var t, r, u;
+		function i(i) {
+			if (
+				(t ||
+					(t = n()).then(
+						function (n) {
+							r = n.default || n;
+						},
+						function (n) {
+							u = n;
+						},
+					),
+				u)
+			)
+				throw u;
+			if (!r) throw t;
+			return e.createElement(r, i);
+		}
+		return (i.displayName = "Lazy"), (i.t = !0), i;
+	}
+	function m() {
+		(this.o = null), (this.l = null);
+	}
+	(e.options.__e = function (n, t, e) {
+		if (n.then)
+			for (var r, u = t; (u = u.__); )
+				if ((r = u.__c) && r.__c) return r.__c(n, t.__c);
+		s(n, t, e);
+	}),
+		((v.prototype = new e.Component()).__c = function (n, t) {
+			var e = this;
+			null == e.u && (e.u = []), e.u.push(t);
+			var r = d(e.__v),
+				u = !1,
+				i = function () {
+					u || ((u = !0), r ? r(o) : o());
+				};
+			(t.__c = t.componentWillUnmount),
+				(t.componentWillUnmount = function () {
+					i(), t.__c && t.__c();
+				});
+			var o = function () {
+				var n;
+				if (!--e.__u)
+					for (
+						e.__v.__k[0] = e.state.i, e.setState({ i: (e.__b = null) });
+						(n = e.u.pop());
+
+					)
+						n.forceUpdate();
+			};
+			e.__u++ || e.setState({ i: (e.__b = e.__v.__k[0]) }), n.then(i, i);
+		}),
+		(v.prototype.render = function (n, t) {
+			return (
+				this.__b && ((this.__v.__k[0] = h(this.__b)), (this.__b = null)),
+				[
+					e.createElement(e.Component, null, t.i ? null : n.children),
+					t.i && n.fallback,
+				]
+			);
+		});
+	var y = function (n, t, e) {
+		if (
+			(++e[1] === e[0] && n.l.delete(t),
+			n.props.revealOrder && ("t" !== n.props.revealOrder[0] || !n.l.size))
+		)
+			for (e = n.o; e; ) {
+				for (; e.length > 3; ) e.pop()();
+				if (e[1] < e[0]) break;
+				n.o = e = e[2];
+			}
+	};
+	((m.prototype = new e.Component()).i = function (n) {
+		var t = this,
+			e = d(t.__v),
+			r = t.l.get(n);
+		return (
+			r[0]++,
+			function (u) {
+				var i = function () {
+					t.props.revealOrder ? (r.push(u), y(t, n, r)) : u();
+				};
+				e ? e(i) : i();
+			}
+		);
+	}),
+		(m.prototype.render = function (n) {
+			(this.o = null), (this.l = new Map());
+			var t = e.toChildArray(n.children);
+			n.revealOrder && "b" === n.revealOrder[0] && t.reverse();
+			for (var r = t.length; r--; ) this.l.set(t[r], (this.o = [1, 0, this.o]));
+			return n.children;
+		}),
+		(m.prototype.componentDidUpdate = m.prototype.componentDidMount = function () {
+			var n = this;
+			n.l.forEach(function (t, e) {
+				y(n, e, t);
+			});
+		});
+	var b = (function () {
+		function n() {}
+		var t = n.prototype;
+		return (
+			(t.getChildContext = function () {
+				return this.props.context;
+			}),
+			(t.render = function (n) {
+				return n.children;
+			}),
+			n
+		);
+	})();
+	function g(n) {
+		var t = this,
+			r = n.container,
+			u = e.createElement(b, { context: t.context }, n.vnode);
+		return (
+			t.s &&
+				t.s !== r &&
+				(t.h.parentNode && t.s.removeChild(t.h), e._e(t.v), (t.p = !1)),
+			n.vnode
+				? t.p
+					? ((r.__k = t.__k), e.render(u, r), (t.__k = r.__k))
+					: ((t.h = document.createTextNode("")),
+					  e.hydrate("", r),
+					  r.appendChild(t.h),
+					  (t.p = !0),
+					  (t.s = r),
+					  e.render(u, r, t.h),
+					  (t.__k = t.h.__k))
+				: t.p && (t.h.parentNode && t.s.removeChild(t.h), e._e(t.v)),
+			(t.v = u),
+			(t.componentWillUnmount = function () {
+				t.h.parentNode && t.s.removeChild(t.h), e._e(t.v);
+			}),
+			null
+		);
+	}
+	function w(n, t) {
+		return e.createElement(g, { vnode: n, container: t });
+	}
+	var x = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
+	e.Component.prototype.isReactComponent = {};
+	var E =
+		("undefined" != typeof Symbol &&
+			Symbol.for &&
+			Symbol.for("react.element")) ||
+		60103;
+	function _(n, t, r) {
+		if (null == t.__k) for (; t.firstChild; ) t.removeChild(t.firstChild);
+		return e.render(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	function A(n, t, r) {
+		return e.hydrate(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	var C = e.options.event;
+	function S(n, t) {
+		n["UNSAFE_" + t] &&
+			!n[t] &&
+			Object.defineProperty(n, t, {
+				configurable: !1,
+				get: function () {
+					return this["UNSAFE_" + t];
+				},
+				set: function (n) {
+					this["UNSAFE_" + t] = n;
+				},
+			});
+	}
+	e.options.event = function (n) {
+		C && (n = C(n)), (n.persist = function () {});
+		var t = !1,
+			e = !1,
+			r = n.stopPropagation;
+		n.stopPropagation = function () {
+			r.call(n), (t = !0);
+		};
+		var u = n.preventDefault;
+		return (
+			(n.preventDefault = function () {
+				u.call(n), (e = !0);
+			}),
+			(n.isPropagationStopped = function () {
+				return t;
+			}),
+			(n.isDefaultPrevented = function () {
+				return e;
+			}),
+			(n.nativeEvent = n)
+		);
+	};
+	var k = {
+			configurable: !0,
+			get: function () {
+				return this.class;
+			},
+		},
+		N = e.options.vnode;
+	function R(n) {
+		return e.createElement.bind(null, n);
+	}
+	function U(n) {
+		return !!n && n.$$typeof === E;
+	}
+	function F(n) {
+		return U(n) ? e.cloneElement.apply(null, arguments) : n;
+	}
+	function j(n) {
+		return !!n.__k && (e.render(null, n), !0);
+	}
+	function M(n) {
+		return (n && (n.base || (1 === n.nodeType && n))) || null;
+	}
+	e.options.vnode = function (n) {
+		n.$$typeof = E;
+		var t = n.type,
+			r = n.props;
+		if (t) {
+			if (
+				(r.class != r.className &&
+					((k.enumerable = "className" in r),
+					null != r.className && (r.class = r.className),
+					Object.defineProperty(r, "className", k)),
+				"function" != typeof t)
+			) {
+				var u, i, o;
+				for (o in (r.defaultValue &&
+					void 0 !== r.value &&
+					(r.value || 0 === r.value || (r.value = r.defaultValue),
+					delete r.defaultValue),
+				Array.isArray(r.value) &&
+					r.multiple &&
+					"select" === t &&
+					(e.toChildArray(r.children).forEach(function (n) {
+						-1 != r.value.indexOf(n.props.value) && (n.props.selected = !0);
+					}),
+					delete r.value),
+				r))
+					if ((u = x.test(o))) break;
+				if (u)
+					for (o in ((i = n.props = {}), r))
+						i[x.test(o) ? o.replace(/[A-Z0-9]/, "-$&").toLowerCase() : o] =
+							r[o];
+			}
+			!(function (t) {
+				var e = n.type,
+					r = n.props;
+				if (r && "string" == typeof e) {
+					var u = {};
+					for (var i in r)
+						/^on(Ani|Tra|Tou)/.test(i) &&
+							((r[i.toLowerCase()] = r[i]), delete r[i]),
+							(u[i.toLowerCase()] = i);
+					if (
+						(u.ondoubleclick &&
+							((r.ondblclick = r[u.ondoubleclick]), delete r[u.ondoubleclick]),
+						u.onbeforeinput &&
+							((r.onbeforeinput = r[u.onbeforeinput]),
+							delete r[u.onbeforeinput]),
+						u.onchange &&
+							("textarea" === e ||
+								("input" === e.toLowerCase() && !/^fil|che|ra/i.test(r.type))))
+					) {
+						var o = u.oninput || "oninput";
+						r[o] || ((r[o] = r[u.onchange]), delete r[u.onchange]);
+					}
+				}
+			})(),
+				"function" == typeof t &&
+					!t.m &&
+					t.prototype &&
+					(S(t.prototype, "componentWillMount"),
+					S(t.prototype, "componentWillReceiveProps"),
+					S(t.prototype, "componentWillUpdate"),
+					(t.m = !0));
+		}
+		N && N(n);
+	};
+	var O = function (n, t) {
+			return n(t);
+		},
+		L = {
+			useState: t.useState,
+			useReducer: t.useReducer,
+			useEffect: t.useEffect,
+			useLayoutEffect: t.useLayoutEffect,
+			useRef: t.useRef,
+			useImperativeHandle: t.useImperativeHandle,
+			useMemo: t.useMemo,
+			useCallback: t.useCallback,
+			useContext: t.useContext,
+			useDebugValue: t.useDebugValue,
+			version: "16.8.0",
+			Children: a,
+			render: _,
+			hydrate: A,
+			unmountComponentAtNode: j,
+			createPortal: w,
+			createElement: e.createElement,
+			createContext: e.createContext,
+			createFactory: R,
+			cloneElement: F,
+			createRef: e.createRef,
+			Fragment: e.Fragment,
+			isValidElement: U,
+			findDOMNode: M,
+			Component: e.Component,
+			PureComponent: i,
+			memo: o,
+			forwardRef: c,
+			unstable_batchedUpdates: O,
+			Suspense: v,
+			SuspenseList: m,
+			lazy: p,
+		};
+	Object.keys(t).forEach(function (e) {
+		n[e] = t[e];
+	}),
+		(n.createElement = e.createElement),
+		(n.createContext = e.createContext),
+		(n.createRef = e.createRef),
+		(n.Fragment = e.Fragment),
+		(n.Component = e.Component),
+		(n.version = "16.8.0"),
+		(n.Children = a),
+		(n.render = _),
+		(n.hydrate = A),
+		(n.unmountComponentAtNode = j),
+		(n.createPortal = w),
+		(n.createFactory = R),
+		(n.cloneElement = F),
+		(n.isValidElement = U),
+		(n.findDOMNode = M),
+		(n.PureComponent = i),
+		(n.memo = o),
+		(n.forwardRef = c),
+		(n.unstable_batchedUpdates = O),
+		(n.Suspense = v),
+		(n.SuspenseList = m),
+		(n.lazy = p),
+		(n.default = L);
+});
+//# sourceMappingURL=compat.umd.js.map
diff --git a/test-e2e/vendor/preact/hook/preactCompat.js b/test-e2e/vendor/preact/hook/preactCompat.js
new file mode 100644
index 0000000..f1f4d30
--- /dev/null
+++ b/test-e2e/vendor/preact/hook/preactCompat.js
@@ -0,0 +1,446 @@
+!(function (n, t) {
+	"object" == typeof exports && "undefined" != typeof module
+		? t(exports, require("preact/hooks"), require("preact"))
+		: "function" == typeof define && define.amd
+		? define(["exports", "preact/hooks", "preact"], t)
+		: t((n.preactCompat = {}), n.preactHooks, n.preact);
+})(this, function (n, t, e) {
+	function r(n, t) {
+		for (var e in t) n[e] = t[e];
+		return n;
+	}
+	function u(n, t) {
+		for (var e in n) if ("__source" !== e && !(e in t)) return !0;
+		for (var r in t) if ("__source" !== r && n[r] !== t[r]) return !0;
+		return !1;
+	}
+	var i = (function (n) {
+		var t, e;
+		function r(t) {
+			var e;
+			return ((e = n.call(this, t) || this).isPureReactComponent = !0), e;
+		}
+		return (
+			(e = n),
+			((t = r).prototype = Object.create(e.prototype)),
+			(t.prototype.constructor = t),
+			(t.__proto__ = e),
+			(r.prototype.shouldComponentUpdate = function (n, t) {
+				return u(this.props, n) || u(this.state, t);
+			}),
+			r
+		);
+	})(e.Component);
+	function o(n, t) {
+		function i(n) {
+			var e = this.props.ref,
+				r = e == n.ref;
+			return (
+				!r && e && (e.call ? e(null) : (e.current = null)),
+				t ? !t(this.props, n) || !r : u(this.props, n)
+			);
+		}
+		function o(t) {
+			return (this.shouldComponentUpdate = i), e.createElement(n, r({}, t));
+		}
+		return (
+			(o.prototype.isReactComponent = !0),
+			(o.displayName = "Memo(" + (n.displayName || n.name) + ")"),
+			(o.t = !0),
+			o
+		);
+	}
+	var f = e.options.__b;
+	function c(n) {
+		function t(t) {
+			var e = r({}, t);
+			return delete e.ref, n(e, t.ref);
+		}
+		return (
+			(t.prototype.isReactComponent = t.t = !0),
+			(t.displayName = "ForwardRef(" + (n.displayName || n.name) + ")"),
+			t
+		);
+	}
+	e.options.__b = function (n) {
+		n.type && n.type.t && n.ref && ((n.props.ref = n.ref), (n.ref = null)),
+			f && f(n);
+	};
+	var l = function (n, t) {
+			return n
+				? e.toChildArray(n).reduce(function (n, e, r) {
+						return n.concat(t(e, r));
+				  }, [])
+				: null;
+		},
+		a = {
+			map: l,
+			forEach: l,
+			count: function (n) {
+				return n ? e.toChildArray(n).length : 0;
+			},
+			only: function (n) {
+				if (1 !== (n = e.toChildArray(n)).length)
+					throw new Error("Children.only() expects only one child.");
+				return n[0];
+			},
+			toArray: e.toChildArray,
+		},
+		s = e.options.__e;
+	function h(n) {
+		return (
+			n && (((n = r({}, n)).__c = null), (n.__k = n.__k && n.__k.map(h))), n
+		);
+	}
+	function v() {
+		(this.__u = 0), (this.u = null), (this.__b = null);
+	}
+	function d(n) {
+		var t = n.__.__c;
+		return t && t.i && t.i(n);
+	}
+	function p(n) {
+		var t, r, u;
+		function i(i) {
+			if (
+				(t ||
+					(t = n()).then(
+						function (n) {
+							r = n.default || n;
+						},
+						function (n) {
+							u = n;
+						},
+					),
+				u)
+			)
+				throw u;
+			if (!r) throw t;
+			return e.createElement(r, i);
+		}
+		return (i.displayName = "Lazy"), (i.t = !0), i;
+	}
+	function m() {
+		(this.o = null), (this.l = null);
+	}
+	(e.options.__e = function (n, t, e) {
+		if (n.then)
+			for (var r, u = t; (u = u.__); )
+				if ((r = u.__c) && r.__c) return r.__c(n, t.__c);
+		s(n, t, e);
+	}),
+		((v.prototype = new e.Component()).__c = function (n, t) {
+			var e = this;
+			null == e.u && (e.u = []), e.u.push(t);
+			var r = d(e.__v),
+				u = !1,
+				i = function () {
+					u || ((u = !0), r ? r(o) : o());
+				};
+			(t.__c = t.componentWillUnmount),
+				(t.componentWillUnmount = function () {
+					i(), t.__c && t.__c();
+				});
+			var o = function () {
+				var n;
+				if (!--e.__u)
+					for (
+						e.__v.__k[0] = e.state.i, e.setState({ i: (e.__b = null) });
+						(n = e.u.pop());
+
+					)
+						n.forceUpdate();
+			};
+			e.__u++ || e.setState({ i: (e.__b = e.__v.__k[0]) }), n.then(i, i);
+		}),
+		(v.prototype.render = function (n, t) {
+			return (
+				this.__b && ((this.__v.__k[0] = h(this.__b)), (this.__b = null)),
+				[
+					e.createElement(e.Component, null, t.i ? null : n.children),
+					t.i && n.fallback,
+				]
+			);
+		});
+	var y = function (n, t, e) {
+		if (
+			(++e[1] === e[0] && n.l.delete(t),
+			n.props.revealOrder && ("t" !== n.props.revealOrder[0] || !n.l.size))
+		)
+			for (e = n.o; e; ) {
+				for (; e.length > 3; ) e.pop()();
+				if (e[1] < e[0]) break;
+				n.o = e = e[2];
+			}
+	};
+	((m.prototype = new e.Component()).i = function (n) {
+		var t = this,
+			e = d(t.__v),
+			r = t.l.get(n);
+		return (
+			r[0]++,
+			function (u) {
+				var i = function () {
+					t.props.revealOrder ? (r.push(u), y(t, n, r)) : u();
+				};
+				e ? e(i) : i();
+			}
+		);
+	}),
+		(m.prototype.render = function (n) {
+			(this.o = null), (this.l = new Map());
+			var t = e.toChildArray(n.children);
+			n.revealOrder && "b" === n.revealOrder[0] && t.reverse();
+			for (var r = t.length; r--; ) this.l.set(t[r], (this.o = [1, 0, this.o]));
+			return n.children;
+		}),
+		(m.prototype.componentDidUpdate = m.prototype.componentDidMount = function () {
+			var n = this;
+			n.l.forEach(function (t, e) {
+				y(n, e, t);
+			});
+		});
+	var b = (function () {
+		function n() {}
+		var t = n.prototype;
+		return (
+			(t.getChildContext = function () {
+				return this.props.context;
+			}),
+			(t.render = function (n) {
+				return n.children;
+			}),
+			n
+		);
+	})();
+	function g(n) {
+		var t = this,
+			r = n.container,
+			u = e.createElement(b, { context: t.context }, n.vnode);
+		return (
+			t.s &&
+				t.s !== r &&
+				(t.h.parentNode && t.s.removeChild(t.h), e._e(t.v), (t.p = !1)),
+			n.vnode
+				? t.p
+					? ((r.__k = t.__k), e.render(u, r), (t.__k = r.__k))
+					: ((t.h = document.createTextNode("")),
+					  e.hydrate("", r),
+					  r.appendChild(t.h),
+					  (t.p = !0),
+					  (t.s = r),
+					  e.render(u, r, t.h),
+					  (t.__k = t.h.__k))
+				: t.p && (t.h.parentNode && t.s.removeChild(t.h), e._e(t.v)),
+			(t.v = u),
+			(t.componentWillUnmount = function () {
+				t.h.parentNode && t.s.removeChild(t.h), e._e(t.v);
+			}),
+			null
+		);
+	}
+	function w(n, t) {
+		return e.createElement(g, { vnode: n, container: t });
+	}
+	var x = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
+	e.Component.prototype.isReactComponent = {};
+	var E =
+		("undefined" != typeof Symbol &&
+			Symbol.for &&
+			Symbol.for("react.element")) ||
+		60103;
+	function _(n, t, r) {
+		if (null == t.__k) for (; t.firstChild; ) t.removeChild(t.firstChild);
+		return e.render(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	function A(n, t, r) {
+		return e.hydrate(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	var C = e.options.event;
+	function S(n, t) {
+		n["UNSAFE_" + t] &&
+			!n[t] &&
+			Object.defineProperty(n, t, {
+				configurable: !1,
+				get: function () {
+					return this["UNSAFE_" + t];
+				},
+				set: function (n) {
+					this["UNSAFE_" + t] = n;
+				},
+			});
+	}
+	e.options.event = function (n) {
+		C && (n = C(n)), (n.persist = function () {});
+		var t = !1,
+			e = !1,
+			r = n.stopPropagation;
+		n.stopPropagation = function () {
+			r.call(n), (t = !0);
+		};
+		var u = n.preventDefault;
+		return (
+			(n.preventDefault = function () {
+				u.call(n), (e = !0);
+			}),
+			(n.isPropagationStopped = function () {
+				return t;
+			}),
+			(n.isDefaultPrevented = function () {
+				return e;
+			}),
+			(n.nativeEvent = n)
+		);
+	};
+	var k = {
+			configurable: !0,
+			get: function () {
+				return this.class;
+			},
+		},
+		N = e.options.vnode;
+	function R(n) {
+		return e.createElement.bind(null, n);
+	}
+	function U(n) {
+		return !!n && n.$$typeof === E;
+	}
+	function F(n) {
+		return U(n) ? e.cloneElement.apply(null, arguments) : n;
+	}
+	function j(n) {
+		return !!n.__k && (e.render(null, n), !0);
+	}
+	function M(n) {
+		return (n && (n.base || (1 === n.nodeType && n))) || null;
+	}
+	e.options.vnode = function (n) {
+		n.$$typeof = E;
+		var t = n.type,
+			r = n.props;
+		if (t) {
+			if (
+				(r.class != r.className &&
+					((k.enumerable = "className" in r),
+					null != r.className && (r.class = r.className),
+					Object.defineProperty(r, "className", k)),
+				"function" != typeof t)
+			) {
+				var u, i, o;
+				for (o in (r.defaultValue &&
+					void 0 !== r.value &&
+					(r.value || 0 === r.value || (r.value = r.defaultValue),
+					delete r.defaultValue),
+				Array.isArray(r.value) &&
+					r.multiple &&
+					"select" === t &&
+					(e.toChildArray(r.children).forEach(function (n) {
+						-1 != r.value.indexOf(n.props.value) && (n.props.selected = !0);
+					}),
+					delete r.value),
+				r))
+					if ((u = x.test(o))) break;
+				if (u)
+					for (o in ((i = n.props = {}), r))
+						i[x.test(o) ? o.replace(/[A-Z0-9]/, "-$&").toLowerCase() : o] =
+							r[o];
+			}
+			!(function (t) {
+				var e = n.type,
+					r = n.props;
+				if (r && "string" == typeof e) {
+					var u = {};
+					for (var i in r)
+						/^on(Ani|Tra|Tou)/.test(i) &&
+							((r[i.toLowerCase()] = r[i]), delete r[i]),
+							(u[i.toLowerCase()] = i);
+					if (
+						(u.ondoubleclick &&
+							((r.ondblclick = r[u.ondoubleclick]), delete r[u.ondoubleclick]),
+						u.onbeforeinput &&
+							((r.onbeforeinput = r[u.onbeforeinput]),
+							delete r[u.onbeforeinput]),
+						u.onchange &&
+							("textarea" === e ||
+								("input" === e.toLowerCase() && !/^fil|che|ra/i.test(r.type))))
+					) {
+						var o = u.oninput || "oninput";
+						r[o] || ((r[o] = r[u.onchange]), delete r[u.onchange]);
+					}
+				}
+			})(),
+				"function" == typeof t &&
+					!t.m &&
+					t.prototype &&
+					(S(t.prototype, "componentWillMount"),
+					S(t.prototype, "componentWillReceiveProps"),
+					S(t.prototype, "componentWillUpdate"),
+					(t.m = !0));
+		}
+		N && N(n);
+	};
+	var O = function (n, t) {
+			return n(t);
+		},
+		L = {
+			useState: t.useState,
+			useReducer: t.useReducer,
+			useEffect: t.useEffect,
+			useLayoutEffect: t.useLayoutEffect,
+			useRef: t.useRef,
+			useImperativeHandle: t.useImperativeHandle,
+			useMemo: t.useMemo,
+			useCallback: t.useCallback,
+			useContext: t.useContext,
+			useDebugValue: t.useDebugValue,
+			version: "16.8.0",
+			Children: a,
+			render: _,
+			hydrate: A,
+			unmountComponentAtNode: j,
+			createPortal: w,
+			createElement: e.createElement,
+			createContext: e.createContext,
+			createFactory: R,
+			cloneElement: F,
+			createRef: e.createRef,
+			Fragment: e.Fragment,
+			isValidElement: U,
+			findDOMNode: M,
+			Component: e.Component,
+			PureComponent: i,
+			memo: o,
+			forwardRef: c,
+			unstable_batchedUpdates: O,
+			Suspense: v,
+			SuspenseList: m,
+			lazy: p,
+		};
+	Object.keys(t).forEach(function (e) {
+		n[e] = t[e];
+	}),
+		(n.createElement = e.createElement),
+		(n.createContext = e.createContext),
+		(n.createRef = e.createRef),
+		(n.Fragment = e.Fragment),
+		(n.Component = e.Component),
+		(n.version = "16.8.0"),
+		(n.Children = a),
+		(n.render = _),
+		(n.hydrate = A),
+		(n.unmountComponentAtNode = j),
+		(n.createPortal = w),
+		(n.createFactory = R),
+		(n.cloneElement = F),
+		(n.isValidElement = U),
+		(n.findDOMNode = M),
+		(n.PureComponent = i),
+		(n.memo = o),
+		(n.forwardRef = c),
+		(n.unstable_batchedUpdates = O),
+		(n.Suspense = v),
+		(n.SuspenseList = m),
+		(n.lazy = p),
+		(n.default = L);
+});
+//# sourceMappingURL=compat.umd.js.map
diff --git a/test-e2e/vendor/preact/next/preactCompat.js b/test-e2e/vendor/preact/next/preactCompat.js
new file mode 100644
index 0000000..f1f4d30
--- /dev/null
+++ b/test-e2e/vendor/preact/next/preactCompat.js
@@ -0,0 +1,446 @@
+!(function (n, t) {
+	"object" == typeof exports && "undefined" != typeof module
+		? t(exports, require("preact/hooks"), require("preact"))
+		: "function" == typeof define && define.amd
+		? define(["exports", "preact/hooks", "preact"], t)
+		: t((n.preactCompat = {}), n.preactHooks, n.preact);
+})(this, function (n, t, e) {
+	function r(n, t) {
+		for (var e in t) n[e] = t[e];
+		return n;
+	}
+	function u(n, t) {
+		for (var e in n) if ("__source" !== e && !(e in t)) return !0;
+		for (var r in t) if ("__source" !== r && n[r] !== t[r]) return !0;
+		return !1;
+	}
+	var i = (function (n) {
+		var t, e;
+		function r(t) {
+			var e;
+			return ((e = n.call(this, t) || this).isPureReactComponent = !0), e;
+		}
+		return (
+			(e = n),
+			((t = r).prototype = Object.create(e.prototype)),
+			(t.prototype.constructor = t),
+			(t.__proto__ = e),
+			(r.prototype.shouldComponentUpdate = function (n, t) {
+				return u(this.props, n) || u(this.state, t);
+			}),
+			r
+		);
+	})(e.Component);
+	function o(n, t) {
+		function i(n) {
+			var e = this.props.ref,
+				r = e == n.ref;
+			return (
+				!r && e && (e.call ? e(null) : (e.current = null)),
+				t ? !t(this.props, n) || !r : u(this.props, n)
+			);
+		}
+		function o(t) {
+			return (this.shouldComponentUpdate = i), e.createElement(n, r({}, t));
+		}
+		return (
+			(o.prototype.isReactComponent = !0),
+			(o.displayName = "Memo(" + (n.displayName || n.name) + ")"),
+			(o.t = !0),
+			o
+		);
+	}
+	var f = e.options.__b;
+	function c(n) {
+		function t(t) {
+			var e = r({}, t);
+			return delete e.ref, n(e, t.ref);
+		}
+		return (
+			(t.prototype.isReactComponent = t.t = !0),
+			(t.displayName = "ForwardRef(" + (n.displayName || n.name) + ")"),
+			t
+		);
+	}
+	e.options.__b = function (n) {
+		n.type && n.type.t && n.ref && ((n.props.ref = n.ref), (n.ref = null)),
+			f && f(n);
+	};
+	var l = function (n, t) {
+			return n
+				? e.toChildArray(n).reduce(function (n, e, r) {
+						return n.concat(t(e, r));
+				  }, [])
+				: null;
+		},
+		a = {
+			map: l,
+			forEach: l,
+			count: function (n) {
+				return n ? e.toChildArray(n).length : 0;
+			},
+			only: function (n) {
+				if (1 !== (n = e.toChildArray(n)).length)
+					throw new Error("Children.only() expects only one child.");
+				return n[0];
+			},
+			toArray: e.toChildArray,
+		},
+		s = e.options.__e;
+	function h(n) {
+		return (
+			n && (((n = r({}, n)).__c = null), (n.__k = n.__k && n.__k.map(h))), n
+		);
+	}
+	function v() {
+		(this.__u = 0), (this.u = null), (this.__b = null);
+	}
+	function d(n) {
+		var t = n.__.__c;
+		return t && t.i && t.i(n);
+	}
+	function p(n) {
+		var t, r, u;
+		function i(i) {
+			if (
+				(t ||
+					(t = n()).then(
+						function (n) {
+							r = n.default || n;
+						},
+						function (n) {
+							u = n;
+						},
+					),
+				u)
+			)
+				throw u;
+			if (!r) throw t;
+			return e.createElement(r, i);
+		}
+		return (i.displayName = "Lazy"), (i.t = !0), i;
+	}
+	function m() {
+		(this.o = null), (this.l = null);
+	}
+	(e.options.__e = function (n, t, e) {
+		if (n.then)
+			for (var r, u = t; (u = u.__); )
+				if ((r = u.__c) && r.__c) return r.__c(n, t.__c);
+		s(n, t, e);
+	}),
+		((v.prototype = new e.Component()).__c = function (n, t) {
+			var e = this;
+			null == e.u && (e.u = []), e.u.push(t);
+			var r = d(e.__v),
+				u = !1,
+				i = function () {
+					u || ((u = !0), r ? r(o) : o());
+				};
+			(t.__c = t.componentWillUnmount),
+				(t.componentWillUnmount = function () {
+					i(), t.__c && t.__c();
+				});
+			var o = function () {
+				var n;
+				if (!--e.__u)
+					for (
+						e.__v.__k[0] = e.state.i, e.setState({ i: (e.__b = null) });
+						(n = e.u.pop());
+
+					)
+						n.forceUpdate();
+			};
+			e.__u++ || e.setState({ i: (e.__b = e.__v.__k[0]) }), n.then(i, i);
+		}),
+		(v.prototype.render = function (n, t) {
+			return (
+				this.__b && ((this.__v.__k[0] = h(this.__b)), (this.__b = null)),
+				[
+					e.createElement(e.Component, null, t.i ? null : n.children),
+					t.i && n.fallback,
+				]
+			);
+		});
+	var y = function (n, t, e) {
+		if (
+			(++e[1] === e[0] && n.l.delete(t),
+			n.props.revealOrder && ("t" !== n.props.revealOrder[0] || !n.l.size))
+		)
+			for (e = n.o; e; ) {
+				for (; e.length > 3; ) e.pop()();
+				if (e[1] < e[0]) break;
+				n.o = e = e[2];
+			}
+	};
+	((m.prototype = new e.Component()).i = function (n) {
+		var t = this,
+			e = d(t.__v),
+			r = t.l.get(n);
+		return (
+			r[0]++,
+			function (u) {
+				var i = function () {
+					t.props.revealOrder ? (r.push(u), y(t, n, r)) : u();
+				};
+				e ? e(i) : i();
+			}
+		);
+	}),
+		(m.prototype.render = function (n) {
+			(this.o = null), (this.l = new Map());
+			var t = e.toChildArray(n.children);
+			n.revealOrder && "b" === n.revealOrder[0] && t.reverse();
+			for (var r = t.length; r--; ) this.l.set(t[r], (this.o = [1, 0, this.o]));
+			return n.children;
+		}),
+		(m.prototype.componentDidUpdate = m.prototype.componentDidMount = function () {
+			var n = this;
+			n.l.forEach(function (t, e) {
+				y(n, e, t);
+			});
+		});
+	var b = (function () {
+		function n() {}
+		var t = n.prototype;
+		return (
+			(t.getChildContext = function () {
+				return this.props.context;
+			}),
+			(t.render = function (n) {
+				return n.children;
+			}),
+			n
+		);
+	})();
+	function g(n) {
+		var t = this,
+			r = n.container,
+			u = e.createElement(b, { context: t.context }, n.vnode);
+		return (
+			t.s &&
+				t.s !== r &&
+				(t.h.parentNode && t.s.removeChild(t.h), e._e(t.v), (t.p = !1)),
+			n.vnode
+				? t.p
+					? ((r.__k = t.__k), e.render(u, r), (t.__k = r.__k))
+					: ((t.h = document.createTextNode("")),
+					  e.hydrate("", r),
+					  r.appendChild(t.h),
+					  (t.p = !0),
+					  (t.s = r),
+					  e.render(u, r, t.h),
+					  (t.__k = t.h.__k))
+				: t.p && (t.h.parentNode && t.s.removeChild(t.h), e._e(t.v)),
+			(t.v = u),
+			(t.componentWillUnmount = function () {
+				t.h.parentNode && t.s.removeChild(t.h), e._e(t.v);
+			}),
+			null
+		);
+	}
+	function w(n, t) {
+		return e.createElement(g, { vnode: n, container: t });
+	}
+	var x = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
+	e.Component.prototype.isReactComponent = {};
+	var E =
+		("undefined" != typeof Symbol &&
+			Symbol.for &&
+			Symbol.for("react.element")) ||
+		60103;
+	function _(n, t, r) {
+		if (null == t.__k) for (; t.firstChild; ) t.removeChild(t.firstChild);
+		return e.render(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	function A(n, t, r) {
+		return e.hydrate(n, t), "function" == typeof r && r(), n ? n.__c : null;
+	}
+	var C = e.options.event;
+	function S(n, t) {
+		n["UNSAFE_" + t] &&
+			!n[t] &&
+			Object.defineProperty(n, t, {
+				configurable: !1,
+				get: function () {
+					return this["UNSAFE_" + t];
+				},
+				set: function (n) {
+					this["UNSAFE_" + t] = n;
+				},
+			});
+	}
+	e.options.event = function (n) {
+		C && (n = C(n)), (n.persist = function () {});
+		var t = !1,
+			e = !1,
+			r = n.stopPropagation;
+		n.stopPropagation = function () {
+			r.call(n), (t = !0);
+		};
+		var u = n.preventDefault;
+		return (
+			(n.preventDefault = function () {
+				u.call(n), (e = !0);
+			}),
+			(n.isPropagationStopped = function () {
+				return t;
+			}),
+			(n.isDefaultPrevented = function () {
+				return e;
+			}),
+			(n.nativeEvent = n)
+		);
+	};
+	var k = {
+			configurable: !0,
+			get: function () {
+				return this.class;
+			},
+		},
+		N = e.options.vnode;
+	function R(n) {
+		return e.createElement.bind(null, n);
+	}
+	function U(n) {
+		return !!n && n.$$typeof === E;
+	}
+	function F(n) {
+		return U(n) ? e.cloneElement.apply(null, arguments) : n;
+	}
+	function j(n) {
+		return !!n.__k && (e.render(null, n), !0);
+	}
+	function M(n) {
+		return (n && (n.base || (1 === n.nodeType && n))) || null;
+	}
+	e.options.vnode = function (n) {
+		n.$$typeof = E;
+		var t = n.type,
+			r = n.props;
+		if (t) {
+			if (
+				(r.class != r.className &&
+					((k.enumerable = "className" in r),
+					null != r.className && (r.class = r.className),
+					Object.defineProperty(r, "className", k)),
+				"function" != typeof t)
+			) {
+				var u, i, o;
+				for (o in (r.defaultValue &&
+					void 0 !== r.value &&
+					(r.value || 0 === r.value || (r.value = r.defaultValue),
+					delete r.defaultValue),
+				Array.isArray(r.value) &&
+					r.multiple &&
+					"select" === t &&
+					(e.toChildArray(r.children).forEach(function (n) {
+						-1 != r.value.indexOf(n.props.value) && (n.props.selected = !0);
+					}),
+					delete r.value),
+				r))
+					if ((u = x.test(o))) break;
+				if (u)
+					for (o in ((i = n.props = {}), r))
+						i[x.test(o) ? o.replace(/[A-Z0-9]/, "-$&").toLowerCase() : o] =
+							r[o];
+			}
+			!(function (t) {
+				var e = n.type,
+					r = n.props;
+				if (r && "string" == typeof e) {
+					var u = {};
+					for (var i in r)
+						/^on(Ani|Tra|Tou)/.test(i) &&
+							((r[i.toLowerCase()] = r[i]), delete r[i]),
+							(u[i.toLowerCase()] = i);
+					if (
+						(u.ondoubleclick &&
+							((r.ondblclick = r[u.ondoubleclick]), delete r[u.ondoubleclick]),
+						u.onbeforeinput &&
+							((r.onbeforeinput = r[u.onbeforeinput]),
+							delete r[u.onbeforeinput]),
+						u.onchange &&
+							("textarea" === e ||
+								("input" === e.toLowerCase() && !/^fil|che|ra/i.test(r.type))))
+					) {
+						var o = u.oninput || "oninput";
+						r[o] || ((r[o] = r[u.onchange]), delete r[u.onchange]);
+					}
+				}
+			})(),
+				"function" == typeof t &&
+					!t.m &&
+					t.prototype &&
+					(S(t.prototype, "componentWillMount"),
+					S(t.prototype, "componentWillReceiveProps"),
+					S(t.prototype, "componentWillUpdate"),
+					(t.m = !0));
+		}
+		N && N(n);
+	};
+	var O = function (n, t) {
+			return n(t);
+		},
+		L = {
+			useState: t.useState,
+			useReducer: t.useReducer,
+			useEffect: t.useEffect,
+			useLayoutEffect: t.useLayoutEffect,
+			useRef: t.useRef,
+			useImperativeHandle: t.useImperativeHandle,
+			useMemo: t.useMemo,
+			useCallback: t.useCallback,
+			useContext: t.useContext,
+			useDebugValue: t.useDebugValue,
+			version: "16.8.0",
+			Children: a,
+			render: _,
+			hydrate: A,
+			unmountComponentAtNode: j,
+			createPortal: w,
+			createElement: e.createElement,
+			createContext: e.createContext,
+			createFactory: R,
+			cloneElement: F,
+			createRef: e.createRef,
+			Fragment: e.Fragment,
+			isValidElement: U,
+			findDOMNode: M,
+			Component: e.Component,
+			PureComponent: i,
+			memo: o,
+			forwardRef: c,
+			unstable_batchedUpdates: O,
+			Suspense: v,
+			SuspenseList: m,
+			lazy: p,
+		};
+	Object.keys(t).forEach(function (e) {
+		n[e] = t[e];
+	}),
+		(n.createElement = e.createElement),
+		(n.createContext = e.createContext),
+		(n.createRef = e.createRef),
+		(n.Fragment = e.Fragment),
+		(n.Component = e.Component),
+		(n.version = "16.8.0"),
+		(n.Children = a),
+		(n.render = _),
+		(n.hydrate = A),
+		(n.unmountComponentAtNode = j),
+		(n.createPortal = w),
+		(n.createFactory = R),
+		(n.cloneElement = F),
+		(n.isValidElement = U),
+		(n.findDOMNode = M),
+		(n.PureComponent = i),
+		(n.memo = o),
+		(n.forwardRef = c),
+		(n.unstable_batchedUpdates = O),
+		(n.Suspense = v),
+		(n.SuspenseList = m),
+		(n.lazy = p),
+		(n.default = L);
+});
+//# sourceMappingURL=compat.umd.js.map