Skip to content

Commit

Permalink
Refactor Astro rendering to write results directly (#7782)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy committed Jul 25, 2023
1 parent ec40c8c commit 0f677c0
Show file tree
Hide file tree
Showing 17 changed files with 322 additions and 461 deletions.
5 changes: 5 additions & 0 deletions .changeset/lemon-snakes-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro": patch
---

Refactor Astro rendering to write results directly. This improves the rendering performance for all Astro files.
12 changes: 4 additions & 8 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
});
Expand All @@ -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;
}
Expand Down
3 changes: 0 additions & 3 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ export {
Fragment,
maybeRenderHead,
renderTemplate as render,
renderAstroTemplateResult as renderAstroComponent,
renderComponent,
renderComponentToIterable,
Renderer as Renderer,
renderHead,
renderHTMLElement,
Expand All @@ -30,7 +28,6 @@ export {
renderTemplate,
renderToString,
renderUniqueStylesheet,
stringifyChunk,
voidElementNames,
} from './render/index.js';
export type {
Expand Down
20 changes: 5 additions & 15 deletions packages/astro/src/runtime/server/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -177,33 +175,25 @@ 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,
props,
slots
);
} else {
output = await renderComponentToIterable(
output = await renderComponentToString(
result,
typeof vnode.type === 'function' ? vnode.type.name : vnode.type,
vnode.type,
props,
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
Expand Down
40 changes: 18 additions & 22 deletions packages/astro/src/runtime/server/render/any.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 1 addition & 5 deletions packages/astro/src/runtime/server/render/astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
16 changes: 12 additions & 4 deletions packages/astro/src/runtime/server/render/astro/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
}
}
Expand All @@ -71,7 +72,7 @@ function validateComponentProps(props: any, displayName: string) {
}
}

export function createAstroComponentInstance(
export async function createAstroComponentInstance(
result: SSRResult,
displayName: string,
factory: AstroComponentFactory,
Expand All @@ -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);

This comment has been minimized.

Copy link
@matthewp

matthewp Jul 27, 2023

Contributor

I think the problem is that doing it here is too late.

This comment has been minimized.

Copy link
@matthewp

matthewp Jul 27, 2023

Contributor

Sorry, this isn't the issue, nm.

}
}

return instance;
}

Expand Down
45 changes: 10 additions & 35 deletions packages/astro/src/runtime/server/render/astro/render-template.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -33,17 +31,15 @@ export class RenderTemplateResult {
});
}

async *[Symbol.asyncIterator]() {
const { htmlParts, expressions } = this;

let iterables = bufferIterators(expressions.map((e) => renderChild(e)));
for (let i = 0; i < htmlParts.length; i++) {
const html = htmlParts[i];
const iterable = iterables[i];
async render(destination: RenderDestination) {
for (let i = 0; i < this.htmlParts.length; i++) {
const html = this.htmlParts[i];
const exp = this.expressions[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);
}
}
}
Expand All @@ -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);
}
34 changes: 6 additions & 28 deletions packages/astro/src/runtime/server/render/astro/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -46,9 +46,7 @@ export async function renderToString(
},
};

for await (const chunk of renderAstroTemplateResult(templateResult)) {
destination.write(chunk);
}
await templateResult.render(destination);

return str;
}
Expand All @@ -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({
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
})();
},
Expand Down Expand Up @@ -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);
}
}
}
Loading

0 comments on commit 0f677c0

Please sign in to comment.