From 0f677c009d102bc12232a966634136be58f34739 Mon Sep 17 00:00:00 2001
From: Bjorn Lu <bjornlu.dev@gmail.com>
Date: Tue, 25 Jul 2023 23:44:25 +0800
Subject: [PATCH] Refactor Astro rendering to write results directly (#7782)

---
 .changeset/lemon-snakes-invite.md             |   5 +
 packages/astro/src/core/render/result.ts      |  12 +-
 packages/astro/src/runtime/server/index.ts    |   3 -
 packages/astro/src/runtime/server/jsx.ts      |  20 +-
 .../astro/src/runtime/server/render/any.ts    |  40 ++-
 .../src/runtime/server/render/astro/index.ts  |   6 +-
 .../runtime/server/render/astro/instance.ts   |  16 +-
 .../server/render/astro/render-template.ts    |  45 +---
 .../src/runtime/server/render/astro/render.ts |  34 +--
 .../astro/src/runtime/server/render/common.ts |  58 ++--
 .../src/runtime/server/render/component.ts    | 250 +++++++++++++-----
 .../astro/src/runtime/server/render/dom.ts    |   2 +-
 .../astro/src/runtime/server/render/index.ts  |  11 +-
 .../astro/src/runtime/server/render/page.ts   |  94 ++-----
 .../astro/src/runtime/server/render/slot.ts   |  43 +--
 .../astro/src/runtime/server/render/util.ts   | 142 ----------
 packages/astro/test/streaming.test.js         |   2 +-
 17 files changed, 322 insertions(+), 461 deletions(-)
 create mode 100644 .changeset/lemon-snakes-invite.md

diff --git a/.changeset/lemon-snakes-invite.md b/.changeset/lemon-snakes-invite.md
new file mode 100644
index 0000000000..49bb98510a
--- /dev/null
+++ b/.changeset/lemon-snakes-invite.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Refactor Astro rendering to write results directly. This improves the rendering performance for all Astro files.
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index d86cce3482..46bae11284 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -7,16 +7,12 @@ import type {
 	SSRLoadedRenderer,
 	SSRResult,
 } from '../../@types/astro';
-import { isHTMLString } from '../../runtime/server/escape.js';
-import {
-	renderSlotToString,
-	stringifyChunk,
-	type ComponentSlots,
-} from '../../runtime/server/index.js';
+import { renderSlotToString, type ComponentSlots } from '../../runtime/server/index.js';
 import { renderJSX } from '../../runtime/server/jsx.js';
 import { AstroCookies } from '../cookies/index.js';
 import { AstroError, AstroErrorData } from '../errors/index.js';
 import { warn, type LogOptions } from '../logger/core.js';
+import { chunkToString } from '../../runtime/server/render/index.js';
 
 const clientAddressSymbol = Symbol.for('astro.clientAddress');
 const responseSentSymbol = Symbol.for('astro.responseSent');
@@ -112,7 +108,7 @@ class Slots {
 			const expression = getFunctionExpression(component);
 			if (expression) {
 				const slot = async () =>
-					isHTMLString(await expression) ? expression : expression(...args);
+					typeof expression === 'function' ? expression(...args) : expression;
 				return await renderSlotToString(result, slot).then((res) => {
 					return res != null ? String(res) : res;
 				});
@@ -126,7 +122,7 @@ class Slots {
 		}
 
 		const content = await renderSlotToString(result, this.#slots[name]);
-		const outHTML = stringifyChunk(result, content);
+		const outHTML = chunkToString(result, content);
 
 		return outHTML;
 	}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 1a03a507b6..aca260d009 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -17,9 +17,7 @@ export {
 	Fragment,
 	maybeRenderHead,
 	renderTemplate as render,
-	renderAstroTemplateResult as renderAstroComponent,
 	renderComponent,
-	renderComponentToIterable,
 	Renderer as Renderer,
 	renderHead,
 	renderHTMLElement,
@@ -30,7 +28,6 @@ export {
 	renderTemplate,
 	renderToString,
 	renderUniqueStylesheet,
-	stringifyChunk,
 	voidElementNames,
 } from './render/index.js';
 export type {
diff --git a/packages/astro/src/runtime/server/jsx.ts b/packages/astro/src/runtime/server/jsx.ts
index 48f879b104..d2cb87a614 100644
--- a/packages/astro/src/runtime/server/jsx.ts
+++ b/packages/astro/src/runtime/server/jsx.ts
@@ -5,13 +5,11 @@ import {
 	HTMLString,
 	escapeHTML,
 	markHTMLString,
-	renderComponentToIterable,
 	renderToString,
 	spreadAttributes,
 	voidElementNames,
 } from './index.js';
-import { HTMLParts } from './render/common.js';
-import type { ComponentIterable } from './render/component';
+import { renderComponentToString } from './render/component.js';
 
 const ClientOnlyPlaceholder = 'astro-client-only';
 
@@ -177,9 +175,9 @@ Did you forget to import the component or is it possible there is a typo?`);
 			await Promise.all(slotPromises);
 
 			props[Skip.symbol] = skip;
-			let output: ComponentIterable;
+			let output: string;
 			if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {
-				output = await renderComponentToIterable(
+				output = await renderComponentToString(
 					result,
 					vnode.props['client:display-name'] ?? '',
 					null,
@@ -187,7 +185,7 @@ Did you forget to import the component or is it possible there is a typo?`);
 					slots
 				);
 			} else {
-				output = await renderComponentToIterable(
+				output = await renderComponentToString(
 					result,
 					typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
 					vnode.type,
@@ -195,15 +193,7 @@ Did you forget to import the component or is it possible there is a typo?`);
 					slots
 				);
 			}
-			if (typeof output !== 'string' && Symbol.asyncIterator in output) {
-				let parts = new HTMLParts();
-				for await (const chunk of output) {
-					parts.append(chunk, result);
-				}
-				return markHTMLString(parts.toString());
-			} else {
-				return markHTMLString(output);
-			}
+			return markHTMLString(output);
 		}
 	}
 	// numbers, plain objects, etc
diff --git a/packages/astro/src/runtime/server/render/any.ts b/packages/astro/src/runtime/server/render/any.ts
index 4ee947ee6e..7c181fecbe 100644
--- a/packages/astro/src/runtime/server/render/any.ts
+++ b/packages/astro/src/runtime/server/render/any.ts
@@ -1,47 +1,43 @@
 import { escapeHTML, isHTMLString, markHTMLString } from '../escape.js';
-import {
-	isAstroComponentInstance,
-	isRenderTemplateResult,
-	renderAstroTemplateResult,
-} from './astro/index.js';
+import { isAstroComponentInstance, isRenderTemplateResult } from './astro/index.js';
+import { isRenderInstance, type RenderDestination } from './common.js';
 import { SlotString } from './slot.js';
-import { bufferIterators } from './util.js';
 
-export async function* renderChild(child: any): AsyncIterable<any> {
+export async function renderChild(destination: RenderDestination, child: any) {
 	child = await child;
 	if (child instanceof SlotString) {
-		if (child.instructions) {
-			yield* child.instructions;
-		}
-		yield child;
+		destination.write(child);
 	} else if (isHTMLString(child)) {
-		yield child;
+		destination.write(child);
 	} else if (Array.isArray(child)) {
-		const bufferedIterators = bufferIterators(child.map((c) => renderChild(c)));
-		for (const value of bufferedIterators) {
-			yield markHTMLString(await value);
+		for (const c of child) {
+			await renderChild(destination, c);
 		}
 	} else if (typeof child === 'function') {
 		// Special: If a child is a function, call it automatically.
 		// This lets you do {() => ...} without the extra boilerplate
 		// of wrapping it in a function and calling it.
-		yield* renderChild(child());
+		await renderChild(destination, child());
 	} else if (typeof child === 'string') {
-		yield markHTMLString(escapeHTML(child));
+		destination.write(markHTMLString(escapeHTML(child)));
 	} else if (!child && child !== 0) {
 		// do nothing, safe to ignore falsey values.
+	} else if (isRenderInstance(child)) {
+		await child.render(destination);
 	} else if (isRenderTemplateResult(child)) {
-		yield* renderAstroTemplateResult(child);
+		await child.render(destination);
 	} else if (isAstroComponentInstance(child)) {
-		yield* child.render();
+		await child.render(destination);
 	} else if (ArrayBuffer.isView(child)) {
-		yield child;
+		destination.write(child);
 	} else if (
 		typeof child === 'object' &&
 		(Symbol.asyncIterator in child || Symbol.iterator in child)
 	) {
-		yield* child;
+		for await (const value of child) {
+			await renderChild(destination, value);
+		}
 	} else {
-		yield child;
+		destination.write(child);
 	}
 }
diff --git a/packages/astro/src/runtime/server/render/astro/index.ts b/packages/astro/src/runtime/server/render/astro/index.ts
index f7d9923ee5..d9283b9f93 100644
--- a/packages/astro/src/runtime/server/render/astro/index.ts
+++ b/packages/astro/src/runtime/server/render/astro/index.ts
@@ -3,9 +3,5 @@ export { isAstroComponentFactory } from './factory.js';
 export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
 export type { AstroComponentInstance } from './instance';
 export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
-export {
-	isRenderTemplateResult,
-	renderAstroTemplateResult,
-	renderTemplate,
-} from './render-template.js';
+export { isRenderTemplateResult, renderTemplate } from './render-template.js';
 export { renderToReadableStream, renderToString } from './render.js';
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index 527d4a8c67..e4df186c66 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -6,6 +6,7 @@ import { isPromise } from '../../util.js';
 import { renderChild } from '../any.js';
 import { isAPropagatingComponent } from './factory.js';
 import { isHeadAndContent } from './head-and-content.js';
+import type { RenderDestination } from '../common.js';
 
 type ComponentProps = Record<string | number, any>;
 
@@ -40,7 +41,7 @@ export class AstroComponentInstance {
 		return this.returnValue;
 	}
 
-	async *render() {
+	async render(destination: RenderDestination) {
 		if (this.returnValue === undefined) {
 			await this.init(this.result);
 		}
@@ -50,9 +51,9 @@ export class AstroComponentInstance {
 			value = await value;
 		}
 		if (isHeadAndContent(value)) {
-			yield* value.content;
+			await value.content.render(destination);
 		} else {
-			yield* renderChild(value);
+			await renderChild(destination, value);
 		}
 	}
 }
@@ -71,7 +72,7 @@ function validateComponentProps(props: any, displayName: string) {
 	}
 }
 
-export function createAstroComponentInstance(
+export async function createAstroComponentInstance(
 	result: SSRResult,
 	displayName: string,
 	factory: AstroComponentFactory,
@@ -80,9 +81,16 @@ export function createAstroComponentInstance(
 ) {
 	validateComponentProps(props, displayName);
 	const instance = new AstroComponentInstance(result, props, slots, factory);
+
 	if (isAPropagatingComponent(result, factory) && !result._metadata.propagators.has(factory)) {
 		result._metadata.propagators.set(factory, instance);
+		// Call component instances that might have head content to be propagated up.
+		const returnValue = await instance.init(result);
+		if (isHeadAndContent(returnValue)) {
+			result._metadata.extraHead.push(returnValue.head);
+		}
 	}
+
 	return instance;
 }
 
diff --git a/packages/astro/src/runtime/server/render/astro/render-template.ts b/packages/astro/src/runtime/server/render/astro/render-template.ts
index b0dbabdc19..1d5af33fc9 100644
--- a/packages/astro/src/runtime/server/render/astro/render-template.ts
+++ b/packages/astro/src/runtime/server/render/astro/render-template.ts
@@ -1,9 +1,7 @@
-import type { RenderInstruction } from '../types';
-
-import { HTMLBytes, markHTMLString } from '../../escape.js';
+import { markHTMLString } from '../../escape.js';
 import { isPromise } from '../../util.js';
 import { renderChild } from '../any.js';
-import { bufferIterators } from '../util.js';
+import type { RenderDestination } from '../common.js';
 
 const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult');
 
@@ -33,17 +31,15 @@ export class RenderTemplateResult {
 		});
 	}
 
-	async *[Symbol.asyncIterator]() {
-		const { htmlParts, expressions } = this;
+	async render(destination: RenderDestination) {
+		for (let i = 0; i < this.htmlParts.length; i++) {
+			const html = this.htmlParts[i];
+			const exp = this.expressions[i];
 
-		let iterables = bufferIterators(expressions.map((e) => renderChild(e)));
-		for (let i = 0; i < htmlParts.length; i++) {
-			const html = htmlParts[i];
-			const iterable = iterables[i];
-
-			yield markHTMLString(html);
-			if (iterable) {
-				yield* iterable;
+			destination.write(markHTMLString(html));
+			// Skip render if falsy, except the number 0
+			if (exp || exp === 0) {
+				await renderChild(destination, exp);
 			}
 		}
 	}
@@ -54,27 +50,6 @@ export function isRenderTemplateResult(obj: unknown): obj is RenderTemplateResul
 	return typeof obj === 'object' && !!(obj as any)[renderTemplateResultSym];
 }
 
-export async function* renderAstroTemplateResult(
-	component: RenderTemplateResult
-): AsyncIterable<string | HTMLBytes | RenderInstruction> {
-	for await (const value of component) {
-		if (value || value === 0) {
-			for await (const chunk of renderChild(value)) {
-				switch (chunk.type) {
-					case 'directive': {
-						yield chunk;
-						break;
-					}
-					default: {
-						yield markHTMLString(chunk);
-						break;
-					}
-				}
-			}
-		}
-	}
-}
-
 export function renderTemplate(htmlParts: TemplateStringsArray, ...expressions: any[]) {
 	return new RenderTemplateResult(htmlParts, expressions);
 }
diff --git a/packages/astro/src/runtime/server/render/astro/render.ts b/packages/astro/src/runtime/server/render/astro/render.ts
index 81b4375be0..89dc28b759 100644
--- a/packages/astro/src/runtime/server/render/astro/render.ts
+++ b/packages/astro/src/runtime/server/render/astro/render.ts
@@ -3,7 +3,7 @@ import { AstroError, AstroErrorData } from '../../../../core/errors/index.js';
 import { chunkToByteArray, chunkToString, encoder, type RenderDestination } from '../common.js';
 import type { AstroComponentFactory } from './factory.js';
 import { isHeadAndContent } from './head-and-content.js';
-import { isRenderTemplateResult, renderAstroTemplateResult } from './render-template.js';
+import { isRenderTemplateResult } from './render-template.js';
 
 // Calls a component and renders it into a string of HTML
 export async function renderToString(
@@ -46,9 +46,7 @@ export async function renderToString(
 		},
 	};
 
-	for await (const chunk of renderAstroTemplateResult(templateResult)) {
-		destination.write(chunk);
-	}
+	await templateResult.render(destination);
 
 	return str;
 }
@@ -73,10 +71,6 @@ export async function renderToReadableStream(
 	// If the Astro component returns a Response on init, return that response
 	if (templateResult instanceof Response) return templateResult;
 
-	if (isPage) {
-		await bufferHeadContent(result);
-	}
-
 	let renderedFirstPageChunk = false;
 
 	return new ReadableStream({
@@ -108,9 +102,7 @@ export async function renderToReadableStream(
 
 			(async () => {
 				try {
-					for await (const chunk of renderAstroTemplateResult(templateResult)) {
-						destination.write(chunk);
-					}
+					await templateResult.render(destination);
 					controller.close();
 				} catch (e) {
 					// We don't have a lot of information downstream, and upstream we can't catch the error properly
@@ -120,7 +112,9 @@ export async function renderToReadableStream(
 							file: route?.component,
 						});
 					}
-					controller.error(e);
+
+					// Queue error on next microtask to flush the remaining chunks written synchronously
+					setTimeout(() => controller.error(e), 0);
 				}
 			})();
 		},
@@ -150,19 +144,3 @@ async function callComponentAsTemplateResultOrResponse(
 
 	return isHeadAndContent(factoryResult) ? factoryResult.content : factoryResult;
 }
-
-// Recursively calls component instances that might have head content
-// to be propagated up.
-async function bufferHeadContent(result: SSRResult) {
-	const iterator = result._metadata.propagators.values();
-	while (true) {
-		const { value, done } = iterator.next();
-		if (done) {
-			break;
-		}
-		const returnValue = await value.init(result);
-		if (isHeadAndContent(returnValue)) {
-			result._metadata.extraHead.push(returnValue.head);
-		}
-	}
-}
diff --git a/packages/astro/src/runtime/server/render/common.ts b/packages/astro/src/runtime/server/render/common.ts
index 206f138cc8..48d8143df8 100644
--- a/packages/astro/src/runtime/server/render/common.ts
+++ b/packages/astro/src/runtime/server/render/common.ts
@@ -1,7 +1,7 @@
 import type { SSRResult } from '../../../@types/astro';
 import type { RenderInstruction } from './types.js';
 
-import { HTMLBytes, markHTMLString } from '../escape.js';
+import { HTMLBytes, HTMLString, markHTMLString } from '../escape.js';
 import {
 	determineIfNeedsHydrationScript,
 	determinesIfNeedsDirectiveScript,
@@ -11,12 +11,32 @@ import {
 import { renderAllHeadContent } from './head.js';
 import { isSlotString, type SlotString } from './slot.js';
 
+/**
+ * Possible chunk types to be written to the destination, and it'll
+ * handle stringifying them at the end.
+ *
+ * NOTE: Try to reduce adding new types here. If possible, serialize
+ * the custom types to a string in `renderChild` in `any.ts`.
+ */
+export type RenderDestinationChunk =
+	| string
+	| HTMLBytes
+	| HTMLString
+	| SlotString
+	| ArrayBufferView
+	| RenderInstruction
+	| Response;
+
 export interface RenderDestination {
 	/**
 	 * Any rendering logic should call this to construct the HTML output.
-	 * See the `chunk` parameter for possible writable values
+	 * See the `chunk` parameter for possible writable values.
 	 */
-	write(chunk: string | HTMLBytes | RenderInstruction | Response): void;
+	write(chunk: RenderDestinationChunk): void;
+}
+
+export interface RenderInstance {
+	render(destination: RenderDestination): Promise<void> | void;
 }
 
 export const Fragment = Symbol.for('astro:fragment');
@@ -28,9 +48,9 @@ export const decoder = new TextDecoder();
 // Rendering produces either marked strings of HTML or instructions for hydration.
 // These directive instructions bubble all the way up to renderPage so that we
 // can ensure they are added only once, and as soon as possible.
-export function stringifyChunk(
+function stringifyChunk(
 	result: SSRResult,
-	chunk: string | SlotString | RenderInstruction
+	chunk: string | HTMLString | SlotString | RenderInstruction
 ): string {
 	if (typeof (chunk as any).type === 'string') {
 		const instruction = chunk as RenderInstruction;
@@ -89,27 +109,7 @@ export function stringifyChunk(
 	}
 }
 
-export class HTMLParts {
-	public parts: string;
-	constructor() {
-		this.parts = '';
-	}
-	append(part: string | HTMLBytes | RenderInstruction, result: SSRResult) {
-		if (ArrayBuffer.isView(part)) {
-			this.parts += decoder.decode(part);
-		} else {
-			this.parts += stringifyChunk(result, part);
-		}
-	}
-	toString() {
-		return this.parts;
-	}
-	toArrayBuffer() {
-		return encoder.encode(this.parts);
-	}
-}
-
-export function chunkToString(result: SSRResult, chunk: string | HTMLBytes | RenderInstruction) {
+export function chunkToString(result: SSRResult, chunk: Exclude<RenderDestinationChunk, Response>) {
 	if (ArrayBuffer.isView(chunk)) {
 		return decoder.decode(chunk);
 	} else {
@@ -119,7 +119,7 @@ export function chunkToString(result: SSRResult, chunk: string | HTMLBytes | Ren
 
 export function chunkToByteArray(
 	result: SSRResult,
-	chunk: string | HTMLBytes | RenderInstruction
+	chunk: Exclude<RenderDestinationChunk, Response>
 ): Uint8Array {
 	if (ArrayBuffer.isView(chunk)) {
 		return chunk as Uint8Array;
@@ -129,3 +129,7 @@ export function chunkToByteArray(
 		return encoder.encode(stringified.toString());
 	}
 }
+
+export function isRenderInstance(obj: unknown): obj is RenderInstance {
+	return !!obj && typeof obj === 'object' && 'render' in obj && typeof obj.render === 'function';
+}
diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts
index 4eacafe806..de36b0ac91 100644
--- a/packages/astro/src/runtime/server/render/component.ts
+++ b/packages/astro/src/runtime/server/render/component.ts
@@ -1,4 +1,9 @@
-import type { AstroComponentMetadata, SSRLoadedRenderer, SSRResult } from '../../../@types/astro';
+import type {
+	AstroComponentMetadata,
+	RouteData,
+	SSRLoadedRenderer,
+	SSRResult,
+} from '../../../@types/astro';
 import type { RenderInstruction } from './types.js';
 
 import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
@@ -10,16 +15,23 @@ import { isPromise } from '../util.js';
 import {
 	createAstroComponentInstance,
 	isAstroComponentFactory,
-	isAstroComponentInstance,
-	renderAstroTemplateResult,
 	renderTemplate,
-	type AstroComponentInstance,
+	type AstroComponentFactory,
 } from './astro/index.js';
-import { Fragment, Renderer, stringifyChunk } from './common.js';
+import {
+	Fragment,
+	Renderer,
+	type RenderDestination,
+	chunkToString,
+	type RenderInstance,
+	type RenderDestinationChunk,
+} from './common.js';
 import { componentIsHTMLElement, renderHTMLElement } from './dom.js';
 import { renderSlotToString, renderSlots, type ComponentSlots } from './slot.js';
 import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
+import { maybeRenderHead } from './head.js';
 
+const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
 const rendererAliases = new Map([['solid', 'solid-js']]);
 
 function guessRenderers(componentUrl?: string): string[] {
@@ -67,7 +79,7 @@ async function renderFrameworkComponent(
 	Component: unknown,
 	_props: Record<string | number, any>,
 	slots: any = {}
-): Promise<ComponentIterable> {
+): Promise<RenderInstance> {
 	if (!Component && !_props['client:only']) {
 		throw new Error(
 			`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
@@ -134,9 +146,17 @@ async function renderFrameworkComponent(
 		}
 
 		if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
-			const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
-
-			return output;
+			const output = await renderHTMLElement(
+				result,
+				Component as typeof HTMLElement,
+				_props,
+				slots
+			);
+			return {
+				render(destination) {
+					destination.write(output);
+				},
+			};
 		}
 	} else {
 		// Attempt: use explicitly passed renderer name
@@ -253,33 +273,43 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
 		// Sanitize tag name because some people might try to inject attributes 🙄
 		const Tag = sanitizeElementName(Component);
 		const childSlots = Object.values(children).join('');
-		const iterable = renderAstroTemplateResult(
-			await renderTemplate`<${Tag}${internalSpreadAttributes(props)}${markHTMLString(
-				childSlots === '' && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
-			)}`
-		);
+
+		const renderTemplateResult = renderTemplate`<${Tag}${internalSpreadAttributes(
+			props
+		)}${markHTMLString(
+			childSlots === '' && voidElementNames.test(Tag) ? `/>` : `>${childSlots}</${Tag}>`
+		)}`;
+
 		html = '';
-		for await (const chunk of iterable) {
-			html += chunk;
-		}
+		const destination: RenderDestination = {
+			write(chunk) {
+				if (chunk instanceof Response) return;
+				html += chunkToString(result, chunk);
+			},
+		};
+		await renderTemplateResult.render(destination);
 	}
 
 	if (!hydration) {
-		return (async function* () {
-			if (slotInstructions) {
-				yield* slotInstructions;
-			}
-
-			if (isPage || renderer?.name === 'astro:jsx') {
-				yield html;
-			} else if (html && html.length > 0) {
-				yield markHTMLString(
-					removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false)
-				);
-			} else {
-				yield '';
-			}
-		})();
+		return {
+			render(destination) {
+				// If no hydration is needed, start rendering the html and return
+				if (slotInstructions) {
+					for (const instruction of slotInstructions) {
+						destination.write(instruction);
+					}
+				}
+				if (isPage || renderer?.name === 'astro:jsx') {
+					destination.write(html);
+				} else if (html && html.length > 0) {
+					destination.write(
+						markHTMLString(
+							removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false)
+						)
+					);
+				}
+			},
+		};
 	}
 
 	// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
@@ -332,15 +362,18 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
 		island.props['await-children'] = '';
 	}
 
-	async function* renderAll() {
-		if (slotInstructions) {
-			yield* slotInstructions;
-		}
-		yield { type: 'directive', hydration, result };
-		yield markHTMLString(renderElement('astro-island', island, false));
-	}
-
-	return renderAll();
+	return {
+		render(destination) {
+			// Render the html
+			if (slotInstructions) {
+				for (const instruction of slotInstructions) {
+					destination.write(instruction);
+				}
+			}
+			destination.write({ type: 'directive', hydration });
+			destination.write(markHTMLString(renderElement('astro-island', island, false)));
+		},
+	};
 }
 
 function sanitizeElementName(tag: string) {
@@ -349,12 +382,17 @@ function sanitizeElementName(tag: string) {
 	return tag.trim().split(unsafe)[0].trim();
 }
 
-async function renderFragmentComponent(result: SSRResult, slots: ComponentSlots = {}) {
+async function renderFragmentComponent(
+	result: SSRResult,
+	slots: ComponentSlots = {}
+): Promise<RenderInstance> {
 	const children = await renderSlotToString(result, slots?.default);
-	if (children == null) {
-		return children;
-	}
-	return markHTMLString(children);
+	return {
+		render(destination) {
+			if (children == null) return;
+			destination.write(children);
+		},
+	};
 }
 
 async function renderHTMLComponent(
@@ -362,54 +400,136 @@ async function renderHTMLComponent(
 	Component: unknown,
 	_props: Record<string | number, any>,
 	slots: any = {}
-) {
+): Promise<RenderInstance> {
 	const { slotInstructions, children } = await renderSlots(result, slots);
 	const html = (Component as any)({ slots: children });
 	const hydrationHtml = slotInstructions
-		? slotInstructions.map((instr) => stringifyChunk(result, instr)).join('')
+		? slotInstructions.map((instr) => chunkToString(result, instr)).join('')
 		: '';
-	return markHTMLString(hydrationHtml + html);
+	return {
+		render(destination) {
+			destination.write(markHTMLString(hydrationHtml + html));
+		},
+	};
 }
 
-export function renderComponent(
+async function renderAstroComponent(
+	result: SSRResult,
+	displayName: string,
+	Component: AstroComponentFactory,
+	props: Record<string | number, any>,
+	slots: any = {}
+): Promise<RenderInstance> {
+	const instance = await createAstroComponentInstance(result, displayName, Component, props, slots);
+
+	// Eagerly render the component so they are rendered in parallel
+	const chunks: RenderDestinationChunk[] = [];
+	const temporaryDestination: RenderDestination = {
+		write: (chunk) => chunks.push(chunk),
+	};
+	await instance.render(temporaryDestination);
+
+	return {
+		render(destination) {
+			// The real render function will simply pass on the results from the temporary destination
+			for (const chunk of chunks) {
+				destination.write(chunk);
+			}
+		},
+	};
+}
+
+export async function renderComponent(
 	result: SSRResult,
 	displayName: string,
 	Component: unknown,
 	props: Record<string | number, any>,
 	slots: any = {}
-): Promise<ComponentIterable> | ComponentIterable | AstroComponentInstance {
+): Promise<RenderInstance> {
 	if (isPromise(Component)) {
-		return Promise.resolve(Component).then((Unwrapped) => {
-			return renderComponent(result, displayName, Unwrapped, props, slots) as any;
-		});
+		Component = await Component;
 	}
 
 	if (isFragmentComponent(Component)) {
-		return renderFragmentComponent(result, slots);
+		return await renderFragmentComponent(result, slots);
 	}
 
 	// .html components
 	if (isHTMLComponent(Component)) {
-		return renderHTMLComponent(result, Component, props, slots);
+		return await renderHTMLComponent(result, Component, props, slots);
 	}
 
 	if (isAstroComponentFactory(Component)) {
-		return createAstroComponentInstance(result, displayName, Component, props, slots);
+		return await renderAstroComponent(result, displayName, Component, props, slots);
 	}
 
-	return renderFrameworkComponent(result, displayName, Component, props, slots);
+	return await renderFrameworkComponent(result, displayName, Component, props, slots);
 }
 
-export function renderComponentToIterable(
+export async function renderComponentToString(
 	result: SSRResult,
 	displayName: string,
 	Component: unknown,
 	props: Record<string | number, any>,
-	slots: any = {}
-): Promise<ComponentIterable> | ComponentIterable {
-	const renderResult = renderComponent(result, displayName, Component, props, slots);
-	if (isAstroComponentInstance(renderResult)) {
-		return renderResult.render();
+	slots: any = {},
+	isPage = false,
+	route?: RouteData
+): Promise<string> {
+	let str = '';
+	let renderedFirstPageChunk = false;
+
+	// Handle head injection if required. Note that this needs to run early so
+	// we can ensure getting a value for `head`.
+	let head = '';
+	if (nonAstroPageNeedsHeadInjection(Component)) {
+		for (const headChunk of maybeRenderHead()) {
+			head += chunkToString(result, headChunk);
+		}
 	}
-	return renderResult;
+
+	try {
+		const destination: RenderDestination = {
+			write(chunk) {
+				// Automatic doctype and head insertion for pages
+				if (isPage && !renderedFirstPageChunk) {
+					renderedFirstPageChunk = true;
+					if (!/<!doctype html/i.test(String(chunk))) {
+						const doctype = result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n';
+						str += doctype + head;
+					}
+				}
+
+				// `renderToString` doesn't work with emitting responses, so ignore here
+				if (chunk instanceof Response) return;
+
+				str += chunkToString(result, chunk);
+			},
+		};
+
+		const renderInstance = await renderComponent(result, displayName, Component, props, slots);
+		await renderInstance.render(destination);
+	} catch (e) {
+		// We don't have a lot of information downstream, and upstream we can't catch the error properly
+		// So let's add the location here
+		if (AstroError.is(e) && !e.loc) {
+			e.setLocation({
+				file: route?.component,
+			});
+		}
+
+		throw e;
+	}
+
+	return str;
+}
+
+export type NonAstroPageComponent = {
+	name: string;
+	[needsHeadRenderingSymbol]: boolean;
+};
+
+function nonAstroPageNeedsHeadInjection(
+	pageComponent: any
+): pageComponent is NonAstroPageComponent {
+	return !!pageComponent?.[needsHeadRenderingSymbol];
 }
diff --git a/packages/astro/src/runtime/server/render/dom.ts b/packages/astro/src/runtime/server/render/dom.ts
index 803f299957..1d0ea192ff 100644
--- a/packages/astro/src/runtime/server/render/dom.ts
+++ b/packages/astro/src/runtime/server/render/dom.ts
@@ -13,7 +13,7 @@ export async function renderHTMLElement(
 	constructor: typeof HTMLElement,
 	props: any,
 	slots: any
-) {
+): Promise<string> {
 	const name = getHTMLElementName(constructor);
 
 	let attrHTML = '';
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index d34bdd6c73..8a53767977 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -1,12 +1,7 @@
 export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
-export {
-	createHeadAndContent,
-	renderAstroTemplateResult,
-	renderTemplate,
-	renderToString,
-} from './astro/index.js';
-export { Fragment, Renderer, stringifyChunk } from './common.js';
-export { renderComponent, renderComponentToIterable } from './component.js';
+export { createHeadAndContent, renderTemplate, renderToString } from './astro/index.js';
+export { Fragment, Renderer, chunkToString, chunkToByteArray } from './common.js';
+export { renderComponent, renderComponentToString } from './component.js';
 export { renderHTMLElement } from './dom.js';
 export { maybeRenderHead, renderHead } from './head.js';
 export { renderPage } from './page.js';
diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts
index 025b0b9e37..cabbe8dae7 100644
--- a/packages/astro/src/runtime/server/render/page.ts
+++ b/packages/astro/src/runtime/server/render/page.ts
@@ -1,50 +1,11 @@
 import type { RouteData, SSRResult } from '../../../@types/astro';
-import type { ComponentIterable } from './component';
+import { renderComponentToString, type NonAstroPageComponent } from './component.js';
 import type { AstroComponentFactory } from './index';
 
-import { AstroError } from '../../../core/errors/index.js';
-import { isHTMLString } from '../escape.js';
 import { createResponse } from '../response.js';
-import { isAstroComponentFactory, isAstroComponentInstance } from './astro/index.js';
+import { isAstroComponentFactory } from './astro/index.js';
 import { renderToReadableStream, renderToString } from './astro/render.js';
-import { HTMLParts, encoder } from './common.js';
-import { renderComponent } from './component.js';
-import { maybeRenderHead } from './head.js';
-
-const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
-
-type NonAstroPageComponent = {
-	name: string;
-	[needsHeadRenderingSymbol]: boolean;
-};
-
-function nonAstroPageNeedsHeadInjection(pageComponent: NonAstroPageComponent): boolean {
-	return needsHeadRenderingSymbol in pageComponent && !!pageComponent[needsHeadRenderingSymbol];
-}
-
-async function iterableToHTMLBytes(
-	result: SSRResult,
-	iterable: ComponentIterable,
-	onDocTypeInjection?: (parts: HTMLParts) => Promise<void>
-): Promise<Uint8Array> {
-	const parts = new HTMLParts();
-	let i = 0;
-	for await (const chunk of iterable) {
-		if (isHTMLString(chunk)) {
-			if (i === 0) {
-				i++;
-				if (!/<!doctype html/i.test(String(chunk))) {
-					parts.append(`${result.compressHTML ? '<!DOCTYPE html>' : '<!DOCTYPE html>\n'}`, result);
-					if (onDocTypeInjection) {
-						await onDocTypeInjection(parts);
-					}
-				}
-			}
-		}
-		parts.append(chunk, result);
-	}
-	return parts.toArrayBuffer();
-}
+import { encoder } from './common.js';
 
 export async function renderPage(
 	result: SSRResult,
@@ -52,49 +13,25 @@ export async function renderPage(
 	props: any,
 	children: any,
 	streaming: boolean,
-	route?: RouteData | undefined
+	route?: RouteData
 ): Promise<Response> {
 	if (!isAstroComponentFactory(componentFactory)) {
 		result._metadata.headInTree =
 			result.componentMetadata.get((componentFactory as any).moduleId)?.containsHead ?? false;
+
 		const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
-		let output: ComponentIterable;
-		let head = '';
-		try {
-			if (nonAstroPageNeedsHeadInjection(componentFactory)) {
-				const parts = new HTMLParts();
-				for await (const chunk of maybeRenderHead()) {
-					parts.append(chunk, result);
-				}
-				head = parts.toString();
-			}
 
-			const renderResult = await renderComponent(
-				result,
-				componentFactory.name,
-				componentFactory,
-				pageProps,
-				null
-			);
-			if (isAstroComponentInstance(renderResult)) {
-				output = renderResult.render();
-			} else {
-				output = renderResult;
-			}
-		} catch (e) {
-			if (AstroError.is(e) && !e.loc) {
-				e.setLocation({
-					file: route?.component,
-				});
-			}
+		const str = await renderComponentToString(
+			result,
+			componentFactory.name,
+			componentFactory,
+			pageProps,
+			null,
+			true,
+			route
+		);
 
-			throw e;
-		}
-
-		// Accumulate the HTML string and append the head if necessary.
-		const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
-			parts.append(head, result);
-		});
+		const bytes = encoder.encode(str);
 
 		return new Response(bytes, {
 			headers: new Headers([
@@ -103,6 +40,7 @@ export async function renderPage(
 			]),
 		});
 	}
+
 	// Mark if this page component contains a <head> within its tree. If it does
 	// We avoid implicit head injection entirely.
 	result._metadata.headInTree =
diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts
index 152230ba9a..daae87a807 100644
--- a/packages/astro/src/runtime/server/render/slot.ts
+++ b/packages/astro/src/runtime/server/render/slot.ts
@@ -4,6 +4,7 @@ import type { RenderInstruction } from './types.js';
 
 import { HTMLString, markHTMLString } from '../escape.js';
 import { renderChild } from './any.js';
+import { chunkToString, type RenderDestination, type RenderInstance } from './common.js';
 
 type RenderTemplateResult = ReturnType<typeof renderTemplate>;
 export type ComponentSlots = Record<string, ComponentSlotValue>;
@@ -27,19 +28,19 @@ export function isSlotString(str: string): str is any {
 	return !!(str as any)[slotString];
 }
 
-export async function* renderSlot(
+export function renderSlot(
 	result: SSRResult,
 	slotted: ComponentSlotValue | RenderTemplateResult,
 	fallback?: ComponentSlotValue | RenderTemplateResult
-): AsyncGenerator<any, void, undefined> {
-	if (slotted) {
-		let iterator = renderChild(typeof slotted === 'function' ? slotted(result) : slotted);
-		yield* iterator;
-	}
-
-	if (fallback && !slotted) {
-		yield* renderSlot(result, fallback);
+): RenderInstance {
+	if (!slotted && fallback) {
+		return renderSlot(result, fallback);
 	}
+	return {
+		async render(destination) {
+			await renderChild(destination, typeof slotted === 'function' ? slotted(result) : slotted);
+		},
+	};
 }
 
 export async function renderSlotToString(
@@ -49,17 +50,21 @@ export async function renderSlotToString(
 ): Promise<string> {
 	let content = '';
 	let instructions: null | RenderInstruction[] = null;
-	let iterator = renderSlot(result, slotted, fallback);
-	for await (const chunk of iterator) {
-		if (typeof chunk.type === 'string') {
-			if (instructions === null) {
-				instructions = [];
+	const temporaryDestination: RenderDestination = {
+		write(chunk) {
+			if (chunk instanceof Response) return;
+			if (typeof chunk === 'object' && 'type' in chunk && typeof chunk.type === 'string') {
+				if (instructions === null) {
+					instructions = [];
+				}
+				instructions.push(chunk);
+			} else {
+				content += chunkToString(result, chunk);
 			}
-			instructions.push(chunk);
-		} else {
-			content += chunk;
-		}
-	}
+		},
+	};
+	const renderInstance = renderSlot(result, slotted, fallback);
+	await renderInstance.render(temporaryDestination);
 	return markHTMLString(new SlotString(content, instructions));
 }
 
diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts
index f422f66d57..e007fe6f16 100644
--- a/packages/astro/src/runtime/server/render/util.ts
+++ b/packages/astro/src/runtime/server/render/util.ts
@@ -145,145 +145,3 @@ export function renderElement(
 	}
 	return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
 }
-
-const iteratorQueue: EagerAsyncIterableIterator[][] = [];
-
-/**
- * Takes an array of iterators and adds them to a list of iterators to start buffering
- * as soon as the execution flow is suspended for the first time. We expect a lot
- * of calls to this function before the first suspension, so to reduce the number
- * of calls to setTimeout we batch the buffering calls.
- * @param iterators
- */
-function queueIteratorBuffers(iterators: EagerAsyncIterableIterator[]) {
-	if (iteratorQueue.length === 0) {
-		setTimeout(() => {
-			// buffer all iterators that haven't started yet
-			iteratorQueue.forEach((its) => its.forEach((it) => !it.isStarted() && it.buffer()));
-			iteratorQueue.length = 0; // fastest way to empty an array
-		});
-	}
-	iteratorQueue.push(iterators);
-}
-
-/**
- * This will take an array of async iterables and start buffering them eagerly.
- * To avoid useless buffering, it will only start buffering the next tick, so the
- * first sync iterables won't be buffered.
- */
-export function bufferIterators<T>(iterators: AsyncIterable<T>[]): AsyncIterable<T>[] {
-	// all async iterators start running in non-buffered mode to avoid useless caching
-	const eagerIterators = iterators.map((it) => new EagerAsyncIterableIterator(it));
-	// once the execution of the next for loop is suspended due to an async component,
-	// this timeout triggers and we start buffering the other iterators
-	queueIteratorBuffers(eagerIterators);
-	return eagerIterators;
-}
-
-// This wrapper around an AsyncIterable can eagerly consume its values, so that
-// its values are ready to yield out ASAP. This is used for list-like usage of
-// Astro components, so that we don't have to wait on earlier components to run
-// to even start running those down in the list.
-export class EagerAsyncIterableIterator {
-	#iterable: AsyncIterable<any>;
-	#queue = new Queue<IteratorResult<any, any>>();
-	#error: any = undefined;
-	#next: Promise<IteratorResult<any, any>> | undefined;
-	/**
-	 * Whether the proxy is running in buffering or pass-through mode
-	 */
-	#isBuffering = false;
-	#gen: AsyncIterator<any> | undefined = undefined;
-	#isStarted = false;
-
-	constructor(iterable: AsyncIterable<any>) {
-		this.#iterable = iterable;
-	}
-
-	/**
-	 * Starts to eagerly fetch the inner iterator and cache the results.
-	 * Note: This might not be called after next() has been called once, e.g. the iterator is started
-	 */
-	async buffer() {
-		if (this.#gen) {
-			// If this called as part of rendering, please open a bug report.
-			// Any call to buffer() should verify that the iterator isn't running
-			throw new Error('Cannot not switch from non-buffer to buffer mode');
-		}
-		this.#isBuffering = true;
-		this.#isStarted = true;
-		this.#gen = this.#iterable[Symbol.asyncIterator]();
-		let value: IteratorResult<any, any> | undefined = undefined;
-		do {
-			this.#next = this.#gen.next();
-			try {
-				value = await this.#next;
-				this.#queue.push(value);
-			} catch (e) {
-				this.#error = e;
-			}
-		} while (value && !value.done);
-	}
-
-	async next() {
-		if (this.#error) {
-			throw this.#error;
-		}
-		// for non-buffered mode, just pass through the next result
-		if (!this.#isBuffering) {
-			if (!this.#gen) {
-				this.#isStarted = true;
-				this.#gen = this.#iterable[Symbol.asyncIterator]();
-			}
-			return await this.#gen.next();
-		}
-		if (!this.#queue.isEmpty()) {
-			return this.#queue.shift()!;
-		}
-		await this.#next;
-		// the previous statement will either put an element in the queue or throw,
-		// so we can safely assume we have something now
-		return this.#queue.shift()!;
-	}
-
-	isStarted() {
-		return this.#isStarted;
-	}
-
-	[Symbol.asyncIterator]() {
-		return this;
-	}
-}
-
-interface QueueItem<T> {
-	item: T;
-	next?: QueueItem<T>;
-}
-
-/**
- * Basis Queue implementation with a linked list
- */
-class Queue<T> {
-	head: QueueItem<T> | undefined = undefined;
-	tail: QueueItem<T> | undefined = undefined;
-
-	push(item: T) {
-		if (this.head === undefined) {
-			this.head = { item };
-			this.tail = this.head;
-		} else {
-			this.tail!.next = { item };
-			this.tail = this.tail!.next;
-		}
-	}
-
-	isEmpty() {
-		return this.head === undefined;
-	}
-
-	shift(): T | undefined {
-		const val = this.head?.item;
-		this.head = this.head?.next;
-		return val;
-	}
-}
diff --git a/packages/astro/test/streaming.test.js b/packages/astro/test/streaming.test.js
index c7b835de1b..e3627d7ba0 100644
--- a/packages/astro/test/streaming.test.js
+++ b/packages/astro/test/streaming.test.js
@@ -48,7 +48,7 @@ describe('Streaming', () => {
 				let chunk = decoder.decode(bytes);
 				chunks.push(chunk);
 			}
-			expect(chunks.length).to.equal(3);
+			expect(chunks.length).to.equal(2);
 		});
 	});