Skip to content

Commit

Permalink
chore: general performance optimisations (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock authored Jul 28, 2024
1 parent fe73d71 commit 883e02b
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 60 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-cherries-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"preact-render-to-string": patch
---

General performance optimisations
5 changes: 2 additions & 3 deletions benchmarks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
})();
15 changes: 6 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
110 changes: 66 additions & 44 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -119,7 +120,7 @@ export async function renderToStringAsync(vnode, context) {
undefined
);

if (Array.isArray(rendered)) {
if (isArray(rendered)) {
let count = 0;
let resolved = rendered;

Expand All @@ -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;
Expand Down Expand Up @@ -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++) {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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];
Expand All @@ -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;
}
}
}
Expand All @@ -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 '<!--' + encodeEntities(props.UNSTABLE_comment || '') + '-->';
return (
'<!--' + encodeEntities(props.UNSTABLE_comment || EMPTY_STR) + '-->'
);
}

rendered = props.children;
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -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 =
Expand All @@ -393,7 +406,7 @@ function _renderToString(
rendered = isTopLevelFragment ? rendered.props.children : rendered;

try {
str = _renderToString(
return _renderToString(
rendered,
context,
isSvgMode,
Expand All @@ -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]) {
Expand Down Expand Up @@ -493,7 +507,7 @@ function _renderToString(

let errorHook = options[CATCH_ERROR];
if (errorHook) errorHook(error, vnode);
return '';
return EMPTY_STR;
}

if (!asyncMode) throw error;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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) +
'"';
}
}
}
Expand Down Expand Up @@ -687,7 +709,7 @@ function _renderToString(
const endTag = '</' + type + '>';
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;
}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down

0 comments on commit 883e02b

Please sign in to comment.