From b6aa5a717088c9822aa3670e2a42c20fe9418a25 Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Fri, 19 Mar 2021 20:15:11 -0400 Subject: [PATCH 1/2] Performance improvements! --- src/index.js | 73 +++++++++++++++++++++++++--------------------------- src/util.js | 15 ++++------- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/index.js b/src/index.js index e92cf1c0..0b470a73 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,9 @@ import { assign, getChildren } from './util'; -import { options, Fragment, createElement } from 'preact'; +import { options } from 'preact'; + +/** @typedef {import('preact').VNode} VNode */ const SHALLOW = { shallow: true }; @@ -28,7 +30,7 @@ const noop = () => {}; * @param {Boolean} [options.shallow=false] If `true`, renders nested Components as HTML elements (``). * @param {Boolean} [options.xml=false] If `true`, uses self-closing tags for elements without children. * @param {Boolean} [options.pretty=false] If `true`, adds whitespace for readability - * @param {RegEx|undefined} [options.voidElements] RegeEx that matches elements that are considered void (self-closing) + * @param {RegExp|undefined} [options.voidElements] RegeEx that matches elements that are considered void (self-closing) */ renderToString.render = renderToString; @@ -43,6 +45,8 @@ let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW); const EMPTY_ARR = []; function renderToString(vnode, context, opts) { + context = context || {}; + opts = opts || {}; const res = _renderToString(vnode, context, opts); // options._commit, we don't schedule any effects in this library right now, // so we can pass an empty queue to this hook. @@ -56,48 +60,40 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { return ''; } + // #text nodes + if (typeof vnode !== 'object') { + return encodeEntities(vnode); + } + + let pretty = opts.pretty, + indentChar = pretty && typeof pretty === 'string' ? pretty : '\t'; + // wrap array nodes in Fragment if (Array.isArray(vnode)) { - vnode = createElement(Fragment, null, vnode); + let rendered = ''; + for (let i = 0; i < vnode.length; i++) { + if (pretty && i > 0) rendered += '\n'; + rendered += _renderToString( + vnode[i], + context, + opts, + inner, + isSvgMode, + selectValue + ); + } + return rendered; } let nodeName = vnode.type, props = vnode.props, isComponent = false; - context = context || {}; - opts = opts || {}; - - let pretty = opts.pretty, - indentChar = pretty && typeof pretty === 'string' ? pretty : '\t'; - - // #text nodes - if (typeof vnode !== 'object' && !nodeName) { - return encodeEntities(vnode); - } // components if (typeof nodeName === 'function') { isComponent = true; if (opts.shallow && (inner || opts.renderRootComponent === false)) { nodeName = getComponentName(nodeName); - } else if (nodeName === Fragment) { - let rendered = ''; - let children = []; - getChildren(children, vnode.props.children); - - for (let i = 0; i < children.length; i++) { - rendered += - (i > 0 && pretty ? '\n' : '') + - _renderToString( - children[i], - context, - opts, - opts.shallowHighOrder !== false, - isSvgMode, - selectValue - ); - } - return rendered; } else { let rendered; @@ -197,7 +193,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { } // render JSX to HTML - let s = '', + let s = '<' + nodeName, propChildren, html; @@ -215,7 +211,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { continue; } - if (name.match(/[\s\n\\/='"\0<>]/)) continue; + if (UNSAFE_NAME.test(name)) continue; if ( !(opts && opts.allAttributes) && @@ -287,18 +283,19 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { // account for >1 multiline attribute if (pretty) { - let sub = s.replace(/^\n\s*/, ' '); + let sub = s.replace(/\n\s*/, ' '); if (sub !== s && !~sub.indexOf('\n')) s = sub; else if (pretty && ~s.indexOf('\n')) s += '\n'; } - s = `<${nodeName}${s}>`; - if (UNSAFE_NAME.test(String(nodeName))) + s += '>'; + + if (UNSAFE_NAME.test(nodeName)) throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`); let isVoid = - VOID_ELEMENTS.test(String(nodeName)) || - (opts.voidElements && opts.voidElements.test(String(nodeName))); + VOID_ELEMENTS.test(nodeName) || + (opts.voidElements && opts.voidElements.test(nodeName)); let pieces = []; let children; diff --git a/src/util.js b/src/util.js index 4e403552..4e9805c1 100644 --- a/src/util.js +++ b/src/util.js @@ -1,17 +1,12 @@ // DOM properties that should NOT have "px" added when numeric export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i; -const HTML_ENTITY_REG = /[&<>"]/g; -const tagsToReplace = { - '&': '&', - '<': '<', - '>': '>', - '"': '"' -}; -const replaceTag = (tag) => tagsToReplace[tag] || tag; export function encodeEntities(s) { - if (typeof s !== 'string') s = String(s); - return s.replace(HTML_ENTITY_REG, replaceTag); + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); } export let indent = (s, char) => From 95e4bc82d9551380c1e971cc01ba27e8da72478e Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Thu, 25 Mar 2021 14:23:58 -0400 Subject: [PATCH 2/2] Reintroduce Fragment optimization --- src/index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 0b470a73..6d8ba167 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ import { assign, getChildren } from './util'; -import { options } from 'preact'; +import { options, Fragment } from 'preact'; /** @typedef {import('preact').VNode} VNode */ @@ -68,7 +68,6 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { let pretty = opts.pretty, indentChar = pretty && typeof pretty === 'string' ? pretty : '\t'; - // wrap array nodes in Fragment if (Array.isArray(vnode)) { let rendered = ''; for (let i = 0; i < vnode.length; i++) { @@ -94,6 +93,17 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { isComponent = true; if (opts.shallow && (inner || opts.renderRootComponent === false)) { nodeName = getComponentName(nodeName); + } else if (nodeName === Fragment) { + const children = []; + getChildren(children, vnode.props.children); + return _renderToString( + children, + context, + opts, + opts.shallowHighOrder !== false, + isSvgMode, + selectValue + ); } else { let rendered;