Skip to content

Commit 8cf7cef

Browse files
authored
Perf improvements v2 (preactjs#278)
* more perf improvements * remove * undo loop change * put self closing in a set * inline * remove cross * add missing void el * create non-dimensional cahce * last bits * Create thick-islands-share.md
1 parent fac1544 commit 8cf7cef

File tree

4 files changed

+61
-34
lines changed

4 files changed

+61
-34
lines changed

.changeset/thick-islands-share.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"preact-render-to-string": patch
3+
---
4+
5+
Improve performance by
6+
7+
- storing the void_elements in a Set
8+
- hoisting the `x-link` regex
9+
- remove case-insensitive from regexes and calling `.toLowerCase()` instead
10+
- caching suffixes for css-props

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.js

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ export default function renderToString(vnode, context) {
4747
parent[CHILDREN] = [vnode];
4848

4949
try {
50-
return _renderToString(vnode, context || {}, false, undefined, parent);
50+
return _renderToString(
51+
vnode,
52+
context || EMPTY_OBJ,
53+
false,
54+
undefined,
55+
parent
56+
);
5157
} finally {
5258
// options._commit, we don't schedule any effects in this library right now,
5359
// so we can pass an empty queue to this hook.
@@ -62,6 +68,8 @@ function markAsDirty() {
6268
this.__d = true;
6369
}
6470

71+
const EMPTY_OBJ = {};
72+
6573
/**
6674
* @param {VNode} vnode
6775
* @param {Record<string, unknown>} context
@@ -79,7 +87,7 @@ function renderClassComponent(vnode, context) {
7987
// turn off stateful re-rendering:
8088
c[DIRTY] = true;
8189

82-
if (c.state == null) c.state = {};
90+
if (c.state == null) c.state = EMPTY_OBJ;
8391

8492
if (c[NEXT_STATE] == null) {
8593
c[NEXT_STATE] = c.state;
@@ -163,8 +171,7 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
163171
component;
164172

165173
// Invoke rendering on Components
166-
let isComponent = typeof type === 'function';
167-
if (isComponent) {
174+
if (typeof type === 'function') {
168175
if (type === Fragment) {
169176
rendered = props.children;
170177
} else {
@@ -307,15 +314,16 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
307314
}
308315
break;
309316

310-
default:
317+
default: {
311318
if (isSvgMode && XLINK.test(name)) {
312-
name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
319+
name = name.toLowerCase().replace(XLINK_REPLACE_REGEX, 'xlink:');
313320
} else if (UNSAFE_NAME.test(name)) {
314321
continue;
315322
} else if (name[0] === 'a' && name[1] === 'r' && v != null) {
316323
// serialize boolean aria-xyz attribute values as strings
317324
v += '';
318325
}
326+
}
319327
}
320328

321329
// write this attribute to the buffer
@@ -349,25 +357,29 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
349357
if (options.unmount) options.unmount(vnode);
350358

351359
// Emit self-closing tag for empty void elements:
352-
if (!html) {
353-
switch (type) {
354-
case 'area':
355-
case 'base':
356-
case 'br':
357-
case 'col':
358-
case 'embed':
359-
case 'hr':
360-
case 'img':
361-
case 'input':
362-
case 'link':
363-
case 'meta':
364-
case 'param':
365-
case 'source':
366-
case 'track':
367-
case 'wbr':
368-
return s + ' />';
369-
}
360+
if (!html && SELF_CLOSING.has(type)) {
361+
return s + ' />';
370362
}
371363

372364
return s + '>' + html + '</' + type + '>';
373365
}
366+
367+
const XLINK_REPLACE_REGEX = /^xlink:?/;
368+
const SELF_CLOSING = new Set([
369+
'area',
370+
'base',
371+
'br',
372+
'col',
373+
'command',
374+
'embed',
375+
'hr',
376+
'img',
377+
'input',
378+
'keygen',
379+
'link',
380+
'meta',
381+
'param',
382+
'source',
383+
'track',
384+
'wbr'
385+
]);

src/util.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
// DOM properties that should NOT have "px" added when numeric
2-
export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i;
31
export const VOID_ELEMENTS = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
42
export const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
53
export const XLINK = /^xlink:?./;
64

5+
// DOM properties that should NOT have "px" added when numeric
6+
const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/;
77
const ENCODED_ENTITIES = /["&<]/;
88

99
/** @param {string} str */
@@ -50,6 +50,7 @@ export let isLargeString = (s, length, ignoreLines) =>
5050
String(s).indexOf('<') !== -1;
5151

5252
const JS_TO_CSS = {};
53+
const SUFFIX_CACHE = {};
5354

5455
const CSS_REGEX = /([A-Z])/g;
5556
// Convert an Object style to a CSSText string
@@ -64,14 +65,17 @@ export function styleObjToCss(s) {
6465
: JS_TO_CSS[prop] ||
6566
(JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$1').toLowerCase());
6667

67-
str =
68-
str +
69-
name +
70-
':' +
71-
val +
72-
(typeof val === 'number' && IS_NON_DIMENSIONAL.test(prop) === false
73-
? 'px;'
74-
: ';');
68+
let suffix = ';';
69+
if (SUFFIX_CACHE[name]) {
70+
suffix = 'px';
71+
} else if (
72+
typeof val === 'number' &&
73+
IS_NON_DIMENSIONAL.test(prop.toLowerCase()) === false
74+
) {
75+
SUFFIX_CACHE[name] = true;
76+
suffix = 'px';
77+
}
78+
str = str + name + ':' + val + suffix;
7579
}
7680
}
7781
return str || undefined;

0 commit comments

Comments
 (0)