diff --git a/.changeset/calm-cherries-lay.md b/.changeset/calm-cherries-lay.md new file mode 100644 index 00000000..8ec9d305 --- /dev/null +++ b/.changeset/calm-cherries-lay.md @@ -0,0 +1,5 @@ +--- +"preact-render-to-string": patch +--- + +General performance optimisations diff --git a/benchmarks/index.js b/benchmarks/index.js index 5820775d..c46a7db3 100644 --- a/benchmarks/index.js +++ b/benchmarks/index.js @@ -4,7 +4,7 @@ import renderToStringBaseline from 'baseline-rts'; // import renderToString from '../src/index'; import renderToString from '../dist/index.module.js'; import TextApp from './text'; -// import StackApp from './stack'; +import StackApp from './stack'; import { App as IsomorphicSearchResults } from './isomorphic-ui/search-results/index'; import { App as ColorPicker } from './isomorphic-ui/color-picker'; @@ -19,6 +19,5 @@ function suite(name, Root) { await suite('Text', TextApp); await suite('SearchResults', IsomorphicSearchResults); await suite('ColorPicker', ColorPicker); - // TODO: Enable this once we switched away from recursion - // await suite('Stack Depth', StackApp); + await suite('Stack Depth', StackApp); })(); diff --git a/package-lock.json b/package-lock.json index c645b168..7045b906 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "preact-render-to-string", - "version": "6.5.2", + "version": "6.5.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "preact-render-to-string", - "version": "6.5.2", + "version": "6.5.7", "license": "MIT", "devDependencies": { "@babel/plugin-transform-react-jsx": "^7.12.12", @@ -14,7 +14,7 @@ "@babel/register": "^7.12.10", "@changesets/changelog-github": "^0.4.1", "@changesets/cli": "^2.18.0", - "baseline-rts": "npm:preact-render-to-string@latest", + "baseline-rts": "npm:preact-render-to-string@6.5.7", "benchmarkjs-pretty": "^2.0.1", "chai": "^4.2.0", "check-export-map": "^1.3.1", @@ -2567,13 +2567,10 @@ }, "node_modules/baseline-rts": { "name": "preact-render-to-string", - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", - "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.7.tgz", + "integrity": "sha512-nACZDdv/ZZciuldVYMcfGqr61DKJeaAfPx96hn6OXoBGhgtU2yGQkA0EpTzWH4SvnwF0syLsL4WK7AIp3Ruc1g==", "dev": true, - "dependencies": { - "pretty-format": "^3.8.0" - }, "peerDependencies": { "preact": ">=10" } diff --git a/package.json b/package.json index ea15c894..fa91b137 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "@babel/register": "^7.12.10", "@changesets/changelog-github": "^0.4.1", "@changesets/cli": "^2.18.0", - "baseline-rts": "npm:preact-render-to-string@latest", + "baseline-rts": "npm:preact-render-to-string@6.5.7", "benchmarkjs-pretty": "^2.0.1", "chai": "^4.2.0", "check-export-map": "^1.3.1", diff --git a/src/index.js b/src/index.js index ab4146fc..a5ce8d9a 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,7 @@ import { const EMPTY_ARR = []; const isArray = Array.isArray; const assign = Object.assign; +const EMPTY_STR = ''; // Global state for the current render pass let beforeDiff, afterDiff, renderHook, ummountHook; @@ -65,8 +66,8 @@ export function renderToString(vnode, context, _rendererState) { _rendererState ); - if (Array.isArray(rendered)) { - return rendered.join(''); + if (isArray(rendered)) { + return rendered.join(EMPTY_STR); } return rendered; } catch (e) { @@ -119,7 +120,7 @@ export async function renderToStringAsync(vnode, context) { undefined ); - if (Array.isArray(rendered)) { + if (isArray(rendered)) { let count = 0; let resolved = rendered; @@ -133,7 +134,7 @@ export async function renderToStringAsync(vnode, context) { resolved = (await Promise.all(resolved)).flat(); } - return resolved.join(''); + return resolved.join(EMPTY_STR); } return rendered; @@ -226,19 +227,26 @@ function _renderToString( renderer ) { // Ignore non-rendered VNodes/values - if (vnode == null || vnode === true || vnode === false || vnode === '') { - return ''; + if ( + vnode == null || + vnode === true || + vnode === false || + vnode === EMPTY_STR + ) { + return EMPTY_STR; } // Text VNodes: escape as HTML if (typeof vnode !== 'object') { - if (typeof vnode === 'function') return ''; - return encodeEntities(vnode + ''); + if (typeof vnode === 'function') return EMPTY_STR; + return typeof vnode === 'string' + ? encodeEntities(vnode) + : vnode + EMPTY_STR; } // Recurse into children / Arrays if (isArray(vnode)) { - let rendered = '', + let rendered = EMPTY_STR, renderArray; parent[CHILDREN] = vnode; for (let i = 0; i < vnode.length; i++) { @@ -256,15 +264,15 @@ function _renderToString( ); if (typeof childRender === 'string') { - rendered += childRender; + rendered = rendered + childRender; } else { renderArray = renderArray || []; if (rendered) renderArray.push(rendered); - rendered = ''; + rendered = EMPTY_STR; - if (Array.isArray(childRender)) { + if (isArray(childRender)) { renderArray.push(...childRender); } else { renderArray.push(childRender); @@ -281,7 +289,7 @@ function _renderToString( } // VNodes have {constructor:undefined} to prevent JSON injection: - if (vnode.constructor !== undefined) return ''; + if (vnode.constructor !== undefined) return EMPTY_STR; vnode[PARENT] = parent; if (beforeDiff) beforeDiff(vnode); @@ -298,9 +306,9 @@ function _renderToString( if (type === Fragment) { // Serialized precompiled JSX. if (props.tpl) { - let out = ''; + let out = EMPTY_STR; for (let i = 0; i < props.tpl.length; i++) { - out += props.tpl[i]; + out = out + props.tpl[i]; if (props.exprs && i < props.exprs.length) { const value = props.exprs[i]; @@ -311,18 +319,20 @@ function _renderToString( typeof value === 'object' && (value.constructor === undefined || isArray(value)) ) { - out += _renderToString( - value, - context, - isSvgMode, - selectValue, - vnode, - asyncMode, - renderer - ); + out = + out + + _renderToString( + value, + context, + isSvgMode, + selectValue, + vnode, + asyncMode, + renderer + ); } else { // Values are pre-escaped by the JSX transform - out += value; + out = out + value; } } } @@ -331,7 +341,9 @@ function _renderToString( } else if (props.UNSTABLE_comment) { // 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; @@ -342,11 +354,13 @@ function _renderToString( cctx = provider ? provider.props.value : contextType.__; } - if (type.prototype && typeof type.prototype.render === 'function') { + let isClassComponent = + type.prototype && typeof type.prototype.render === 'function'; + if (isClassComponent) { rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx); component = vnode[COMPONENT]; } else { - component = { + vnode[COMPONENT] = component = { __v: vnode, props, context: cctx, @@ -357,7 +371,6 @@ function _renderToString( // hooks __h: [] }; - vnode[COMPONENT] = component; // If a hook invokes setState() to invalidate the component during rendering, // re-render it up to 25 times to allow "settling" of memoized states. @@ -380,10 +393,10 @@ function _renderToString( } if ( - (type.getDerivedStateFromError || component.componentDidCatch) && - options.errorBoundaries + isClassComponent && + options.errorBoundaries && + (type.getDerivedStateFromError || component.componentDidCatch) ) { - let str = ''; // When a component returns a Fragment node we flatten it in core, so we // need to mirror that logic here too let isTopLevelFragment = @@ -393,7 +406,7 @@ function _renderToString( rendered = isTopLevelFragment ? rendered.props.children : rendered; try { - str = _renderToString( + return _renderToString( rendered, context, isSvgMode, @@ -402,14 +415,15 @@ function _renderToString( asyncMode, renderer ); - return str; } catch (err) { + let str = EMPTY_STR; + if (type.getDerivedStateFromError) { component[NEXT_STATE] = type.getDerivedStateFromError(err); } if (component.componentDidCatch) { - component.componentDidCatch(err, {}); + component.componentDidCatch(err, EMPTY_OBJ); } if (component[DIRTY]) { @@ -493,7 +507,7 @@ function _renderToString( let errorHook = options[CATCH_ERROR]; if (errorHook) errorHook(error, vnode); - return ''; + return EMPTY_STR; } if (!asyncMode) throw error; @@ -525,23 +539,25 @@ function _renderToString( asyncMode, renderer ), - () => renderNestedChildren() + renderNestedChildren ); } }; - return error.then(() => renderNestedChildren()); + return error.then(renderNestedChildren); } } // Serialize Element VNodes to HTML let s = '<' + type, - html = '', + html = EMPTY_STR, children; for (let name in props) { let v = props[name]; + if (typeof v === 'function') continue; + switch (name) { case 'children': children = v; @@ -622,7 +638,7 @@ function _renderToString( // serialize boolean aria-xyz or draggable attribute values as strings // `draggable` is an enumerated attribute and not Boolean. A value of `true` or `false` is mandatory // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable - v += ''; + v = v + EMPTY_STR; } else if (isSvgMode) { if (SVG_CAMEL_CASE.test(name)) { name = @@ -637,11 +653,17 @@ function _renderToString( } // write this attribute to the buffer - if (v != null && v !== false && typeof v !== 'function') { - if (v === true || v === '') { + if (v != null && v !== false) { + if (v === true || v === EMPTY_STR) { s = s + ' ' + name; } else { - s = s + ' ' + name + '="' + encodeEntities(v + '') + '"'; + s = + s + + ' ' + + name + + '="' + + (typeof v === 'string' ? encodeEntities(v) : v + EMPTY_STR) + + '"'; } } } @@ -687,7 +709,7 @@ function _renderToString( const endTag = ''; const startTag = s + '>'; - if (Array.isArray(html)) return [startTag, ...html, endTag]; + if (isArray(html)) 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 fd86e5d6..691373c5 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -33,12 +33,12 @@ export function encodeEntities(str) { continue; } // Append skipped/buffered characters and the encoded entity: - if (i !== last) out += str.slice(last, i); - out += ch; + if (i !== last) out = out + str.slice(last, i); + out = out + ch; // Start the next seek/buffer after the entity's offset: last = i + 1; } - if (i !== last) out += str.slice(last, i); + if (i !== last) out = out + str.slice(last, i); return out; }