From 220ad45cc2674cc7d927ca52d555ea8e5ec2ddbc Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 8 Aug 2024 11:53:40 +0200 Subject: [PATCH] Add async benchmark and perf iterations (#386) --- .changeset/angry-sheep-design.md | 5 ++ benchmarks/async.js | 42 ++++++++++++++++ benchmarks/index.js | 16 ++++++- src/index.js | 82 +++++++++++++------------------- src/lib/util.js | 2 +- 5 files changed, 96 insertions(+), 51 deletions(-) create mode 100644 .changeset/angry-sheep-design.md create mode 100644 benchmarks/async.js diff --git a/.changeset/angry-sheep-design.md b/.changeset/angry-sheep-design.md new file mode 100644 index 0000000..d6b34b9 --- /dev/null +++ b/.changeset/angry-sheep-design.md @@ -0,0 +1,5 @@ +--- +'preact-render-to-string': patch +--- + +Add async benchmarks and iterate on perf improvements diff --git a/benchmarks/async.js b/benchmarks/async.js new file mode 100644 index 0000000..8ac8b88 --- /dev/null +++ b/benchmarks/async.js @@ -0,0 +1,42 @@ +import { h } from 'preact'; +import { lazy } from 'preact/compat'; + +function Leaf() { + return ( +
+ + deep stack + +
+ ); +} + +const lazies = new Array(600) + .fill(600) + .map(() => + lazy(() => + Promise.resolve().then(() => ({ + default: (props) =>
{props.children}
+ })) + ) + ); +function PassThrough(props) { + const Lazy = lazies(props.id); + return ; +} + +function recursive(n, m) { + if (n <= 0) { + return ; + } + return {recursive(n - 1)}; +} + +const content = []; +for (let i = 0; i < 5; i++) { + content.push(recursive(10, i)); +} + +export default function App() { + return
{content}
; +} diff --git a/benchmarks/index.js b/benchmarks/index.js index c46a7db..436478d 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -1,14 +1,23 @@ import { h } from 'preact'; import Suite from 'benchmarkjs-pretty'; -import renderToStringBaseline from 'baseline-rts'; +import renderToStringBaseline, { + renderToStringAsync as renderToStringAsyncBaseline +} from 'baseline-rts'; // import renderToString from '../src/index'; -import renderToString from '../dist/index.module.js'; +import renderToString, { renderToStringAsync } from '../dist/index.module.js'; import TextApp from './text'; import StackApp from './stack'; import { App as IsomorphicSearchResults } from './isomorphic-ui/search-results/index'; import { App as ColorPicker } from './isomorphic-ui/color-picker'; function suite(name, Root) { + return new Suite(name) + .add('baseline', () => renderToStringAsyncBaseline()) + .add('current', () => renderToStringAsync()) + .run(); +} + +function asyncSuite(name, Root) { return new Suite(name) .add('baseline', () => renderToStringBaseline()) .add('current', () => renderToString()) @@ -20,4 +29,7 @@ function suite(name, Root) { await suite('SearchResults', IsomorphicSearchResults); await suite('ColorPicker', ColorPicker); await suite('Stack Depth', StackApp); + + const { App: Async } = await import('./async.js'); + await asyncSuite('async', Async); })(); diff --git a/src/index.js b/src/index.js index c2efbdf..1ac2147 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,8 @@ import { UNSAFE_NAME, NAMESPACE_REPLACE_REGEX, HTML_LOWER_CASE, - SVG_CAMEL_CASE + SVG_CAMEL_CASE, + createComponent } from './lib/util.js'; import { options, h, Fragment } from 'preact'; import { @@ -22,6 +23,7 @@ import { CATCH_ERROR } from './lib/constants.js'; +const EMPTY_OBJ = {}; const EMPTY_ARR = []; const isArray = Array.isArray; const assign = Object.assign; @@ -147,13 +149,6 @@ export async function renderToStringAsync(vnode, context) { } } -// Installed as setState/forceUpdate for function components -function markAsDirty() { - this.__d = true; -} - -const EMPTY_OBJ = {}; - /** * @param {VNode} vnode * @param {Record} context @@ -238,9 +233,9 @@ function _renderToString( let vnodeType = typeof vnode; // Text VNodes: escape as HTML - if (vnodeType !== 'object') { - if (vnodeType === 'function') return EMPTY_STR; - return vnodeType === 'string' ? encodeEntities(vnode) : vnode + EMPTY_STR; + if (vnodeType != 'object') { + if (vnodeType == 'function') return EMPTY_STR; + return vnodeType == 'string' ? encodeEntities(vnode) : vnode + EMPTY_STR; } // Recurse into children / Arrays @@ -250,7 +245,7 @@ function _renderToString( parent[CHILDREN] = vnode; for (let i = 0; i < vnode.length; i++) { let child = vnode[i]; - if (child == null || typeof child === 'boolean') continue; + if (child == null || typeof child == 'boolean') continue; const childRender = _renderToString( child, @@ -262,10 +257,12 @@ function _renderToString( renderer ); - if (typeof childRender === 'string') { + if (typeof childRender == 'string') { rendered = rendered + childRender; } else { - renderArray = renderArray || []; + if (!renderArray) { + renderArray = []; + } if (rendered) renderArray.push(rendered); @@ -294,14 +291,14 @@ function _renderToString( if (beforeDiff) beforeDiff(vnode); let type = vnode.type, - props = vnode.props, - cctx = context, - contextType, - rendered, - component; + props = vnode.props; // Invoke rendering on Components - if (typeof type === 'function') { + if (typeof type == 'function') { + let cctx = context, + contextType, + rendered, + component; if (type === Fragment) { // Serialized precompiled JSX. if ('tpl' in props) { @@ -315,7 +312,7 @@ function _renderToString( // Check if we're dealing with a vnode or an array of nodes if ( - typeof value === 'object' && + typeof value == 'object' && (value.constructor === undefined || isArray(value)) ) { out = @@ -340,9 +337,7 @@ function _renderToString( } else if ('UNSTABLE_comment' in props) { // Fragments are the least used components of core that's why // branching here for comments has the least effect on perf. - return ( - '' - ); + return ''; } rendered = props.children; @@ -354,22 +349,15 @@ function _renderToString( } let isClassComponent = - type.prototype && typeof type.prototype.render === 'function'; + type.prototype && typeof type.prototype.render == 'function'; if (isClassComponent) { rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx); component = vnode[COMPONENT]; } else { - vnode[COMPONENT] = component = { - __v: vnode, - props, - context: cctx, - // silently drop state updates - setState: markAsDirty, - forceUpdate: markAsDirty, - __d: true, - // hooks - __h: [] - }; + vnode[COMPONENT] = component = /**#__NOINLINE__**/ createComponent( + vnode, + cctx + ); // If a hook invokes setState() to invalidate the component during rendering, // re-render it up to 25 times to allow "settling" of memoized states. @@ -402,7 +390,7 @@ function _renderToString( rendered != null && rendered.type === Fragment && rendered.key == null && - !('tpl' in rendered.props); + rendered.props.tpl == null; rendered = isTopLevelFragment ? rendered.props.children : rendered; try { @@ -416,8 +404,6 @@ function _renderToString( renderer ); } catch (err) { - let str = EMPTY_STR; - if (type.getDerivedStateFromError) { component[NEXT_STATE] = type.getDerivedStateFromError(err); } @@ -438,10 +424,10 @@ function _renderToString( rendered != null && rendered.type === Fragment && rendered.key == null && - !('tpl' in rendered.props); + rendered.props.tpl == null; rendered = isTopLevelFragment ? rendered.props.children : rendered; - str = _renderToString( + return _renderToString( rendered, context, isSvgMode, @@ -452,7 +438,7 @@ function _renderToString( ); } - return str; + return EMPTY_STR; } finally { if (afterDiff) afterDiff(vnode); vnode[PARENT] = null; @@ -468,7 +454,7 @@ function _renderToString( rendered != null && rendered.type === Fragment && rendered.key == null && - !('tpl' in rendered.props); + rendered.props.tpl == null; rendered = isTopLevelFragment ? rendered.props.children : rendered; try { @@ -513,7 +499,7 @@ function _renderToString( if (!asyncMode) throw error; - if (!error || typeof error.then !== 'function') throw error; + if (!error || typeof error.then != 'function') throw error; const renderNestedChildren = () => { try { @@ -527,7 +513,7 @@ function _renderToString( renderer ); } catch (e) { - if (!e || typeof e.then !== 'function') throw e; + if (!e || typeof e.then != 'function') throw e; return e.then( () => @@ -557,7 +543,7 @@ function _renderToString( for (let name in props) { let v = props[name]; - if (typeof v === 'function') continue; + if (typeof v == 'function') continue; switch (name) { case 'children': @@ -663,7 +649,7 @@ function _renderToString( ' ' + name + '="' + - (typeof v === 'string' ? encodeEntities(v) : v + EMPTY_STR) + + (typeof v == 'string' ? encodeEntities(v) : v + EMPTY_STR) + '"'; } } @@ -711,7 +697,7 @@ function _renderToString( const startTag = s + '>'; if (isArray(html)) return [startTag, ...html, endTag]; - else if (typeof html !== 'string') return [startTag, html, endTag]; + else if (typeof html != 'string') return [startTag, html, endTag]; return startTag + html + endTag; } diff --git a/src/lib/util.js b/src/lib/util.js index 691373c..e0002a8 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -147,7 +147,7 @@ export function createComponent(vnode, context) { forceUpdate: markAsDirty, __d: true, // hooks - __h: [] + __h: new Array(0) }; }