Repository: preactjs/preact-render-to-string
Pull request #232 (2022-07-21 12:48:58)

More perf improvements

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 3e03ada750925bb2ebc1f41c81d0b0382fcdc675

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/preactjs/preact-render-to-string/new/perf-v2?filename=.changeset/cold-suns-ring.md&value=---%0A%22preact-render-to-string%22%3A%20patch%0A---%0A%0AMore%20perf%20improvements%0A)



Patch:
diff --git a/.changeset/famous-days-invite.md b/.changeset/famous-days-invite.md
new file mode 100644
index 0000000..a550a32
--- /dev/null
+++ b/.changeset/famous-days-invite.md
@@ -0,0 +1,5 @@
+---
+"preact-render-to-string": patch
+---
+
+Performance enhancements
diff --git a/.changeset/orange-poems-learn.md b/.changeset/orange-poems-learn.md
deleted file mode 100644
index 9290fb8..0000000
--- a/.changeset/orange-poems-learn.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"preact-render-to-string": patch
----
-
-Improve string encoding performance by ~50%
diff --git a/.changeset/silly-masks-jump.md b/.changeset/silly-masks-jump.md
deleted file mode 100644
index 9967d7d..0000000
--- a/.changeset/silly-masks-jump.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'preact-render-to-string': patch
----
-
-Split up hot paths and make separate path for opts.pretty
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd43d25..17daae3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # preact-render-to-string
 
+## 5.2.2
+
+### Patch Changes
+
+- [#235](https://github.com/preactjs/preact-render-to-string/pull/235) [`2f6d6e8`](https://github.com/preactjs/preact-render-to-string/commit/2f6d6e8dd0573eb075273c2c9a20d7df289dacc8) Thanks [@marvinhagemeister](https://github.com/marvinhagemeister)! - Remove duplicate type path in `package.json`. Only one of `types` or `typings` is needed.
+
+* [#228](https://github.com/preactjs/preact-render-to-string/pull/228) [`e4fe799`](https://github.com/preactjs/preact-render-to-string/commit/e4fe7992d717eb3cb8740d2d28696bf2ba6c3d1e) Thanks [@developit](https://github.com/developit)! - Improve string encoding performance by ~50%
+
+- [#229](https://github.com/preactjs/preact-render-to-string/pull/229) [`d83def7`](https://github.com/preactjs/preact-render-to-string/commit/d83def7c1765c4ad1665598905531f5157366abd) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Split up hot paths and make separate path for opts.pretty
+
 ## 5.2.1
 
 ### Patch Changes
diff --git a/benchmarks/index.js b/benchmarks/index.js
index dca5f9a..f90fb43 100644
--- a/benchmarks/index.js
+++ b/benchmarks/index.js
@@ -1,10 +1,12 @@
 import { h } from 'preact';
 import Suite from 'benchmarkjs-pretty';
 import renderToStringBaseline from './lib/render-to-string';
-import renderToString from '../src/index';
+// import renderToString from '../src/index';
+import renderToString from '../dist/index.mjs';
 import TextApp from './text';
 // import StackApp from './stack';
-import { App as IsomorphicSearchResults } from './isomorphic-ui-search-results';
+import { App as IsomorphicSearchResults } from './isomorphic-ui/search-results/index';
+import { App as ColorPicker } from './isomorphic-ui/color-picker';
 
 function suite(name, Root) {
 	return new Suite(name)
@@ -16,6 +18,7 @@ function suite(name, Root) {
 (async () => {
 	await suite('Text', TextApp);
 	await suite('SearchResults', IsomorphicSearchResults);
+	await suite('ColorPicker', ColorPicker);
 	// TODO: Enable this once we switched away from recursion
 	// await suite('Stack Depth', StackApp);
 })();
diff --git a/benchmarks/isomorphic-ui/_colors.js b/benchmarks/isomorphic-ui/_colors.js
new file mode 100644
index 0000000..b0bf934
--- /dev/null
+++ b/benchmarks/isomorphic-ui/_colors.js
@@ -0,0 +1,667 @@
+export default [
+	{
+		hex: '#EFDECD',
+		name: 'Almond',
+		rgb: '(239, 222, 205)'
+	},
+	{
+		hex: '#CD9575',
+		name: 'Antique Brass',
+		rgb: '(205, 149, 117)'
+	},
+	{
+		hex: '#FDD9B5',
+		name: 'Apricot',
+		rgb: '(253, 217, 181)'
+	},
+	{
+		hex: '#78DBE2',
+		name: 'Aquamarine',
+		rgb: '(120, 219, 226)'
+	},
+	{
+		hex: '#87A96B',
+		name: 'Asparagus',
+		rgb: '(135, 169, 107)'
+	},
+	{
+		hex: '#FFA474',
+		name: 'Atomic Tangerine',
+		rgb: '(255, 164, 116)'
+	},
+	{
+		hex: '#FAE7B5',
+		name: 'Banana Mania',
+		rgb: '(250, 231, 181)'
+	},
+	{
+		hex: '#9F8170',
+		name: 'Beaver',
+		rgb: '(159, 129, 112)'
+	},
+	{
+		hex: '#FD7C6E',
+		name: 'Bittersweet',
+		rgb: '(253, 124, 110)'
+	},
+	{
+		hex: '#000000',
+		name: 'Black',
+		rgb: '(0,0,0)'
+	},
+	{
+		hex: '#ACE5EE',
+		name: 'Blizzard Blue',
+		rgb: '(172, 229, 238)'
+	},
+	{
+		hex: '#1F75FE',
+		name: 'Blue',
+		rgb: '(31, 117, 254)'
+	},
+	{
+		hex: '#A2A2D0',
+		name: 'Blue Bell',
+		rgb: '(162, 162, 208)'
+	},
+	{
+		hex: '#6699CC',
+		name: 'Blue Gray',
+		rgb: '(102, 153, 204)'
+	},
+	{
+		hex: '#0D98BA',
+		name: 'Blue Green',
+		rgb: '(13, 152, 186)'
+	},
+	{
+		hex: '#7366BD',
+		name: 'Blue Violet',
+		rgb: '(115, 102, 189)'
+	},
+	{
+		hex: '#DE5D83',
+		name: 'Blush',
+		rgb: '(222, 93, 131)'
+	},
+	{
+		hex: '#CB4154',
+		name: 'Brick Red',
+		rgb: '(203, 65, 84)'
+	},
+	{
+		hex: '#B4674D',
+		name: 'Brown',
+		rgb: '(180, 103, 77)'
+	},
+	{
+		hex: '#FF7F49',
+		name: 'Burnt Orange',
+		rgb: '(255, 127, 73)'
+	},
+	{
+		hex: '#EA7E5D',
+		name: 'Burnt Sienna',
+		rgb: '(234, 126, 93)'
+	},
+	{
+		hex: '#B0B7C6',
+		name: 'Cadet Blue',
+		rgb: '(176, 183, 198)'
+	},
+	{
+		hex: '#FFFF99',
+		name: 'Canary',
+		rgb: '(255, 255, 153)'
+	},
+	{
+		hex: '#1CD3A2',
+		name: 'Caribbean Green',
+		rgb: '(28, 211, 162)'
+	},
+	{
+		hex: '#FFAACC',
+		name: 'Carnation Pink',
+		rgb: '(255, 170, 204)'
+	},
+	{
+		hex: '#DD4492',
+		name: 'Cerise',
+		rgb: '(221, 68, 146)'
+	},
+	{
+		hex: '#1DACD6',
+		name: 'Cerulean',
+		rgb: '(29, 172, 214)'
+	},
+	{
+		hex: '#BC5D58',
+		name: 'Chestnut',
+		rgb: '(188, 93, 88)'
+	},
+	{
+		hex: '#DD9475',
+		name: 'Copper',
+		rgb: '(221, 148, 117)'
+	},
+	{
+		hex: '#9ACEEB',
+		name: 'Cornflower',
+		rgb: '(154, 206, 235)'
+	},
+	{
+		hex: '#FFBCD9',
+		name: 'Cotton Candy',
+		rgb: '(255, 188, 217)'
+	},
+	{
+		hex: '#FDDB6D',
+		name: 'Dandelion',
+		rgb: '(253, 219, 109)'
+	},
+	{
+		hex: '#2B6CC4',
+		name: 'Denim',
+		rgb: '(43, 108, 196)'
+	},
+	{
+		hex: '#EFCDB8',
+		name: 'Desert Sand',
+		rgb: '(239, 205, 184)'
+	},
+	{
+		hex: '#6E5160',
+		name: 'Eggplant',
+		rgb: '(110, 81, 96)'
+	},
+	{
+		hex: '#CEFF1D',
+		name: 'Electric Lime',
+		rgb: '(206, 255, 29)'
+	},
+	{
+		hex: '#71BC78',
+		name: 'Fern',
+		rgb: '(113, 188, 120)'
+	},
+	{
+		hex: '#6DAE81',
+		name: 'Forest Green',
+		rgb: '(109, 174, 129)'
+	},
+	{
+		hex: '#C364C5',
+		name: 'Fuchsia',
+		rgb: '(195, 100, 197)'
+	},
+	{
+		hex: '#CC6666',
+		name: 'Fuzzy Wuzzy',
+		rgb: '(204, 102, 102)'
+	},
+	{
+		hex: '#E7C697',
+		name: 'Gold',
+		rgb: '(231, 198, 151)'
+	},
+	{
+		hex: '#FCD975',
+		name: 'Goldenrod',
+		rgb: '(252, 217, 117)'
+	},
+	{
+		hex: '#A8E4A0',
+		name: 'Granny Smith Apple',
+		rgb: '(168, 228, 160)'
+	},
+	{
+		hex: '#95918C',
+		name: 'Gray',
+		rgb: '(149, 145, 140)'
+	},
+	{
+		hex: '#1CAC78',
+		name: 'Green',
+		rgb: '(28, 172, 120)'
+	},
+	{
+		hex: '#1164B4',
+		name: 'Green Blue',
+		rgb: '(17, 100, 180)'
+	},
+	{
+		hex: '#F0E891',
+		name: 'Green Yellow',
+		rgb: '(240, 232, 145)'
+	},
+	{
+		hex: '#FF1DCE',
+		name: 'Hot Magenta',
+		rgb: '(255, 29, 206)'
+	},
+	{
+		hex: '#B2EC5D',
+		name: 'Inchworm',
+		rgb: '(178, 236, 93)'
+	},
+	{
+		hex: '#5D76CB',
+		name: 'Indigo',
+		rgb: '(93, 118, 203)'
+	},
+	{
+		hex: '#CA3767',
+		name: 'Jazzberry Jam',
+		rgb: '(202, 55, 103)'
+	},
+	{
+		hex: '#3BB08F',
+		name: 'Jungle Green',
+		rgb: '(59, 176, 143)'
+	},
+	{
+		hex: '#FEFE22',
+		name: 'Laser Lemon',
+		rgb: '(254, 254, 34)'
+	},
+	{
+		hex: '#FCB4D5',
+		name: 'Lavender',
+		rgb: '(252, 180, 213)'
+	},
+	{
+		hex: '#FFF44F',
+		name: 'Lemon Yellow',
+		rgb: '(255, 244, 79)'
+	},
+	{
+		hex: '#FFBD88',
+		name: 'Macaroni and Cheese',
+		rgb: '(255, 189, 136)'
+	},
+	{
+		hex: '#F664AF',
+		name: 'Magenta',
+		rgb: '(246, 100, 175)'
+	},
+	{
+		hex: '#AAF0D1',
+		name: 'Magic Mint',
+		rgb: '(170, 240, 209)'
+	},
+	{
+		hex: '#CD4A4C',
+		name: 'Mahogany',
+		rgb: '(205, 74, 76)'
+	},
+	{
+		hex: '#EDD19C',
+		name: 'Maize',
+		rgb: '(237, 209, 156)'
+	},
+	{
+		hex: '#979AAA',
+		name: 'Manatee',
+		rgb: '(151, 154, 170)'
+	},
+	{
+		hex: '#FF8243',
+		name: 'Mango Tango',
+		rgb: '(255, 130, 67)'
+	},
+	{
+		hex: '#C8385A',
+		name: 'Maroon',
+		rgb: '(200, 56, 90)'
+	},
+	{
+		hex: '#EF98AA',
+		name: 'Mauvelous',
+		rgb: '(239, 152, 170)'
+	},
+	{
+		hex: '#FDBCB4',
+		name: 'Melon',
+		rgb: '(253, 188, 180)'
+	},
+	{
+		hex: '#1A4876',
+		name: 'Midnight Blue',
+		rgb: '(26, 72, 118)'
+	},
+	{
+		hex: '#30BA8F',
+		name: 'Mountain Meadow',
+		rgb: '(48, 186, 143)'
+	},
+	{
+		hex: '#C54B8C',
+		name: 'Mulberry',
+		rgb: '(197, 75, 140)'
+	},
+	{
+		hex: '#1974D2',
+		name: 'Navy Blue',
+		rgb: '(25, 116, 210)'
+	},
+	{
+		hex: '#FFA343',
+		name: 'Neon Carrot',
+		rgb: '(255, 163, 67)'
+	},
+	{
+		hex: '#BAB86C',
+		name: 'Olive Green',
+		rgb: '(186, 184, 108)'
+	},
+	{
+		hex: '#FF7538',
+		name: 'Orange',
+		rgb: '(255, 117, 56)'
+	},
+	{
+		hex: '#FF2B2B',
+		name: 'Orange Red',
+		rgb: '(255, 43, 43)'
+	},
+	{
+		hex: '#F8D568',
+		name: 'Orange Yellow',
+		rgb: '(248, 213, 104)'
+	},
+	{
+		hex: '#E6A8D7',
+		name: 'Orchid',
+		rgb: '(230, 168, 215)'
+	},
+	{
+		hex: '#414A4C',
+		name: 'Outer Space',
+		rgb: '(65, 74, 76)'
+	},
+	{
+		hex: '#FF6E4A',
+		name: 'Outrageous Orange',
+		rgb: '(255, 110, 74)'
+	},
+	{
+		hex: '#1CA9C9',
+		name: 'Pacific Blue',
+		rgb: '(28, 169, 201)'
+	},
+	{
+		hex: '#FFCFAB',
+		name: 'Peach',
+		rgb: '(255, 207, 171)'
+	},
+	{
+		hex: '#C5D0E6',
+		name: 'Periwinkle',
+		rgb: '(197, 208, 230)'
+	},
+	{
+		hex: '#FDDDE6',
+		name: 'Piggy Pink',
+		rgb: '(253, 221, 230)'
+	},
+	{
+		hex: '#158078',
+		name: 'Pine Green',
+		rgb: '(21, 128, 120)'
+	},
+	{
+		hex: '#FC74FD',
+		name: 'Pink Flamingo',
+		rgb: '(252, 116, 253)'
+	},
+	{
+		hex: '#F78FA7',
+		name: 'Pink Sherbet',
+		rgb: '(247, 143, 167)'
+	},
+	{
+		hex: '#8E4585',
+		name: 'Plum',
+		rgb: '(142, 69, 133)'
+	},
+	{
+		hex: '#7442C8',
+		name: 'Purple Heart',
+		rgb: '(116, 66, 200)'
+	},
+	{
+		hex: '#9D81BA',
+		name: "Purple Mountain's Majesty",
+		rgb: '(157, 129, 186)'
+	},
+	{
+		hex: '#FE4EDA',
+		name: 'Purple Pizzazz',
+		rgb: '(254, 78, 218)'
+	},
+	{
+		hex: '#FF496C',
+		name: 'Radical Red',
+		rgb: '(255, 73, 108)'
+	},
+	{
+		hex: '#D68A59',
+		name: 'Raw Sienna',
+		rgb: '(214, 138, 89)'
+	},
+	{
+		hex: '#714B23',
+		name: 'Raw Umber',
+		rgb: '(113, 75, 35)'
+	},
+	{
+		hex: '#FF48D0',
+		name: 'Razzle Dazzle Rose',
+		rgb: '(255, 72, 208)'
+	},
+	{
+		hex: '#E3256B',
+		name: 'Razzmatazz',
+		rgb: '(227, 37, 107)'
+	},
+	{
+		hex: '#EE204D',
+		name: 'Red',
+		rgb: '(238,32 ,77 )'
+	},
+	{
+		hex: '#FF5349',
+		name: 'Red Orange',
+		rgb: '(255, 83, 73)'
+	},
+	{
+		hex: '#C0448F',
+		name: 'Red Violet',
+		rgb: '(192, 68, 143)'
+	},
+	{
+		hex: '#1FCECB',
+		name: "Robin's Egg Blue",
+		rgb: '(31, 206, 203)'
+	},
+	{
+		hex: '#7851A9',
+		name: 'Royal Purple',
+		rgb: '(120, 81, 169)'
+	},
+	{
+		hex: '#FF9BAA',
+		name: 'Salmon',
+		rgb: '(255, 155, 170)'
+	},
+	{
+		hex: '#FC2847',
+		name: 'Scarlet',
+		rgb: '(252, 40, 71)'
+	},
+	{
+		hex: '#76FF7A',
+		name: "Screamin' Green",
+		rgb: '(118, 255, 122)'
+	},
+	{
+		hex: '#9FE2BF',
+		name: 'Sea Green',
+		rgb: '(159, 226, 191)'
+	},
+	{
+		hex: '#A5694F',
+		name: 'Sepia',
+		rgb: '(165, 105, 79)'
+	},
+	{
+		hex: '#8A795D',
+		name: 'Shadow',
+		rgb: '(138, 121, 93)'
+	},
+	{
+		hex: '#45CEA2',
+		name: 'Shamrock',
+		rgb: '(69, 206, 162)'
+	},
+	{
+		hex: '#FB7EFD',
+		name: 'Shocking Pink',
+		rgb: '(251, 126, 253)'
+	},
+	{
+		hex: '#CDC5C2',
+		name: 'Silver',
+		rgb: '(205, 197, 194)'
+	},
+	{
+		hex: '#80DAEB',
+		name: 'Sky Blue',
+		rgb: '(128, 218, 235)'
+	},
+	{
+		hex: '#ECEABE',
+		name: 'Spring Green',
+		rgb: '(236, 234, 190)'
+	},
+	{
+		hex: '#FFCF48',
+		name: 'Sunglow',
+		rgb: '(255, 207, 72)'
+	},
+	{
+		hex: '#FD5E53',
+		name: 'Sunset Orange',
+		rgb: '(253, 94, 83)'
+	},
+	{
+		hex: '#FAA76C',
+		name: 'Tan',
+		rgb: '(250, 167, 108)'
+	},
+	{
+		hex: '#18A7B5',
+		name: 'Teal Blue',
+		rgb: '(24, 167, 181)'
+	},
+	{
+		hex: '#EBC7DF',
+		name: 'Thistle',
+		rgb: '(235, 199, 223)'
+	},
+	{
+		hex: '#FC89AC',
+		name: 'Tickle Me Pink',
+		rgb: '(252, 137, 172)'
+	},
+	{
+		hex: '#DBD7D2',
+		name: 'Timberwolf',
+		rgb: '(219, 215, 210)'
+	},
+	{
+		hex: '#17806D',
+		name: 'Tropical Rain Forest',
+		rgb: '(23, 128, 109)'
+	},
+	{
+		hex: '#DEAA88',
+		name: 'Tumbleweed',
+		rgb: '(222, 170, 136)'
+	},
+	{
+		hex: '#77DDE7',
+		name: 'Turquoise Blue',
+		rgb: '(119, 221, 231)'
+	},
+	{
+		hex: '#FFFF66',
+		name: 'Unmellow Yellow',
+		rgb: '(255, 255, 102)'
+	},
+	{
+		hex: '#926EAE',
+		name: 'Violet (Purple)',
+		rgb: '(146, 110, 174)'
+	},
+	{
+		hex: '#324AB2',
+		name: 'Violet Blue',
+		rgb: '(50, 74, 178)'
+	},
+	{
+		hex: '#F75394',
+		name: 'Violet Red',
+		rgb: '(247, 83, 148)'
+	},
+	{
+		hex: '#FFA089',
+		name: 'Vivid Tangerine',
+		rgb: '(255, 160, 137)'
+	},
+	{
+		hex: '#8F509D',
+		name: 'Vivid Violet',
+		rgb: '(143, 80, 157)'
+	},
+	{
+		hex: '#FFFFFF',
+		name: 'White',
+		rgb: '(255, 255, 255)'
+	},
+	{
+		hex: '#A2ADD0',
+		name: 'Wild Blue Yonder',
+		rgb: '(162, 173, 208)'
+	},
+	{
+		hex: '#FF43A4',
+		name: 'Wild Strawberry',
+		rgb: '(255, 67, 164)'
+	},
+	{
+		hex: '#FC6C85',
+		name: 'Wild Watermelon',
+		rgb: '(252, 108, 133)'
+	},
+	{
+		hex: '#CDA4DE',
+		name: 'Wisteria',
+		rgb: '(205, 164, 222)'
+	},
+	{
+		hex: '#FCE883',
+		name: 'Yellow',
+		rgb: '(252, 232, 131)'
+	},
+	{
+		hex: '#C5E384',
+		name: 'Yellow Green',
+		rgb: '(197, 227, 132)'
+	},
+	{
+		hex: '#FFAE42',
+		name: 'Yellow Orange',
+		rgb: '(255, 174, 66)'
+	}
+];
diff --git a/benchmarks/isomorphic-ui/color-picker.js b/benchmarks/isomorphic-ui/color-picker.js
new file mode 100644
index 0000000..e736e5b
--- /dev/null
+++ b/benchmarks/isomorphic-ui/color-picker.js
@@ -0,0 +1,47 @@
+import { h } from 'preact';
+import { useState, useEffect } from 'preact/hooks';
+import colors from './_colors.js';
+
+export function App({ onMount }) {
+	const [selectedColorIndex, setSelectedColorIndex] = useState(0);
+	const selectedColor = colors[selectedColorIndex];
+
+	useEffect(() => {
+		if (onMount) {
+			onMount(setSelectedColorIndex);
+		}
+		if (typeof window !== 'undefined') window.onMount();
+	}, []);
+
+	return (
+		<div className="colors">
+			<h1>Choose your favorite color:</h1>
+			<div className="colors">
+				{colors.length ? (
+					<ul>
+						{colors.map((color, i) => (
+							<li
+								className={
+									'color' + (selectedColorIndex === i ? ' selected' : '')
+								}
+								key={i}
+								style={{
+									backgroundColor: color.hex
+								}}
+								onClick={() => setSelectedColorIndex(i)}
+							>
+								{color.name}
+							</li>
+						))}
+					</ul>
+				) : (
+					<div>No colors!</div>
+				)}
+			</div>
+			<div>
+				You chose:
+				<div className="chosen-color">{selectedColor.name}</div>
+			</div>
+		</div>
+	);
+}
diff --git a/benchmarks/isomorphic-ui-search-results/Footer.js b/benchmarks/isomorphic-ui/search-results/Footer.js
similarity index 100%
rename from benchmarks/isomorphic-ui-search-results/Footer.js
rename to benchmarks/isomorphic-ui/search-results/Footer.js
diff --git a/benchmarks/isomorphic-ui-search-results/SearchResultsItem.js b/benchmarks/isomorphic-ui/search-results/SearchResultsItem.js
similarity index 100%
rename from benchmarks/isomorphic-ui-search-results/SearchResultsItem.js
rename to benchmarks/isomorphic-ui/search-results/SearchResultsItem.js
diff --git a/benchmarks/isomorphic-ui-search-results/data.js b/benchmarks/isomorphic-ui/search-results/data.js
similarity index 100%
rename from benchmarks/isomorphic-ui-search-results/data.js
rename to benchmarks/isomorphic-ui/search-results/data.js
diff --git a/benchmarks/isomorphic-ui-search-results/index.js b/benchmarks/isomorphic-ui/search-results/index.js
similarity index 100%
rename from benchmarks/isomorphic-ui-search-results/index.js
rename to benchmarks/isomorphic-ui/search-results/index.js
diff --git a/benchmarks/isomorphic-ui-search-results/search-results-data.json b/benchmarks/isomorphic-ui/search-results/search-results-data.json
similarity index 100%
rename from benchmarks/isomorphic-ui-search-results/search-results-data.json
rename to benchmarks/isomorphic-ui/search-results/search-results-data.json
diff --git a/benchmarks/lib/benchmark-lite.js b/benchmarks/lib/benchmark-lite.js
index 0750ef6..87f3ffe 100644
--- a/benchmarks/lib/benchmark-lite.js
+++ b/benchmarks/lib/benchmark-lite.js
@@ -1,3 +1,5 @@
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
 export default class Suite {
 	constructor(name, { iterations = 10, timeLimit = 5000 } = {}) {
 		this.name = name;
@@ -9,12 +11,14 @@ export default class Suite {
 		this.tests.push({ name, executor });
 		return this;
 	}
-	run() {
+	async run() {
 		console.log(`  ${this.name}:`);
 		const results = [];
 		let fastest = 0;
 		for (const test of this.tests) {
+			await sleep(50);
 			for (let i = 0; i < 5; i++) test.executor(i);
+			await sleep(10);
 			const result = this.runOne(test);
 			if (result.hz > fastest) fastest = result.hz;
 			results.push({ ...test, ...result });
diff --git a/benchmarks/lib/render-to-string.js b/benchmarks/lib/render-to-string.js
index 275bb31..e7f4c32 100644
--- a/benchmarks/lib/render-to-string.js
+++ b/benchmarks/lib/render-to-string.js
@@ -1,255 +1,268 @@
 /* eslint-disable */
-// preact-render-to-string@5.1.16
-import { options as e, createElement as t, Fragment as r } from 'preact';
-var n = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i,
-	o = /[&<>"]/g,
-	i = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' },
-	a = function (e) {
-		return i[e] || e;
-	};
-function l(e) {
-	return 'string' != typeof e && (e = String(e)), e.replace(o, a);
+// preact-render-to-string@5.1.21
+import { options as e, Fragment as t } from 'preact';
+var r = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i,
+	n = /[&<>"]/;
+function o(e) {
+	var t = String(e);
+	return n.test(t)
+		? t
+				.replace(/&/g, '&amp;')
+				.replace(/</g, '&lt;')
+				.replace(/>/g, '&gt;')
+				.replace(/"/g, '&quot;')
+		: t;
 }
-var s = function (e, t) {
+var a = function (e, t) {
 		return String(e).replace(/(\n+)/g, '$1' + (t || '\t'));
 	},
-	f = function (e, t, r) {
+	i = function (e, t, r) {
 		return (
 			String(e).length > (t || 40) ||
 			(!r && -1 !== String(e).indexOf('\n')) ||
 			-1 !== String(e).indexOf('<')
 		);
 	},
-	u = {};
-function c(e) {
+	l = {};
+function s(e) {
 	var t = '';
-	for (var r in e) {
-		var o = e[r];
+	for (var n in e) {
+		var o = e[n];
 		null != o &&
 			'' !== o &&
 			(t && (t += ' '),
 			(t +=
-				'-' == r[0]
-					? r
-					: u[r] || (u[r] = r.replace(/([A-Z])/g, '-$1').toLowerCase())),
+				'-' == n[0]
+					? n
+					: l[n] || (l[n] = n.replace(/([A-Z])/g, '-$1').toLowerCase())),
 			(t += ': '),
 			(t += o),
-			'number' == typeof o && !1 === n.test(r) && (t += 'px'),
+			'number' == typeof o && !1 === r.test(n) && (t += 'px'),
 			(t += ';'));
 	}
 	return t || void 0;
 }
-function p(e, t) {
+function f(e, t) {
 	for (var r in t) e[r] = t[r];
 	return e;
 }
-function _(e, t) {
+function u(e, t) {
 	return (
-		Array.isArray(t) ? t.reduce(_, e) : null != t && !1 !== t && e.push(t), e
+		Array.isArray(t) ? t.reduce(u, e) : null != t && !1 !== t && e.push(t), e
 	);
 }
-var v = { shallow: !0 },
-	d = [],
-	g = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/,
-	h = /[\s\n\\/='"\0<>]/,
-	m = function () {};
-b.render = b;
-var x = function (e, t) {
-		return b(e, t, v);
+var c = { shallow: !0 },
+	p = [],
+	_ = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/,
+	d = /[\s\n\\/='"\0<>]/;
+function v() {
+	this.__d = !0;
+}
+m.render = m;
+var g = function (e, t) {
+		return m(e, t, c);
 	},
-	y = [];
-function b(t, r, n) {
-	var o = S(t, r, n);
-	return e.__c && e.__c(t, y), o;
+	h = [];
+function m(t, r, n) {
+	(r = r || {}), (n = n || {});
+	var o = e.__s;
+	e.__s = !0;
+	var a = x(t, r, n);
+	return e.__c && e.__c(t, h), (h.length = 0), (e.__s = o), a;
 }
-function S(n, o, i, a, u, v) {
-	if (null == n || 'boolean' == typeof n) return '';
-	Array.isArray(n) && (n = t(r, null, n));
-	var x = n.type,
-		y = n.props,
-		b = !1;
-	o = o || {};
+function x(r, n, l, c, g, h) {
+	if (null == r || 'boolean' == typeof r) return '';
+	if ('object' != typeof r) return o(r);
+	var m = l.pretty,
+		y = m && 'string' == typeof m ? m : '\t';
+	if (Array.isArray(r)) {
+		for (var b = '', S = 0; S < r.length; S++)
+			m && S > 0 && (b += '\n'), (b += x(r[S], n, l, c, g, h));
+		return b;
+	}
 	var w,
-		k = (i = i || {}).pretty,
-		O = k && 'string' == typeof k ? k : '\t';
-	if ('object' != typeof n && !x) return l(n);
-	if ('function' == typeof x) {
-		if (((b = !0), !i.shallow || (!a && !1 !== i.renderRootComponent))) {
-			if (x === r) {
-				var C = '',
-					A = [];
-				_(A, n.props.children);
-				for (var H = 0; H < A.length; H++)
-					C +=
-						(H > 0 && k ? '\n' : '') +
-						S(A[H], o, i, !1 !== i.shallowHighOrder, u, v);
-				return C;
+		k = r.type,
+		O = r.props,
+		C = !1;
+	if ('function' == typeof k) {
+		if (((C = !0), !l.shallow || (!c && !1 !== l.renderRootComponent))) {
+			if (k === t) {
+				var A = [];
+				return (
+					u(A, r.props.children), x(A, n, l, !1 !== l.shallowHighOrder, g, h)
+				);
 			}
-			var j,
-				F = (n.__c = {
-					__v: n,
-					context: o,
-					props: n.props,
-					setState: m,
-					forceUpdate: m,
+			var H,
+				j = (r.__c = {
+					__v: r,
+					context: n,
+					props: r.props,
+					setState: v,
+					forceUpdate: v,
+					__d: !0,
 					__h: []
 				});
-			if (
-				(e.__b && e.__b(n),
-				e.__r && e.__r(n),
-				x.prototype && 'function' == typeof x.prototype.render)
-			) {
-				var M = x.contextType,
-					T = M && o[M.__c],
-					$ = null != M ? (T ? T.props.value : M.__) : o;
-				((F = n.__c = new x(y, $)).__v = n),
-					(F._dirty = F.__d = !0),
-					(F.props = y),
-					null == F.state && (F.state = {}),
-					null == F._nextState &&
-						null == F.__s &&
-						(F._nextState = F.__s = F.state),
-					(F.context = $),
-					x.getDerivedStateFromProps
-						? (F.state = p(
-								p({}, F.state),
-								x.getDerivedStateFromProps(F.props, F.state)
+			e.__b && e.__b(r);
+			var F = e.__r;
+			if (k.prototype && 'function' == typeof k.prototype.render) {
+				var M = k.contextType,
+					T = M && n[M.__c],
+					$ = null != M ? (T ? T.props.value : M.__) : n;
+				((j = r.__c = new k(O, $)).__v = r),
+					(j._dirty = j.__d = !0),
+					(j.props = O),
+					null == j.state && (j.state = {}),
+					null == j._nextState &&
+						null == j.__s &&
+						(j._nextState = j.__s = j.state),
+					(j.context = $),
+					k.getDerivedStateFromProps
+						? (j.state = f(
+								f({}, j.state),
+								k.getDerivedStateFromProps(j.props, j.state)
 						  ))
-						: F.componentWillMount &&
-						  (F.componentWillMount(),
-						  (F.state =
-								F._nextState !== F.state
-									? F._nextState
-									: F.__s !== F.state
-									? F.__s
-									: F.state)),
-					(j = F.render(F.props, F.state, F.context));
-			} else {
-				var L = x.contextType,
-					E = L && o[L.__c];
-				j = x.call(n.__c, y, null != L ? (E ? E.props.value : L.__) : o);
-			}
+						: j.componentWillMount &&
+						  (j.componentWillMount(),
+						  (j.state =
+								j._nextState !== j.state
+									? j._nextState
+									: j.__s !== j.state
+									? j.__s
+									: j.state)),
+					F && F(r),
+					(H = j.render(j.props, j.state, j.context));
+			} else
+				for (
+					var L = k.contextType,
+						E = L && n[L.__c],
+						D = null != L ? (E ? E.props.value : L.__) : n,
+						N = 0;
+					j.__d && N++ < 25;
+
+				)
+					(j.__d = !1), F && F(r), (H = k.call(r.__c, O, D));
 			return (
-				F.getChildContext && (o = p(p({}, o), F.getChildContext())),
-				e.diffed && e.diffed(n),
-				S(j, o, i, !1 !== i.shallowHighOrder, u, v)
+				j.getChildContext && (n = f(f({}, n), j.getChildContext())),
+				e.diffed && e.diffed(r),
+				x(H, n, l, !1 !== l.shallowHighOrder, g, h)
 			);
 		}
-		x =
-			(w = x).displayName ||
+		k =
+			(w = k).displayName ||
 			(w !== Function && w.name) ||
 			(function (e) {
 				var t = (Function.prototype.toString
 					.call(e)
 					.match(/^\s*function\s+([^( ]+)/) || '')[1];
 				if (!t) {
-					for (var r = -1, n = d.length; n--; )
-						if (d[n] === e) {
+					for (var r = -1, n = p.length; n--; )
+						if (p[n] === e) {
 							r = n;
 							break;
 						}
-					r < 0 && (r = d.push(e) - 1), (t = 'UnnamedComponent' + r);
+					r < 0 && (r = p.push(e) - 1), (t = 'UnnamedComponent' + r);
 				}
 				return t;
 			})(w);
 	}
-	var D,
-		N,
-		P = '';
-	if (y) {
-		var R = Object.keys(y);
-		i && !0 === i.sortAttributes && R.sort();
-		for (var U = 0; U < R.length; U++) {
-			var W = R[U],
-				q = y[W];
-			if ('children' !== W) {
+	var P,
+		R,
+		U = '<' + k;
+	if (O) {
+		var W = Object.keys(O);
+		l && !0 === l.sortAttributes && W.sort();
+		for (var q = 0; q < W.length; q++) {
+			var z = W[q],
+				I = O[z];
+			if ('children' !== z) {
 				if (
-					!W.match(/[\s\n\\/='"\0<>]/) &&
-					((i && i.allAttributes) ||
-						('key' !== W &&
-							'ref' !== W &&
-							'__self' !== W &&
-							'__source' !== W &&
-							'defaultValue' !== W))
+					!d.test(z) &&
+					((l && l.allAttributes) ||
+						('key' !== z && 'ref' !== z && '__self' !== z && '__source' !== z))
 				) {
-					if ('className' === W) {
-						if (y.class) continue;
-						W = 'class';
+					if ('defaultValue' === z) z = 'value';
+					else if ('className' === z) {
+						if (void 0 !== O.class) continue;
+						z = 'class';
 					} else
-						u &&
-							W.match(/^xlink:?./) &&
-							(W = W.toLowerCase().replace(/^xlink:?/, 'xlink:'));
-					if ('htmlFor' === W) {
-						if (y.for) continue;
-						W = 'for';
+						g &&
+							z.match(/^xlink:?./) &&
+							(z = z.toLowerCase().replace(/^xlink:?/, 'xlink:'));
+					if ('htmlFor' === z) {
+						if (O.for) continue;
+						z = 'for';
 					}
-					'style' === W && q && 'object' == typeof q && (q = c(q)),
-						'a' === W[0] &&
-							'r' === W[1] &&
-							'boolean' == typeof q &&
-							(q = String(q));
-					var z = i.attributeHook && i.attributeHook(W, q, o, i, b);
-					if (z || '' === z) P += z;
-					else if ('dangerouslySetInnerHTML' === W) N = q && q.__html;
-					else if ('textarea' === x && 'value' === W) D = q;
-					else if ((q || 0 === q || '' === q) && 'function' != typeof q) {
-						if (!((!0 !== q && '' !== q) || ((q = W), i && i.xml))) {
-							P += ' ' + W;
+					'style' === z && I && 'object' == typeof I && (I = s(I)),
+						'a' === z[0] &&
+							'r' === z[1] &&
+							'boolean' == typeof I &&
+							(I = String(I));
+					var V = l.attributeHook && l.attributeHook(z, I, n, l, C);
+					if (V || '' === V) U += V;
+					else if ('dangerouslySetInnerHTML' === z) R = I && I.__html;
+					else if ('textarea' === k && 'value' === z) P = I;
+					else if ((I || 0 === I || '' === I) && 'function' != typeof I) {
+						if (!((!0 !== I && '' !== I) || ((I = z), l && l.xml))) {
+							U += ' ' + z;
 							continue;
 						}
-						if ('value' === W) {
-							if ('select' === x) {
-								v = q;
+						if ('value' === z) {
+							if ('select' === k) {
+								h = I;
 								continue;
 							}
-							'option' === x && v == q && (P += ' selected');
+							'option' === k &&
+								h == I &&
+								void 0 === O.selected &&
+								(U += ' selected');
 						}
-						P += ' ' + W + '="' + l(q) + '"';
+						U += ' ' + z + '="' + o(I) + '"';
 					}
 				}
-			} else D = q;
+			} else P = I;
 		}
 	}
-	if (k) {
-		var I = P.replace(/^\n\s*/, ' ');
-		I === P || ~I.indexOf('\n')
-			? k && ~P.indexOf('\n') && (P += '\n')
-			: (P = I);
+	if (m) {
+		var Z = U.replace(/\n\s*/, ' ');
+		Z === U || ~Z.indexOf('\n')
+			? m && ~U.indexOf('\n') && (U += '\n')
+			: (U = Z);
 	}
-	if (((P = '<' + x + P + '>'), h.test(String(x))))
-		throw new Error(x + ' is not a valid HTML tag name in ' + P);
-	var V,
-		Z = g.test(String(x)) || (i.voidElements && i.voidElements.test(String(x))),
-		B = [];
-	if (N) k && f(N) && (N = '\n' + O + s(N, O)), (P += N);
-	else if (null != D && _((V = []), D).length) {
-		for (var G = k && ~P.indexOf('\n'), J = !1, K = 0; K < V.length; K++) {
-			var Q = V[K];
-			if (null != Q && !1 !== Q) {
-				var X = S(Q, o, i, !0, 'svg' === x || ('foreignObject' !== x && u), v);
-				if ((k && !G && f(X) && (G = !0), X))
-					if (k) {
-						var Y = X.length > 0 && '<' != X[0];
-						J && Y ? (B[B.length - 1] += X) : B.push(X), (J = Y);
-					} else B.push(X);
+	if (((U += '>'), d.test(k)))
+		throw new Error(k + ' is not a valid HTML tag name in ' + U);
+	var B,
+		G = _.test(k) || (l.voidElements && l.voidElements.test(k)),
+		J = [];
+	if (R) m && i(R) && (R = '\n' + y + a(R, y)), (U += R);
+	else if (null != P && u((B = []), P).length) {
+		for (var K = m && ~U.indexOf('\n'), Q = !1, X = 0; X < B.length; X++) {
+			var Y = B[X];
+			if (null != Y && !1 !== Y) {
+				var ee = x(Y, n, l, !0, 'svg' === k || ('foreignObject' !== k && g), h);
+				if ((m && !K && i(ee) && (K = !0), ee))
+					if (m) {
+						var te = ee.length > 0 && '<' != ee[0];
+						Q && te ? (J[J.length - 1] += ee) : J.push(ee), (Q = te);
+					} else J.push(ee);
 			}
 		}
-		if (k && G) for (var ee = B.length; ee--; ) B[ee] = '\n' + O + s(B[ee], O);
+		if (m && K) for (var re = J.length; re--; ) J[re] = '\n' + y + a(J[re], y);
 	}
-	if (B.length || N) P += B.join('');
-	else if (i && i.xml) return P.substring(0, P.length - 1) + ' />';
+	if (J.length || R) U += J.join('');
+	else if (l && l.xml) return U.substring(0, U.length - 1) + ' />';
 	return (
-		!Z || V || N
-			? (k && ~P.indexOf('\n') && (P += '\n'), (P += '</' + x + '>'))
-			: (P = P.replace(/>$/, ' />')),
-		P
+		!G || B || R
+			? (m && ~U.indexOf('\n') && (U += '\n'), (U += '</' + k + '>'))
+			: (U = U.replace(/>$/, ' />')),
+		U
 	);
 }
-b.shallowRender = x;
-export default b;
+m.shallowRender = g;
+export default m;
 export {
-	b as render,
-	b as renderToStaticMarkup,
-	b as renderToString,
-	x as shallowRender
+	m as render,
+	m as renderToStaticMarkup,
+	m as renderToString,
+	g as shallowRender
 };
+//# sourceMappingURL=index.module.js.map
diff --git a/package-lock.json b/package-lock.json
index e504348..079e9c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
 				"@babel/register": "^7.12.10",
 				"@changesets/changelog-github": "^0.4.1",
 				"@changesets/cli": "^2.18.0",
+				"benchmarkjs": "^0.1.8",
 				"benchmarkjs-pretty": "^2.0.1",
 				"chai": "^4.2.0",
 				"copyfiles": "^2.4.1",
@@ -2628,6 +2629,12 @@
 				"platform": "^1.3.3"
 			}
 		},
+		"node_modules/benchmarkjs": {
+			"version": "0.1.8",
+			"resolved": "https://registry.npmjs.org/benchmarkjs/-/benchmarkjs-0.1.8.tgz",
+			"integrity": "sha512-0NBdYmBvCe5EdKRYiRk0op3MzbnEcLz/3kjKl1BD5b8q0HmzeHOs10lwZXJoC95sfgQOnxBTBNbbe1t56yvYpg==",
+			"dev": true
+		},
 		"node_modules/benchmarkjs-pretty": {
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/benchmarkjs-pretty/-/benchmarkjs-pretty-2.0.1.tgz",
@@ -15697,6 +15704,12 @@
 				"platform": "^1.3.3"
 			}
 		},
+		"benchmarkjs": {
+			"version": "0.1.8",
+			"resolved": "https://registry.npmjs.org/benchmarkjs/-/benchmarkjs-0.1.8.tgz",
+			"integrity": "sha512-0NBdYmBvCe5EdKRYiRk0op3MzbnEcLz/3kjKl1BD5b8q0HmzeHOs10lwZXJoC95sfgQOnxBTBNbbe1t56yvYpg==",
+			"dev": true
+		},
 		"benchmarkjs-pretty": {
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/benchmarkjs-pretty/-/benchmarkjs-pretty-2.0.1.tgz",
diff --git a/package.json b/package.json
index 9faec54..dfad98a 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,22 @@
 {
 	"name": "preact-render-to-string",
 	"amdName": "preactRenderToString",
-	"version": "5.2.1",
+	"version": "5.2.2",
 	"description": "Render JSX to an HTML string, with support for Preact components.",
 	"main": "dist/index.js",
 	"umd:main": "dist/index.js",
 	"module": "dist/index.module.js",
 	"jsnext:main": "dist/index.module.js",
+	"types": "src/index.d.ts",
 	"exports": {
 		".": {
+			"types": "./src/index.d.ts",
 			"import": "./dist/index.mjs",
 			"browser": "./dist/index.module.js",
 			"require": "./dist/index.js"
 		},
 		"./jsx": {
+			"types": "./jsx.d.ts",
 			"import": "./dist/jsx.mjs",
 			"browser": "./dist/jsx.module.js",
 			"require": "./dist/jsx.js"
@@ -91,7 +94,6 @@
 	},
 	"author": "Jason Miller <jason@developit.ca>",
 	"license": "MIT",
-	"typings": "src/index.d.ts",
 	"repository": "developit/preact-render-to-string",
 	"bugs": "https://github.com/developit/preact-render-to-string/issues",
 	"homepage": "https://github.com/developit/preact-render-to-string",
@@ -137,5 +139,10 @@
 		"hooks": {
 			"pre-commit": "lint-staged"
 		}
+	},
+	"mangle": {
+		"compress": {
+			"reduce_funcs": false
+		}
 	}
 }
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..655802b
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,14 @@
+// Options hooks
+export const DIFF = '__b';
+export const RENDER = '__r';
+export const DIFFED = 'diffed';
+export const COMMIT = '__c';
+export const SKIP_EFFECTS = '__s';
+
+// VNode properties
+export const COMPONENT = '__c';
+
+// Component properties
+export const VNODE = '__v';
+export const DIRTY = '__d';
+export const NEXT_STATE = '__s';
diff --git a/src/index.js b/src/index.js
index b16fbbf..c85c0dd 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,28 +1,30 @@
 import {
 	encodeEntities,
-	indent,
-	isLargeString,
 	styleObjToCss,
-	getChildren
+	getContext,
+	createComponent,
+	UNSAFE_NAME,
+	XLINK,
+	VOID_ELEMENTS
 } from './util';
 import { options, Fragment } from 'preact';
+import { _renderToStringPretty } from './pretty';
+import {
+	COMMIT,
+	COMPONENT,
+	DIFF,
+	DIFFED,
+	DIRTY,
+	NEXT_STATE,
+	RENDER,
+	SKIP_EFFECTS,
+	VNODE
+} from './constants';
 
 /** @typedef {import('preact').VNode} VNode */
 
 const SHALLOW = { shallow: true };
 
-// components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names.
-const UNNAMED = [];
-
-const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
-
-const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
-const XLINK = /^xlink:?./;
-
-function markAsDirty() {
-	this.__d = true;
-}
-
 /** Render Preact JSX + Components to an HTML string.
  *	@name render
  *	@function
@@ -48,346 +50,139 @@ let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW);
 const EMPTY_ARR = [];
 function renderToString(vnode, context, opts) {
 	context = context || {};
-	opts = opts || {};
 
 	// Performance optimization: `renderToString` is synchronous and we
 	// therefore don't execute any effects. To do that we pass an empty
 	// array to `options._commit` (`__c`). But we can go one step further
 	// and avoid a lot of dirty checks and allocations by setting
 	// `options._skipEffects` (`__s`) too.
-	const previousSkipEffects = options.__s;
-	options.__s = true;
+	const previousSkipEffects = options[SKIP_EFFECTS];
+	options[SKIP_EFFECTS] = true;
 
 	let res;
-	if (opts.pretty || opts.sortAttributes) {
+	if (
+		opts &&
+		(opts.pretty ||
+			opts.voidElements ||
+			opts.sortAttributes ||
+			opts.shallow ||
+			opts.allAttributes ||
+			opts.xml ||
+			opts.attributeHook)
+	) {
 		res = _renderToStringPretty(vnode, context, opts);
 	} else {
-		res = _renderToString(vnode, context, opts);
+		res = _renderToString(vnode, context, false, undefined);
 	}
 
 	// options._commit, we don't schedule any effects in this library right now,
 	// so we can pass an empty queue to this hook.
-	if (options.__c) options.__c(vnode, EMPTY_ARR);
+	if (options[COMMIT]) options[COMMIT](vnode, EMPTY_ARR);
+	options[SKIP_EFFECTS] = previousSkipEffects;
 	EMPTY_ARR.length = 0;
-	options.__s = previousSkipEffects;
 	return res;
 }
 
-function createComponent(vnode, context) {
-	return {
-		__v: vnode,
-		context,
-		props: vnode.props,
-		// silently drop state updates
-		setState: markAsDirty,
-		forceUpdate: markAsDirty,
-		__d: true,
-		// hooks
-		__h: []
-	};
-}
+function renderFunctionComponent(vnode, context) {
+	let rendered,
+		c = createComponent(vnode, context),
+		cctx = getContext(vnode.type, context);
 
-// Necessary for createContext api. Setting this property will pass
-// the context value as `this.context` just for this component.
-function getContext(nodeName, context) {
-	let cxType = nodeName.contextType;
-	let provider = cxType && context[cxType.__c];
-	return cxType != null
-		? provider
-			? provider.props.value
-			: cxType.__
-		: context;
-}
+	vnode[COMPONENT] = c;
 
-/** The default export is an alias of `render()`. */
-function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
-	if (vnode == null || typeof vnode === 'boolean') {
-		return '';
-	}
+	// If a hook invokes setState() to invalidate the component during rendering,
+	// re-render it up to 25 times to allow "settling" of memoized states.
+	// Note:
+	//   This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
+	//   https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
+	let renderHook = options[RENDER];
+	let count = 0;
+	while (c[DIRTY] && count++ < 25) {
+		c[DIRTY] = false;
 
-	// #text nodes
-	if (typeof vnode !== 'object') {
-		return encodeEntities(vnode);
-	}
+		if (renderHook) renderHook(vnode);
 
-	if (Array.isArray(vnode)) {
-		const rendered = [];
-		for (let i = 0; i < vnode.length; i++) {
-			rendered.push(
-				_renderToString(vnode[i], context, opts, inner, isSvgMode, selectValue)
-			);
-		}
-		return rendered.join('');
+		// stateless functional components
+		rendered = vnode.type.call(c, vnode.props, cctx);
 	}
 
-	let nodeName = vnode.type,
-		props = vnode.props,
-		isComponent = false;
-
-	// components
-	if (typeof nodeName === 'function') {
-		isComponent = true;
-		if (opts.shallow && (inner || opts.renderRootComponent === false)) {
-			nodeName = getComponentName(nodeName);
-		} else if (nodeName === Fragment) {
-			const children = [];
-			getChildren(children, vnode.props.children);
-			return _renderToString(
-				children,
-				context,
-				opts,
-				opts.shallowHighOrder !== false,
-				isSvgMode,
-				selectValue
-			);
-		} else {
-			let rendered;
-
-			let c = (vnode.__c = createComponent(vnode, context));
-
-			// options._diff
-			if (options.__b) options.__b(vnode);
-
-			// options._render
-			let renderHook = options.__r;
-
-			if (
-				!nodeName.prototype ||
-				typeof nodeName.prototype.render !== 'function'
-			) {
-				let cctx = getContext(nodeName, context);
-
-				// If a hook invokes setState() to invalidate the component during rendering,
-				// re-render it up to 25 times to allow "settling" of memoized states.
-				// Note:
-				//   This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
-				//   https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
-				let count = 0;
-				while (c.__d && count++ < 25) {
-					c.__d = false;
-
-					if (renderHook) renderHook(vnode);
-
-					// stateless functional components
-					rendered = nodeName.call(vnode.__c, props, cctx);
-				}
-			} else {
-				let cctx = getContext(nodeName, context);
-
-				// c = new nodeName(props, context);
-				c = vnode.__c = new nodeName(props, cctx);
-				c.__v = vnode;
-				// turn off stateful re-rendering:
-				c._dirty = c.__d = true;
-				c.props = props;
-				if (c.state == null) c.state = {};
-
-				if (c._nextState == null && c.__s == null) {
-					c._nextState = c.__s = c.state;
-				}
-
-				c.context = cctx;
-				if (nodeName.getDerivedStateFromProps)
-					c.state = Object.assign(
-						{},
-						c.state,
-						nodeName.getDerivedStateFromProps(c.props, c.state)
-					);
-				else if (c.componentWillMount) {
-					c.componentWillMount();
-
-					// If the user called setState in cWM we need to flush pending,
-					// state updates. This is the same behaviour in React.
-					c.state =
-						c._nextState !== c.state
-							? c._nextState
-							: c.__s !== c.state
-							? c.__s
-							: c.state;
-				}
-
-				if (renderHook) renderHook(vnode);
-
-				rendered = c.render(c.props, c.state, c.context);
-			}
-
-			if (c.getChildContext) {
-				context = Object.assign({}, context, c.getChildContext());
-			}
+	return rendered;
+}
 
-			if (options.diffed) options.diffed(vnode);
-			return _renderToString(
-				rendered,
-				context,
-				opts,
-				opts.shallowHighOrder !== false,
-				isSvgMode,
-				selectValue
-			);
-		}
+function renderClassComponent(vnode, context) {
+	let nodeName = vnode.type,
+		cctx = getContext(nodeName, context);
+
+	// c = new nodeName(props, context);
+	let c = new nodeName(vnode.props, cctx);
+	vnode[COMPONENT] = c;
+	c[VNODE] = vnode;
+	// turn off stateful re-rendering:
+	c[DIRTY] = true;
+	c.props = vnode.props;
+	if (c.state == null) c.state = {};
+
+	if (c[NEXT_STATE] == null) {
+		c[NEXT_STATE] = c.state;
 	}
 
-	// render JSX to HTML
-	let s = `<${nodeName}`,
-		propChildren,
-		html;
-
-	if (props) {
-		for (let name in props) {
-			let v = props[name];
-			if (name === 'children') {
-				propChildren = v;
-				continue;
-			}
-
-			if (UNSAFE_NAME.test(name)) continue;
-
-			if (
-				!(opts && opts.allAttributes) &&
-				(name === 'key' ||
-					name === 'ref' ||
-					name === '__self' ||
-					name === '__source')
-			)
-				continue;
-
-			if (name === 'defaultValue') {
-				name = 'value';
-			} else if (name === 'defaultChecked') {
-				name = 'checked';
-			} else if (name === 'defaultSelected') {
-				name = 'selected';
-			} else if (name === 'className') {
-				if (typeof props.class !== 'undefined') continue;
-				name = 'class';
-			} else if (isSvgMode && XLINK.test(name)) {
-				name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
-			}
-
-			if (name === 'htmlFor') {
-				if (props.for) continue;
-				name = 'for';
-			}
-
-			if (name === 'style' && v && typeof v === 'object') {
-				v = styleObjToCss(v);
-			}
-
-			// always use string values instead of booleans for aria attributes
-			// also see https://github.com/preactjs/preact/pull/2347/files
-			if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') {
-				v = String(v);
-			}
-
-			let hooked =
-				opts.attributeHook &&
-				opts.attributeHook(name, v, context, opts, isComponent);
-			if (hooked || hooked === '') {
-				s = s + hooked;
-				continue;
-			}
-
-			if (name === 'dangerouslySetInnerHTML') {
-				html = v && v.__html;
-			} else if (nodeName === 'textarea' && name === 'value') {
-				// <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
-				propChildren = v;
-			} else if ((v || v === 0 || v === '') && typeof v !== 'function') {
-				if (v === true || v === '') {
-					v = name;
-					// in non-xml mode, allow boolean attributes
-					if (!opts || !opts.xml) {
-						s = s + ' ' + name;
-						continue;
-					}
-				}
-
-				if (name === 'value') {
-					if (nodeName === 'select') {
-						selectValue = v;
-						continue;
-					} else if (
-						// If we're looking at an <option> and it's the currently selected one
-						nodeName === 'option' &&
-						selectValue == v &&
-						// and the <option> doesn't already have a selected attribute on it
-						typeof props.selected === 'undefined'
-					) {
-						s = `${s} selected`;
-					}
-				}
-				s = s + ` ${name}="${encodeEntities(v)}"`;
-			}
-		}
+	c.context = cctx;
+	if (nodeName.getDerivedStateFromProps) {
+		c.state = assign(
+			{},
+			c.state,
+			nodeName.getDerivedStateFromProps(c.props, c.state)
+		);
+	} else if (c.componentWillMount) {
+		c.componentWillMount();
+
+		// If the user called setState in cWM we need to flush pending,
+		// state updates. This is the same behaviour in React.
+		c.state = c[NEXT_STATE] !== c.state ? c[NEXT_STATE] : c.state;
 	}
 
-	s = s + '>';
+	let renderHook = options[RENDER];
+	if (renderHook) renderHook(vnode);
 
-	if (UNSAFE_NAME.test(nodeName))
-		throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`);
-
-	let isVoid =
-		VOID_ELEMENTS.test(nodeName) ||
-		(opts.voidElements && opts.voidElements.test(nodeName));
-	let pieces = '';
-
-	let children;
-	if (html) {
-		s = s + html;
-	} else if (
-		propChildren != null &&
-		getChildren((children = []), propChildren).length
-	) {
-		for (let i = 0; i < children.length; i++) {
-			let child = children[i];
-
-			if (child != null && child !== false) {
-				let childSvgMode =
-						nodeName === 'svg'
-							? true
-							: nodeName === 'foreignObject'
-							? false
-							: isSvgMode,
-					ret = _renderToString(
-						child,
-						context,
-						opts,
-						true,
-						childSvgMode,
-						selectValue
-					);
+	return c.render(c.props, c.state, c.context);
+}
 
-				// Skip if we received an empty string
-				if (ret) {
-					pieces = pieces + ret;
-				}
-			}
-		}
+function normalizePropName(name, isSvgMode) {
+	if (name === 'className') {
+		return 'class';
+	} else if (name === 'htmlFor') {
+		return 'for';
+	} else if (name === 'defaultValue') {
+		return 'value';
+	} else if (name === 'defaultChecked') {
+		return 'checked';
+	} else if (name === 'defaultSelected') {
+		return 'selected';
+	} else if (isSvgMode && XLINK.test(name)) {
+		return name.toLowerCase().replace(/^xlink:?/, 'xlink:');
 	}
 
-	if (pieces.length || html) {
-		s = s + pieces;
-	} else if (opts && opts.xml) {
-		return s.substring(0, s.length - 1) + ' />';
-	}
+	return name;
+}
 
-	if (isVoid && !children && !html) {
-		return s.replace(/>$/, ' />');
+function normalizePropValue(name, v) {
+	if (name === 'style' && v != null && typeof v === 'object') {
+		return styleObjToCss(v);
+	} else if (name[0] === 'a' && name[1] === 'r' && typeof v === 'boolean') {
+		// always use string values instead of booleans for aria attributes
+		// also see https://github.com/preactjs/preact/pull/2347/files
+		return String(v);
 	}
 
-	return `${s}</${nodeName}>`;
+	return v;
 }
 
+const isArray = Array.isArray;
+const assign = Object.assign;
+
 /** The default export is an alias of `render()`. */
-function _renderToStringPretty(
-	vnode,
-	context,
-	opts,
-	inner,
-	isSvgMode,
-	selectValue
-) {
-	if (vnode == null || typeof vnode === 'boolean') {
+function _renderToString(vnode, context, isSvgMode, selectValue) {
+	if (vnode == null || vnode === true || vnode === false || vnode === '') {
 		return '';
 	}
 
@@ -396,213 +191,119 @@ function _renderToStringPretty(
 		return encodeEntities(vnode);
 	}
 
-	let pretty = opts.pretty,
-		indentChar = pretty && typeof pretty === 'string' ? pretty : '\t';
-
-	if (Array.isArray(vnode)) {
+	if (isArray(vnode)) {
 		let rendered = '';
 		for (let i = 0; i < vnode.length; i++) {
-			if (pretty && i > 0) rendered = rendered + '\n';
 			rendered =
-				rendered +
-				_renderToStringPretty(
-					vnode[i],
-					context,
-					opts,
-					inner,
-					isSvgMode,
-					selectValue
-				);
+				rendered + _renderToString(vnode[i], context, isSvgMode, selectValue);
 		}
 		return rendered;
 	}
 
 	let nodeName = vnode.type,
-		props = vnode.props,
-		isComponent = false;
+		props = vnode.props;
+	const isComponent = typeof nodeName === 'function';
 
 	// components
-	if (typeof nodeName === 'function') {
-		isComponent = true;
-		if (opts.shallow && (inner || opts.renderRootComponent === false)) {
-			nodeName = getComponentName(nodeName);
-		} else if (nodeName === Fragment) {
-			const children = [];
-			getChildren(children, vnode.props.children);
-			return _renderToStringPretty(
-				children,
+	if (isComponent) {
+		if (nodeName === Fragment) {
+			return _renderToString(
+				vnode.props.children,
 				context,
-				opts,
-				opts.shallowHighOrder !== false,
 				isSvgMode,
 				selectValue
 			);
-		} else {
-			let rendered;
-
-			let c = (vnode.__c = createComponent(vnode, context));
-
-			// options._diff
-			if (options.__b) options.__b(vnode);
-
-			// options._render
-			let renderHook = options.__r;
-
-			if (
-				!nodeName.prototype ||
-				typeof nodeName.prototype.render !== 'function'
-			) {
-				let cctx = getContext(nodeName, context);
-
-				// If a hook invokes setState() to invalidate the component during rendering,
-				// re-render it up to 25 times to allow "settling" of memoized states.
-				// Note:
-				//   This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
-				//   https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
-				let count = 0;
-				while (c.__d && count++ < 25) {
-					c.__d = false;
-
-					if (renderHook) renderHook(vnode);
-
-					// stateless functional components
-					rendered = nodeName.call(vnode.__c, props, cctx);
-				}
-			} else {
-				let cctx = getContext(nodeName, context);
-
-				// c = new nodeName(props, context);
-				c = vnode.__c = new nodeName(props, cctx);
-				c.__v = vnode;
-				// turn off stateful re-rendering:
-				c._dirty = c.__d = true;
-				c.props = props;
-				if (c.state == null) c.state = {};
-
-				if (c._nextState == null && c.__s == null) {
-					c._nextState = c.__s = c.state;
-				}
+		}
 
-				c.context = cctx;
-				if (nodeName.getDerivedStateFromProps)
-					c.state = Object.assign(
-						{},
-						c.state,
-						nodeName.getDerivedStateFromProps(c.props, c.state)
-					);
-				else if (c.componentWillMount) {
-					c.componentWillMount();
-
-					// If the user called setState in cWM we need to flush pending,
-					// state updates. This is the same behaviour in React.
-					c.state =
-						c._nextState !== c.state
-							? c._nextState
-							: c.__s !== c.state
-							? c.__s
-							: c.state;
-				}
+		if (options[DIFF]) options[DIFF](vnode);
 
-				if (renderHook) renderHook(vnode);
+		let rendered;
+		if (nodeName.prototype && typeof nodeName.prototype.render === 'function') {
+			rendered = renderClassComponent(vnode, context);
+		} else {
+			rendered = renderFunctionComponent(vnode, context);
+		}
 
-				rendered = c.render(c.props, c.state, c.context);
-			}
+		let component = vnode[COMPONENT];
+		if (component.getChildContext) {
+			context = assign({}, context, component.getChildContext());
+		}
 
-			if (c.getChildContext) {
-				context = Object.assign({}, context, c.getChildContext());
-			}
+		if (options[DIFFED]) options[DIFFED](vnode);
 
-			if (options.diffed) options.diffed(vnode);
-			return _renderToStringPretty(
-				rendered,
-				context,
-				opts,
-				opts.shallowHighOrder !== false,
-				isSvgMode,
-				selectValue
-			);
-		}
+		return _renderToString(rendered, context, isSvgMode, selectValue);
 	}
 
 	// render JSX to HTML
-	let s = '<' + nodeName,
-		propChildren,
+	let s = '<',
+		children,
 		html;
 
-	if (props) {
-		let attrs = Object.keys(props);
-
-		// allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai)
-		if (opts && opts.sortAttributes === true) attrs.sort();
+	s = s + nodeName;
 
-		for (let i = 0; i < attrs.length; i++) {
-			let name = attrs[i],
-				v = props[name];
-			if (name === 'children') {
-				propChildren = v;
-				continue;
-			}
+	if (props) {
+		children = props.children;
+		for (let name in props) {
+			let v = props[name];
 
-			if (UNSAFE_NAME.test(name)) continue;
+			// switch (name) {
+			// 	case 'className':
+			// 		if ('class' in props) continue;
+			// 		name = 'class';
+			// 		break;
+			// 	case 'htmlFor':
+			// 		if ('for' in props) continue;
+			// 		name = 'for';
+			// 		break;
+			// 	case 'defaultValue':
+			// 		name = 'value';
+			// 		break;
+			// 	case 'defaultChecked':
+			// 		name = 'checked';
+			// 		break;
+			// 	case 'defaultSelected':
+			// 		name = 'selected';
+			// 		break;
+			// 	case 'key':
+			// 	case 'ref':
+			// 	case '__self':
+			// 	case '__source':
+			// 	case 'children':
+			// 		continue;
+			// 	default:
+			// 		if (isSvgMode && XLINK.test(name)) {
+			// 			name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
+			// 		}
+			// }
 
 			if (
-				!(opts && opts.allAttributes) &&
-				(name === 'key' ||
-					name === 'ref' ||
-					name === '__self' ||
-					name === '__source')
-			)
+				name === 'key' ||
+				name === 'ref' ||
+				name === '__self' ||
+				name === '__source' ||
+				name === 'children' ||
+				(name === 'className' && 'class' in props) ||
+				(name === 'htmlFor' && 'for' in props)
+			) {
 				continue;
-
-			if (name === 'defaultValue') {
-				name = 'value';
-			} else if (name === 'defaultChecked') {
-				name = 'checked';
-			} else if (name === 'defaultSelected') {
-				name = 'selected';
-			} else if (name === 'className') {
-				if (typeof props.class !== 'undefined') continue;
-				name = 'class';
-			} else if (isSvgMode && XLINK.test(name)) {
-				name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
-			}
-
-			if (name === 'htmlFor') {
-				if (props.for) continue;
-				name = 'for';
 			}
 
-			if (name === 'style' && v && typeof v === 'object') {
-				v = styleObjToCss(v);
-			}
-
-			// always use string values instead of booleans for aria attributes
-			// also see https://github.com/preactjs/preact/pull/2347/files
-			if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') {
-				v = String(v);
-			}
+			if (UNSAFE_NAME.test(name)) continue;
 
-			let hooked =
-				opts.attributeHook &&
-				opts.attributeHook(name, v, context, opts, isComponent);
-			if (hooked || hooked === '') {
-				s = s + hooked;
-				continue;
-			}
+			name = normalizePropName(name, isSvgMode);
+			v = normalizePropValue(name, v);
 
 			if (name === 'dangerouslySetInnerHTML') {
 				html = v && v.__html;
 			} else if (nodeName === 'textarea' && name === 'value') {
 				// <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
-				propChildren = v;
+				children = v;
+				// html = encodeEntities(v);
 			} else if ((v || v === 0 || v === '') && typeof v !== 'function') {
 				if (v === true || v === '') {
 					v = name;
-					// in non-xml mode, allow boolean attributes
-					if (!opts || !opts.xml) {
-						s = s + ' ' + name;
-						continue;
-					}
+					s = s + ' ' + name;
+					continue;
 				}
 
 				if (name === 'value') {
@@ -614,139 +315,82 @@ function _renderToStringPretty(
 						nodeName === 'option' &&
 						selectValue == v &&
 						// and the <option> doesn't already have a selected attribute on it
-						typeof props.selected === 'undefined'
+						!('selected' in props)
 					) {
-						s = s + ` selected`;
+						s = s + ' selected';
 					}
 				}
-				s = s + ` ${name}="${encodeEntities(v)}"`;
+				s = s + ' ' + name + '="' + encodeEntities(v) + '"';
 			}
 		}
 	}
 
-	// account for >1 multiline attribute
-	if (pretty) {
-		let sub = s.replace(/\n\s*/, ' ');
-		if (sub !== s && !~sub.indexOf('\n')) s = sub;
-		else if (pretty && ~s.indexOf('\n')) s = s + '\n';
-	}
-
+	let startElement = s;
 	s = s + '>';
 
-	if (UNSAFE_NAME.test(nodeName))
+	if (UNSAFE_NAME.test(nodeName)) {
 		throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`);
+	}
 
-	let isVoid =
-		VOID_ELEMENTS.test(nodeName) ||
-		(opts.voidElements && opts.voidElements.test(nodeName));
-	let pieces = [];
+	let pieces = '';
+	let hasChildren = false;
 
-	let children;
+	// let children = isArray(propChildren)
+	// 	? propChildren
+	// 	: propChildren != null
+	// 	? [propChildren]
+	// 	: undefined;
 	if (html) {
-		// if multiline, indent.
-		if (pretty && isLargeString(html)) {
-			html = '\n' + indentChar + indent(html, indentChar);
-		}
-		s = s + html;
-	} else if (
-		propChildren != null &&
-		getChildren((children = []), propChildren).length
-	) {
-		let hasLarge = pretty && ~s.indexOf('\n');
-		let lastWasText = false;
-
+		// return s + html + '</' + nodeName + '>';
+		// s = s + html;
+		pieces = pieces + html;
+		hasChildren = true;
+	} else if (typeof children === 'string') {
+		// s = s + encodeEntities(children);
+		pieces = pieces + encodeEntities(children);
+		hasChildren = true;
+	} else if (isArray(children)) {
 		for (let i = 0; i < children.length; i++) {
 			let child = children[i];
 
 			if (child != null && child !== false) {
 				let childSvgMode =
-						nodeName === 'svg'
-							? true
-							: nodeName === 'foreignObject'
-							? false
-							: isSvgMode,
-					ret = _renderToStringPretty(
-						child,
-						context,
-						opts,
-						true,
-						childSvgMode,
-						selectValue
-					);
-
-				if (pretty && !hasLarge && isLargeString(ret)) hasLarge = true;
+					nodeName === 'svg' || (nodeName !== 'foreignObject' && isSvgMode);
+				let ret = _renderToString(child, context, childSvgMode, selectValue);
 
 				// Skip if we received an empty string
 				if (ret) {
-					if (pretty) {
-						let isText = ret.length > 0 && ret[0] != '<';
-
-						// We merge adjacent text nodes, otherwise each piece would be printed
-						// on a new line.
-						if (lastWasText && isText) {
-							pieces[pieces.length - 1] += ret;
-						} else {
-							pieces.push(ret);
-						}
-
-						lastWasText = isText;
-					} else {
-						pieces.push(ret);
-					}
+					// s = s + ret;
+					pieces = pieces + ret;
+					hasChildren = true;
 				}
 			}
 		}
-		if (pretty && hasLarge) {
-			for (let i = pieces.length; i--; ) {
-				pieces[i] = '\n' + indentChar + indent(pieces[i], indentChar);
-			}
+	} else if (children != null && children !== false && children !== true) {
+		let childSvgMode =
+			nodeName === 'svg' || (nodeName !== 'foreignObject' && isSvgMode);
+		let ret = _renderToString(children, context, childSvgMode, selectValue);
+
+		// Skip if we received an empty string
+		if (ret) {
+			// s = s + ret;
+			pieces = pieces + ret;
+			hasChildren = true;
 		}
 	}
 
-	if (pieces.length || html) {
-		s = s + pieces.join('');
-	} else if (opts && opts.xml) {
-		return s.substring(0, s.length - 1) + ' />';
-	}
-
-	if (isVoid && !children && !html) {
-		s = s.replace(/>$/, ' />');
-	} else {
-		if (pretty && ~s.indexOf('\n')) s = s + '\n';
-		s = s + `</${nodeName}>`;
+	if (hasChildren) {
+		s = s + pieces;
+		// return s + pieces + '</' + nodeName + '>';
+	} else if (VOID_ELEMENTS.test(nodeName)) {
+		return startElement + ' />';
 	}
 
-	return s;
+	return s + '</' + nodeName + '>';
 }
 
-function getComponentName(component) {
-	return (
-		component.displayName ||
-		(component !== Function && component.name) ||
-		getFallbackComponentName(component)
-	);
-}
+/** The default export is an alias of `render()`. */
 
-function getFallbackComponentName(component) {
-	let str = Function.prototype.toString.call(component),
-		name = (str.match(/^\s*function\s+([^( ]+)/) || '')[1];
-	if (!name) {
-		// search for an existing indexed name for the given component:
-		let index = -1;
-		for (let i = UNNAMED.length; i--; ) {
-			if (UNNAMED[i] === component) {
-				index = i;
-				break;
-			}
-		}
-		// not found, create a new indexed name:
-		if (index < 0) {
-			index = UNNAMED.push(component) - 1;
-		}
-		name = `UnnamedComponent${index}`;
-	}
-	return name;
-}
 renderToString.shallowRender = shallowRender;
 
 export default renderToString;
diff --git a/src/jsx.js b/src/jsx.js
index f2ac1c7..64e6e15 100644
--- a/src/jsx.js
+++ b/src/jsx.js
@@ -1,6 +1,6 @@
 import './polyfills';
 import renderToString from './index';
-import { indent, encodeEntities, assign } from './util';
+import { indent, encodeEntities } from './util';
 import prettyFormat from 'pretty-format';
 
 // we have to patch in Array support, Possible issue in npm.im/pretty-format
@@ -68,7 +68,7 @@ let defaultOpts = {
 };
 
 function renderToJsxString(vnode, context, opts, inner) {
-	opts = assign(assign({}, defaultOpts), opts || {});
+	opts = Object.assign({}, defaultOpts, opts || {});
 	return renderToString(vnode, context, opts, inner);
 }
 
diff --git a/src/pretty.js b/src/pretty.js
new file mode 100644
index 0000000..4d85cbc
--- /dev/null
+++ b/src/pretty.js
@@ -0,0 +1,385 @@
+import {
+	encodeEntities,
+	indent,
+	isLargeString,
+	styleObjToCss,
+	getChildren,
+	createComponent,
+	getContext,
+	UNSAFE_NAME,
+	XLINK,
+	VOID_ELEMENTS
+} from './util';
+import { options, Fragment } from 'preact';
+
+// components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names.
+const UNNAMED = [];
+
+export function _renderToStringPretty(
+	vnode,
+	context,
+	opts,
+	inner,
+	isSvgMode,
+	selectValue
+) {
+	if (vnode == null || typeof vnode === 'boolean') {
+		return '';
+	}
+
+	// #text nodes
+	if (typeof vnode !== 'object') {
+		return encodeEntities(vnode);
+	}
+
+	let pretty = opts.pretty,
+		indentChar = pretty && typeof pretty === 'string' ? pretty : '\t';
+
+	if (Array.isArray(vnode)) {
+		let rendered = '';
+		for (let i = 0; i < vnode.length; i++) {
+			if (pretty && i > 0) rendered = rendered + '\n';
+			rendered =
+				rendered +
+				_renderToStringPretty(
+					vnode[i],
+					context,
+					opts,
+					inner,
+					isSvgMode,
+					selectValue
+				);
+		}
+		return rendered;
+	}
+
+	let nodeName = vnode.type,
+		props = vnode.props,
+		isComponent = false;
+
+	// components
+	if (typeof nodeName === 'function') {
+		isComponent = true;
+		if (opts.shallow && (inner || opts.renderRootComponent === false)) {
+			nodeName = getComponentName(nodeName);
+		} else if (nodeName === Fragment) {
+			const children = [];
+			getChildren(children, vnode.props.children);
+			return _renderToStringPretty(
+				children,
+				context,
+				opts,
+				opts.shallowHighOrder !== false,
+				isSvgMode,
+				selectValue
+			);
+		} else {
+			let rendered;
+
+			let c = (vnode.__c = createComponent(vnode, context));
+
+			// options._diff
+			if (options.__b) options.__b(vnode);
+
+			// options._render
+			let renderHook = options.__r;
+
+			if (
+				!nodeName.prototype ||
+				typeof nodeName.prototype.render !== 'function'
+			) {
+				let cctx = getContext(nodeName, context);
+
+				// If a hook invokes setState() to invalidate the component during rendering,
+				// re-render it up to 25 times to allow "settling" of memoized states.
+				// Note:
+				//   This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
+				//   https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
+				let count = 0;
+				while (c.__d && count++ < 25) {
+					c.__d = false;
+
+					if (renderHook) renderHook(vnode);
+
+					// stateless functional components
+					rendered = nodeName.call(vnode.__c, props, cctx);
+				}
+			} else {
+				let cctx = getContext(nodeName, context);
+
+				// c = new nodeName(props, context);
+				c = vnode.__c = new nodeName(props, cctx);
+				c.__v = vnode;
+				// turn off stateful re-rendering:
+				c._dirty = c.__d = true;
+				c.props = props;
+				if (c.state == null) c.state = {};
+
+				if (c._nextState == null && c.__s == null) {
+					c._nextState = c.__s = c.state;
+				}
+
+				c.context = cctx;
+				if (nodeName.getDerivedStateFromProps)
+					c.state = Object.assign(
+						{},
+						c.state,
+						nodeName.getDerivedStateFromProps(c.props, c.state)
+					);
+				else if (c.componentWillMount) {
+					c.componentWillMount();
+
+					// If the user called setState in cWM we need to flush pending,
+					// state updates. This is the same behaviour in React.
+					c.state =
+						c._nextState !== c.state
+							? c._nextState
+							: c.__s !== c.state
+							? c.__s
+							: c.state;
+				}
+
+				if (renderHook) renderHook(vnode);
+
+				rendered = c.render(c.props, c.state, c.context);
+			}
+
+			if (c.getChildContext) {
+				context = Object.assign({}, context, c.getChildContext());
+			}
+
+			if (options.diffed) options.diffed(vnode);
+			return _renderToStringPretty(
+				rendered,
+				context,
+				opts,
+				opts.shallowHighOrder !== false,
+				isSvgMode,
+				selectValue
+			);
+		}
+	}
+
+	// render JSX to HTML
+	let s = '<' + nodeName,
+		propChildren,
+		html;
+
+	if (props) {
+		let attrs = Object.keys(props);
+
+		// allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai)
+		if (opts && opts.sortAttributes === true) attrs.sort();
+
+		for (let i = 0; i < attrs.length; i++) {
+			let name = attrs[i],
+				v = props[name];
+			if (name === 'children') {
+				propChildren = v;
+				continue;
+			}
+
+			if (UNSAFE_NAME.test(name)) continue;
+
+			if (
+				!(opts && opts.allAttributes) &&
+				(name === 'key' ||
+					name === 'ref' ||
+					name === '__self' ||
+					name === '__source')
+			)
+				continue;
+
+			if (name === 'defaultValue') {
+				name = 'value';
+			} else if (name === 'defaultChecked') {
+				name = 'checked';
+			} else if (name === 'defaultSelected') {
+				name = 'selected';
+			} else if (name === 'className') {
+				if (typeof props.class !== 'undefined') continue;
+				name = 'class';
+			} else if (isSvgMode && XLINK.test(name)) {
+				name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
+			}
+
+			if (name === 'htmlFor') {
+				if (props.for) continue;
+				name = 'for';
+			}
+
+			if (name === 'style' && v && typeof v === 'object') {
+				v = styleObjToCss(v);
+			}
+
+			// always use string values instead of booleans for aria attributes
+			// also see https://github.com/preactjs/preact/pull/2347/files
+			if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') {
+				v = String(v);
+			}
+
+			let hooked =
+				opts.attributeHook &&
+				opts.attributeHook(name, v, context, opts, isComponent);
+			if (hooked || hooked === '') {
+				s = s + hooked;
+				continue;
+			}
+
+			if (name === 'dangerouslySetInnerHTML') {
+				html = v && v.__html;
+			} else if (nodeName === 'textarea' && name === 'value') {
+				// <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
+				propChildren = v;
+			} else if ((v || v === 0 || v === '') && typeof v !== 'function') {
+				if (v === true || v === '') {
+					v = name;
+					// in non-xml mode, allow boolean attributes
+					if (!opts || !opts.xml) {
+						s = s + ' ' + name;
+						continue;
+					}
+				}
+
+				if (name === 'value') {
+					if (nodeName === 'select') {
+						selectValue = v;
+						continue;
+					} else if (
+						// If we're looking at an <option> and it's the currently selected one
+						nodeName === 'option' &&
+						selectValue == v &&
+						// and the <option> doesn't already have a selected attribute on it
+						typeof props.selected === 'undefined'
+					) {
+						s = s + ` selected`;
+					}
+				}
+				s = s + ` ${name}="${encodeEntities(v)}"`;
+			}
+		}
+	}
+
+	// account for >1 multiline attribute
+	if (pretty) {
+		let sub = s.replace(/\n\s*/, ' ');
+		if (sub !== s && !~sub.indexOf('\n')) s = sub;
+		else if (pretty && ~s.indexOf('\n')) s = s + '\n';
+	}
+
+	s = s + '>';
+
+	if (UNSAFE_NAME.test(nodeName))
+		throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`);
+
+	let isVoid =
+		VOID_ELEMENTS.test(nodeName) ||
+		(opts.voidElements && opts.voidElements.test(nodeName));
+	let pieces = [];
+
+	let children;
+	if (html) {
+		// if multiline, indent.
+		if (pretty && isLargeString(html)) {
+			html = '\n' + indentChar + indent(html, indentChar);
+		}
+		s = s + html;
+	} else if (
+		propChildren != null &&
+		getChildren((children = []), propChildren).length
+	) {
+		let hasLarge = pretty && ~s.indexOf('\n');
+		let lastWasText = false;
+
+		for (let i = 0; i < children.length; i++) {
+			let child = children[i];
+
+			if (child != null && child !== false) {
+				let childSvgMode =
+						nodeName === 'svg'
+							? true
+							: nodeName === 'foreignObject'
+							? false
+							: isSvgMode,
+					ret = _renderToStringPretty(
+						child,
+						context,
+						opts,
+						true,
+						childSvgMode,
+						selectValue
+					);
+
+				if (pretty && !hasLarge && isLargeString(ret)) hasLarge = true;
+
+				// Skip if we received an empty string
+				if (ret) {
+					if (pretty) {
+						let isText = ret.length > 0 && ret[0] != '<';
+
+						// We merge adjacent text nodes, otherwise each piece would be printed
+						// on a new line.
+						if (lastWasText && isText) {
+							pieces[pieces.length - 1] += ret;
+						} else {
+							pieces.push(ret);
+						}
+
+						lastWasText = isText;
+					} else {
+						pieces.push(ret);
+					}
+				}
+			}
+		}
+		if (pretty && hasLarge) {
+			for (let i = pieces.length; i--; ) {
+				pieces[i] = '\n' + indentChar + indent(pieces[i], indentChar);
+			}
+		}
+	}
+
+	if (pieces.length || html) {
+		s = s + pieces.join('');
+	} else if (opts && opts.xml) {
+		return s.substring(0, s.length - 1) + ' />';
+	}
+
+	if (isVoid && !children && !html) {
+		s = s.replace(/>$/, ' />');
+	} else {
+		if (pretty && ~s.indexOf('\n')) s = s + '\n';
+		s = s + `</${nodeName}>`;
+	}
+
+	return s;
+}
+
+function getComponentName(component) {
+	return (
+		component.displayName ||
+		(component !== Function && component.name) ||
+		getFallbackComponentName(component)
+	);
+}
+
+function getFallbackComponentName(component) {
+	let str = Function.prototype.toString.call(component),
+		name = (str.match(/^\s*function\s+([^( ]+)/) || '')[1];
+	if (!name) {
+		// search for an existing indexed name for the given component:
+		let index = -1;
+		for (let i = UNNAMED.length; i--; ) {
+			if (UNNAMED[i] === component) {
+				index = i;
+				break;
+			}
+		}
+		// not found, create a new indexed name:
+		if (index < 0) {
+			index = UNNAMED.push(component) - 1;
+		}
+		name = `UnnamedComponent${index}`;
+	}
+	return name;
+}
diff --git a/src/util.js b/src/util.js
index dac8c05..5da7f18 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,7 +1,10 @@
 // DOM properties that should NOT have "px" added when numeric
 export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i;
+export const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
+export const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
+export const XLINK = /^xlink:?./;
 
-const ENCODED_ENTITIES = /[&<>"]/;
+const ENCODED_ENTITIES = /["&<]/;
 
 export function encodeEntities(str) {
 	// Ensure we're always parsing and returning a string:
@@ -16,21 +19,28 @@ export function encodeEntities(str) {
 		ch = '';
 
 	// Seek forward in str until the next entity char:
-	for (; i<str.length; i++) {
+	for (; i < str.length; i++) {
 		switch (str.charCodeAt(i)) {
-			case 60: ch = '&lt;'; break;
-			case 62: ch = '&gt;'; break;
-			case 34: ch = '&quot;'; break;
-			case 38: ch = '&amp;'; break;
-			default: continue;
+			case 34:
+				ch = '&quot;';
+				break;
+			case 38:
+				ch = '&amp;';
+				break;
+			case 60:
+				ch = '&lt;';
+				break;
+			default:
+				continue;
 		}
 		// Append skipped/buffered characters and the encoded entity:
-		if (i > last) out += str.slice(last, i);
+		if (i !== last) out += str.slice(last, i);
 		out += ch;
 		// Start the next seek/buffer after the entity's offset:
 		last = i + 1;
 	}
-	return out + str.slice(last, i);
+	if (i !== last) out += str.slice(last, i);
+	return out;
 }
 
 export let indent = (s, char) =>
@@ -43,6 +53,7 @@ export let isLargeString = (s, length, ignoreLines) =>
 
 const JS_TO_CSS = {};
 
+const CSS_REGEX = /([A-Z])/g;
 // Convert an Object style to a CSSText string
 export function styleObjToCss(s) {
 	let str = '';
@@ -55,29 +66,18 @@ export function styleObjToCss(s) {
 				prop[0] == '-'
 					? prop
 					: JS_TO_CSS[prop] ||
-					  (JS_TO_CSS[prop] = prop.replace(/([A-Z])/g, '-$1').toLowerCase());
-			str += ': ';
-			str += val;
+					  (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$1').toLowerCase());
+
 			if (typeof val === 'number' && IS_NON_DIMENSIONAL.test(prop) === false) {
-				str += 'px';
+				str = str + ': ' + val + 'px;';
+			} else {
+				str = str + ': ' + val + ';';
 			}
-			str += ';';
 		}
 	}
 	return str || undefined;
 }
 
-/**
- * Copy all properties from `props` onto `obj`.
- * @param {object} obj Object onto which properties should be copied.
- * @param {object} props Object from which to copy properties.
- * @returns {object}
- * @private
- */
-export function assign(obj, props) {
-	return Object.assign(obj, props);
-}
-
 /**
  * Get flattened children from the children prop
  * @param {Array} accumulator
@@ -93,3 +93,33 @@ export function getChildren(accumulator, children) {
 	}
 	return accumulator;
 }
+
+function markAsDirty() {
+	this.__d = true;
+}
+
+export function createComponent(vnode, context) {
+	return {
+		__v: vnode,
+		context,
+		props: vnode.props,
+		// silently drop state updates
+		setState: markAsDirty,
+		forceUpdate: markAsDirty,
+		__d: true,
+		// hooks
+		__h: []
+	};
+}
+
+// Necessary for createContext api. Setting this property will pass
+// the context value as `this.context` just for this component.
+export function getContext(nodeName, context) {
+	let cxType = nodeName.contextType;
+	let provider = cxType && context[cxType.__c];
+	return cxType != null
+		? provider
+			? provider.props.value
+			: cxType.__
+		: context;
+}


Test patch:
diff --git a/test/render.test.js b/test/render.test.js
index fe3e9bf..f89a600 100644
--- a/test/render.test.js
+++ b/test/render.test.js
@@ -177,7 +177,7 @@ describe('render', () => {
 
 		it('should encode entities', () => {
 			let rendered = render(<div a={'"<>&'}>{'"<>&'}</div>),
-				expected = `<div a="&quot;&lt;&gt;&amp;">&quot;&lt;&gt;&amp;</div>`;
+				expected = `<div a="&quot;&lt;>&amp;">&quot;&lt;>&amp;</div>`;
 
 			expect(rendered).to.equal(expected);
 		});
@@ -871,7 +871,7 @@ describe('render', () => {
 	});
 
 	describe('state locking', () => {
-		it('should set _dirty and __d to true', () => {
+		it('should set __d (_dirty) to true', () => {
 			let inst;
 			class Foo extends Component {
 				constructor(props, context) {
@@ -885,7 +885,7 @@ describe('render', () => {
 
 			expect(render(<Foo />)).to.equal('<div></div>');
 
-			expect(inst).to.have.property('_dirty', true);
+			// expect(inst).to.have.property('_dirty', true);
 			expect(inst).to.have.property('__d', true);
 		});