Skip to content

Commit

Permalink
Parallel array rendering (#7136)
Browse files Browse the repository at this point in the history
  • Loading branch information
Johannes Spohr authored Jun 2, 2023
1 parent 6b8d55b commit fea3069
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-gorillas-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Render arrays of components in parallel
6 changes: 4 additions & 2 deletions packages/astro/src/runtime/server/render/any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
child = await child;
Expand All @@ -16,8 +17,9 @@ export async function* renderChild(child: any): AsyncIterable<any> {
} 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.
Expand Down
18 changes: 5 additions & 13 deletions packages/astro/src/runtime/server/render/astro/render-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -36,23 +36,15 @@ export class RenderTemplateResult {
async *[Symbol.asyncIterator]() {
const { htmlParts, expressions } = this;

let iterables: Array<EagerAsyncIterableIterator> = [];
// 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;
}
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/astro/src/runtime/server/render/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ export function renderElement(
return `<${name}${internalSpreadAttributes(props, shouldEscape)}>${children}</${name}>`;
}

/**
* 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
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
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/test/fixtures/parallel/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
import Delayed from '../components/Delayed.astro'
import Delayed from '../components/Delayed.astro';
---

<Delayed ms={30} />
<Delayed ms={20} />
<Delayed ms={40} />
<Delayed ms={10} />
{[<Delayed ms={30} />, <Delayed ms={20} />, <Delayed ms={40} />, <Delayed ms={10} />]}

0 comments on commit fea3069

Please sign in to comment.