diff --git a/packages/server-renderer/__tests__/unrollBuffer.bench.ts b/packages/server-renderer/__tests__/unrollBuffer.bench.ts new file mode 100644 index 000000000..b5e03cea6 --- /dev/null +++ b/packages/server-renderer/__tests__/unrollBuffer.bench.ts @@ -0,0 +1,74 @@ +import { bench, describe } from 'vitest' + +import { type SSRBuffer, createBuffer } from '../src/render' +import { unrollBuffer } from '../src/renderToString' + +function createSyncBuffer(levels: number, itemsPerLevel: number): SSRBuffer { + const buffer = createBuffer() + + function addItems(buf: ReturnType, level: number) { + for (let i = 1; i <= levels * itemsPerLevel; i++) { + buf.push(`sync${level}.${i}`) + } + if (level < levels) { + const subBuffer = createBuffer() + addItems(subBuffer, level + 1) + buf.push(subBuffer.getBuffer()) + } + } + + addItems(buffer, 1) + return buffer.getBuffer() +} + +function createMixedBuffer(levels: number, itemsPerLevel: number): SSRBuffer { + const buffer = createBuffer() + + function addItems(buf: ReturnType, level: number) { + for (let i = 1; i <= levels * itemsPerLevel; i++) { + if (i % 3 === 0) { + // @ts-expect-error testing... + buf.push(Promise.resolve(`async${level}.${i}`)) + } else { + buf.push(`sync${level}.${i}`) + } + } + if (level < levels) { + const subBuffer = createBuffer() + addItems(subBuffer, level + 1) + buf.push(subBuffer.getBuffer()) + } + } + + addItems(buffer, 1) + return buffer.getBuffer() +} + +describe('unrollBuffer', () => { + let syncBuffer = createBuffer().getBuffer() + let mixedBuffer = createBuffer().getBuffer() + + bench( + 'sync', + () => { + return unrollBuffer(syncBuffer) as any + }, + { + setup() { + syncBuffer = createSyncBuffer(5, 3) + }, + }, + ) + + bench( + 'mixed', + () => { + return unrollBuffer(mixedBuffer) as any + }, + { + setup() { + mixedBuffer = createMixedBuffer(5, 3) + }, + }, + ) +}) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 0e9299ee8..b931a4d55 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -11,26 +11,46 @@ import { type SSRBuffer, type SSRContext, renderComponentVNode } from './render' const { isVNode } = ssrUtils -async function unrollBuffer(buffer: SSRBuffer): Promise { - if (buffer.hasAsync) { - let ret = '' - for (let i = 0; i < buffer.length; i++) { - let item = buffer[i] - if (isPromise(item)) { - item = await item - } - if (isString(item)) { - ret += item - } else { - ret += await unrollBuffer(item) - } - } - return ret - } else { - // sync buffer can be more efficiently unrolled without unnecessary await - // ticks - return unrollBufferSync(buffer) +function nestedUnrollBuffer( + buffer: SSRBuffer, + parentRet: string, + startIndex: number, +): Promise | string { + if (!buffer.hasAsync) { + return parentRet + unrollBufferSync(buffer) } + + let ret = parentRet + for (let i = startIndex; i < buffer.length; i += 1) { + const item = buffer[i] + if (isString(item)) { + ret += item + continue + } + + if (isPromise(item)) { + return item.then(nestedItem => { + buffer[i] = nestedItem + return nestedUnrollBuffer(buffer, ret, i) + }) + } + + const result = nestedUnrollBuffer(item, ret, 0) + if (isPromise(result)) { + return result.then(nestedItem => { + buffer[i] = nestedItem + return nestedUnrollBuffer(buffer, '', i) + }) + } + + ret = result + } + + return ret +} + +export function unrollBuffer(buffer: SSRBuffer): Promise | string { + return nestedUnrollBuffer(buffer, '', 0) } function unrollBufferSync(buffer: SSRBuffer): string {