From fea30693609cc517d8660972151f4d12a0dd4e82 Mon Sep 17 00:00:00 2001 From: Johannes Spohr Date: Fri, 2 Jun 2023 10:46:12 +0200 Subject: [PATCH] Parallel array rendering (#7136) --- .changeset/olive-gorillas-cheat.md | 5 +++++ .../astro/src/runtime/server/render/any.ts | 6 ++++-- .../server/render/astro/render-template.ts | 18 +++++------------- .../astro/src/runtime/server/render/util.ts | 17 +++++++++++++++++ .../fixtures/parallel/src/pages/index.astro | 3 ++- 5 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 .changeset/olive-gorillas-cheat.md diff --git a/.changeset/olive-gorillas-cheat.md b/.changeset/olive-gorillas-cheat.md new file mode 100644 index 000000000000..0c20121ab365 --- /dev/null +++ b/.changeset/olive-gorillas-cheat.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Render arrays of components in parallel diff --git a/packages/astro/src/runtime/server/render/any.ts b/packages/astro/src/runtime/server/render/any.ts index cded2c49603f..4ee947ee6e89 100644 --- a/packages/astro/src/runtime/server/render/any.ts +++ b/packages/astro/src/runtime/server/render/any.ts @@ -5,6 +5,7 @@ import { renderAstroTemplateResult, } from './astro/index.js'; import { SlotString } from './slot.js'; +import { bufferIterators } from './util.js'; export async function* renderChild(child: any): AsyncIterable { child = await child; @@ -16,8 +17,9 @@ export async function* renderChild(child: any): AsyncIterable { } else if (isHTMLString(child)) { yield child; } else if (Array.isArray(child)) { - for (const value of child) { - yield markHTMLString(await renderChild(value)); + const bufferedIterators = bufferIterators(child.map((c) => renderChild(c))); + for (const value of bufferedIterators) { + yield markHTMLString(await value); } } else if (typeof child === 'function') { // Special: If a child is a function, call it automatically. 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 2433596bdcad..b0dbabdc1910 100644 --- a/packages/astro/src/runtime/server/render/astro/render-template.ts +++ b/packages/astro/src/runtime/server/render/astro/render-template.ts @@ -3,7 +3,7 @@ import type { RenderInstruction } from '../types'; import { HTMLBytes, markHTMLString } from '../../escape.js'; import { isPromise } from '../../util.js'; import { renderChild } from '../any.js'; -import { EagerAsyncIterableIterator } from '../util.js'; +import { bufferIterators } from '../util.js'; const renderTemplateResultSym = Symbol.for('astro.renderTemplateResult'); @@ -36,23 +36,15 @@ export class RenderTemplateResult { async *[Symbol.asyncIterator]() { const { htmlParts, expressions } = this; - let iterables: Array = []; - // all async iterators start running in non-buffered mode to avoid useless caching - for (let i = 0; i < htmlParts.length; i++) { - iterables.push(new EagerAsyncIterableIterator(renderChild(expressions[i]))); - } - // 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 - setTimeout(() => { - // buffer all iterators that haven't started yet - iterables.forEach((it) => !it.isStarted() && it.buffer()); - }, 0); + 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); - yield* iterable; + if (iterable) { + yield* iterable; + } } } } diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index c2599401dace..1f0aae047b34 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -139,6 +139,23 @@ export function renderElement( return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}`; } +/** + * 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(iterators: AsyncIterable[]): AsyncIterable[] { + // 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 + setTimeout(() => { + // buffer all iterators that haven't started yet + eagerIterators.forEach((it) => !it.isStarted() && it.buffer()); + }, 0); + 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 diff --git a/packages/astro/test/fixtures/parallel/src/pages/index.astro b/packages/astro/test/fixtures/parallel/src/pages/index.astro index 952e9efcebc6..b0571103ef43 100644 --- a/packages/astro/test/fixtures/parallel/src/pages/index.astro +++ b/packages/astro/test/fixtures/parallel/src/pages/index.astro @@ -1,8 +1,9 @@ --- -import Delayed from '../components/Delayed.astro' +import Delayed from '../components/Delayed.astro'; --- +{[, , , ]}