Skip to content

Commit 883e02b

Browse files
authored
chore: general performance optimisations (#383)
1 parent fe73d71 commit 883e02b

File tree

6 files changed

+83
-60
lines changed

6 files changed

+83
-60
lines changed

.changeset/calm-cherries-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"preact-render-to-string": patch
3+
---
4+
5+
General performance optimisations

benchmarks/index.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import renderToStringBaseline from 'baseline-rts';
44
// import renderToString from '../src/index';
55
import renderToString from '../dist/index.module.js';
66
import TextApp from './text';
7-
// import StackApp from './stack';
7+
import StackApp from './stack';
88
import { App as IsomorphicSearchResults } from './isomorphic-ui/search-results/index';
99
import { App as ColorPicker } from './isomorphic-ui/color-picker';
1010

@@ -19,6 +19,5 @@ function suite(name, Root) {
1919
await suite('Text', TextApp);
2020
await suite('SearchResults', IsomorphicSearchResults);
2121
await suite('ColorPicker', ColorPicker);
22-
// TODO: Enable this once we switched away from recursion
23-
// await suite('Stack Depth', StackApp);
22+
await suite('Stack Depth', StackApp);
2423
})();

package-lock.json

Lines changed: 6 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
"@babel/register": "^7.12.10",
133133
"@changesets/changelog-github": "^0.4.1",
134134
"@changesets/cli": "^2.18.0",
135-
"baseline-rts": "npm:preact-render-to-string@latest",
135+
"baseline-rts": "npm:preact-render-to-string@6.5.7",
136136
"benchmarkjs-pretty": "^2.0.1",
137137
"chai": "^4.2.0",
138138
"check-export-map": "^1.3.1",

src/index.js

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
const EMPTY_ARR = [];
2626
const isArray = Array.isArray;
2727
const assign = Object.assign;
28+
const EMPTY_STR = '';
2829

2930
// Global state for the current render pass
3031
let beforeDiff, afterDiff, renderHook, ummountHook;
@@ -65,8 +66,8 @@ export function renderToString(vnode, context, _rendererState) {
6566
_rendererState
6667
);
6768

68-
if (Array.isArray(rendered)) {
69-
return rendered.join('');
69+
if (isArray(rendered)) {
70+
return rendered.join(EMPTY_STR);
7071
}
7172
return rendered;
7273
} catch (e) {
@@ -119,7 +120,7 @@ export async function renderToStringAsync(vnode, context) {
119120
undefined
120121
);
121122

122-
if (Array.isArray(rendered)) {
123+
if (isArray(rendered)) {
123124
let count = 0;
124125
let resolved = rendered;
125126

@@ -133,7 +134,7 @@ export async function renderToStringAsync(vnode, context) {
133134
resolved = (await Promise.all(resolved)).flat();
134135
}
135136

136-
return resolved.join('');
137+
return resolved.join(EMPTY_STR);
137138
}
138139

139140
return rendered;
@@ -226,19 +227,26 @@ function _renderToString(
226227
renderer
227228
) {
228229
// Ignore non-rendered VNodes/values
229-
if (vnode == null || vnode === true || vnode === false || vnode === '') {
230-
return '';
230+
if (
231+
vnode == null ||
232+
vnode === true ||
233+
vnode === false ||
234+
vnode === EMPTY_STR
235+
) {
236+
return EMPTY_STR;
231237
}
232238

233239
// Text VNodes: escape as HTML
234240
if (typeof vnode !== 'object') {
235-
if (typeof vnode === 'function') return '';
236-
return encodeEntities(vnode + '');
241+
if (typeof vnode === 'function') return EMPTY_STR;
242+
return typeof vnode === 'string'
243+
? encodeEntities(vnode)
244+
: vnode + EMPTY_STR;
237245
}
238246

239247
// Recurse into children / Arrays
240248
if (isArray(vnode)) {
241-
let rendered = '',
249+
let rendered = EMPTY_STR,
242250
renderArray;
243251
parent[CHILDREN] = vnode;
244252
for (let i = 0; i < vnode.length; i++) {
@@ -256,15 +264,15 @@ function _renderToString(
256264
);
257265

258266
if (typeof childRender === 'string') {
259-
rendered += childRender;
267+
rendered = rendered + childRender;
260268
} else {
261269
renderArray = renderArray || [];
262270

263271
if (rendered) renderArray.push(rendered);
264272

265-
rendered = '';
273+
rendered = EMPTY_STR;
266274

267-
if (Array.isArray(childRender)) {
275+
if (isArray(childRender)) {
268276
renderArray.push(...childRender);
269277
} else {
270278
renderArray.push(childRender);
@@ -281,7 +289,7 @@ function _renderToString(
281289
}
282290

283291
// VNodes have {constructor:undefined} to prevent JSON injection:
284-
if (vnode.constructor !== undefined) return '';
292+
if (vnode.constructor !== undefined) return EMPTY_STR;
285293

286294
vnode[PARENT] = parent;
287295
if (beforeDiff) beforeDiff(vnode);
@@ -298,9 +306,9 @@ function _renderToString(
298306
if (type === Fragment) {
299307
// Serialized precompiled JSX.
300308
if (props.tpl) {
301-
let out = '';
309+
let out = EMPTY_STR;
302310
for (let i = 0; i < props.tpl.length; i++) {
303-
out += props.tpl[i];
311+
out = out + props.tpl[i];
304312

305313
if (props.exprs && i < props.exprs.length) {
306314
const value = props.exprs[i];
@@ -311,18 +319,20 @@ function _renderToString(
311319
typeof value === 'object' &&
312320
(value.constructor === undefined || isArray(value))
313321
) {
314-
out += _renderToString(
315-
value,
316-
context,
317-
isSvgMode,
318-
selectValue,
319-
vnode,
320-
asyncMode,
321-
renderer
322-
);
322+
out =
323+
out +
324+
_renderToString(
325+
value,
326+
context,
327+
isSvgMode,
328+
selectValue,
329+
vnode,
330+
asyncMode,
331+
renderer
332+
);
323333
} else {
324334
// Values are pre-escaped by the JSX transform
325-
out += value;
335+
out = out + value;
326336
}
327337
}
328338
}
@@ -331,7 +341,9 @@ function _renderToString(
331341
} else if (props.UNSTABLE_comment) {
332342
// Fragments are the least used components of core that's why
333343
// branching here for comments has the least effect on perf.
334-
return '<!--' + encodeEntities(props.UNSTABLE_comment || '') + '-->';
344+
return (
345+
'<!--' + encodeEntities(props.UNSTABLE_comment || EMPTY_STR) + '-->'
346+
);
335347
}
336348

337349
rendered = props.children;
@@ -342,11 +354,13 @@ function _renderToString(
342354
cctx = provider ? provider.props.value : contextType.__;
343355
}
344356

345-
if (type.prototype && typeof type.prototype.render === 'function') {
357+
let isClassComponent =
358+
type.prototype && typeof type.prototype.render === 'function';
359+
if (isClassComponent) {
346360
rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx);
347361
component = vnode[COMPONENT];
348362
} else {
349-
component = {
363+
vnode[COMPONENT] = component = {
350364
__v: vnode,
351365
props,
352366
context: cctx,
@@ -357,7 +371,6 @@ function _renderToString(
357371
// hooks
358372
__h: []
359373
};
360-
vnode[COMPONENT] = component;
361374

362375
// If a hook invokes setState() to invalidate the component during rendering,
363376
// re-render it up to 25 times to allow "settling" of memoized states.
@@ -380,10 +393,10 @@ function _renderToString(
380393
}
381394

382395
if (
383-
(type.getDerivedStateFromError || component.componentDidCatch) &&
384-
options.errorBoundaries
396+
isClassComponent &&
397+
options.errorBoundaries &&
398+
(type.getDerivedStateFromError || component.componentDidCatch)
385399
) {
386-
let str = '';
387400
// When a component returns a Fragment node we flatten it in core, so we
388401
// need to mirror that logic here too
389402
let isTopLevelFragment =
@@ -393,7 +406,7 @@ function _renderToString(
393406
rendered = isTopLevelFragment ? rendered.props.children : rendered;
394407

395408
try {
396-
str = _renderToString(
409+
return _renderToString(
397410
rendered,
398411
context,
399412
isSvgMode,
@@ -402,14 +415,15 @@ function _renderToString(
402415
asyncMode,
403416
renderer
404417
);
405-
return str;
406418
} catch (err) {
419+
let str = EMPTY_STR;
420+
407421
if (type.getDerivedStateFromError) {
408422
component[NEXT_STATE] = type.getDerivedStateFromError(err);
409423
}
410424

411425
if (component.componentDidCatch) {
412-
component.componentDidCatch(err, {});
426+
component.componentDidCatch(err, EMPTY_OBJ);
413427
}
414428

415429
if (component[DIRTY]) {
@@ -493,7 +507,7 @@ function _renderToString(
493507

494508
let errorHook = options[CATCH_ERROR];
495509
if (errorHook) errorHook(error, vnode);
496-
return '';
510+
return EMPTY_STR;
497511
}
498512

499513
if (!asyncMode) throw error;
@@ -525,23 +539,25 @@ function _renderToString(
525539
asyncMode,
526540
renderer
527541
),
528-
() => renderNestedChildren()
542+
renderNestedChildren
529543
);
530544
}
531545
};
532546

533-
return error.then(() => renderNestedChildren());
547+
return error.then(renderNestedChildren);
534548
}
535549
}
536550

537551
// Serialize Element VNodes to HTML
538552
let s = '<' + type,
539-
html = '',
553+
html = EMPTY_STR,
540554
children;
541555

542556
for (let name in props) {
543557
let v = props[name];
544558

559+
if (typeof v === 'function') continue;
560+
545561
switch (name) {
546562
case 'children':
547563
children = v;
@@ -622,7 +638,7 @@ function _renderToString(
622638
// serialize boolean aria-xyz or draggable attribute values as strings
623639
// `draggable` is an enumerated attribute and not Boolean. A value of `true` or `false` is mandatory
624640
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable
625-
v += '';
641+
v = v + EMPTY_STR;
626642
} else if (isSvgMode) {
627643
if (SVG_CAMEL_CASE.test(name)) {
628644
name =
@@ -637,11 +653,17 @@ function _renderToString(
637653
}
638654

639655
// write this attribute to the buffer
640-
if (v != null && v !== false && typeof v !== 'function') {
641-
if (v === true || v === '') {
656+
if (v != null && v !== false) {
657+
if (v === true || v === EMPTY_STR) {
642658
s = s + ' ' + name;
643659
} else {
644-
s = s + ' ' + name + '="' + encodeEntities(v + '') + '"';
660+
s =
661+
s +
662+
' ' +
663+
name +
664+
'="' +
665+
(typeof v === 'string' ? encodeEntities(v) : v + EMPTY_STR) +
666+
'"';
645667
}
646668
}
647669
}
@@ -687,7 +709,7 @@ function _renderToString(
687709
const endTag = '</' + type + '>';
688710
const startTag = s + '>';
689711

690-
if (Array.isArray(html)) return [startTag, ...html, endTag];
712+
if (isArray(html)) return [startTag, ...html, endTag];
691713
else if (typeof html !== 'string') return [startTag, html, endTag];
692714
return startTag + html + endTag;
693715
}

src/lib/util.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ export function encodeEntities(str) {
3333
continue;
3434
}
3535
// Append skipped/buffered characters and the encoded entity:
36-
if (i !== last) out += str.slice(last, i);
37-
out += ch;
36+
if (i !== last) out = out + str.slice(last, i);
37+
out = out + ch;
3838
// Start the next seek/buffer after the entity's offset:
3939
last = i + 1;
4040
}
41-
if (i !== last) out += str.slice(last, i);
41+
if (i !== last) out = out + str.slice(last, i);
4242
return out;
4343
}
4444

0 commit comments

Comments
 (0)