Optimized JSX (v2)

Revision 2 of this benchmark created on


Preparation HTML

<h3>log</h3>
<pre id="log" style="border: 1px solid black"></pre>
<h1>Classic JSX Output using h() function</h1>
<textarea id="classicJsxOutput" cols="80" rows="20"></textarea>
<h1>Optimized JSX Output using arrays</h1>
<textarea id="optimizedJsxOutput" cols="80" rows="20"></textarea>

Setup

const tags =
  'a abbr acronym address applet area article aside audio b base basefont bdi bdo bgsound big blink blockquote body br button canvas caption center cite code col colgroup content data datalist dd decorator del details dfn dir div dl dt element em embed fieldset figcaption figure font footer form frame frameset h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins isindex kbd keygen label legend li link listing main map mark marquee menu menuitem meta meter nav nobr noframes noscript object ol optgroup option output p param plaintext pre progress q rp rt ruby s samp script section select shadow small source spacer span strike strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track tt u ul var video wbr xmp'.split(
    ' '
  );

log('Initializing....');
const DEPTH = 5;
const REPS = 20;
const setAttribute = HTMLElement.prototype.setAttribute;
const appendChild = Node.prototype.appendChild;

const attrs = new Array(500)
  .fill('')
  .map((_, i) => 'a' + Number(i).toString(36));

const attrValues = attrs.map((v) => v.toUpperCase());

function roundRobinFactor(array) {
  let idx = 0;
  return Object.assign(
    function () {
      const value = array[idx++];
      if (idx === array.length) idx = 0;
      return value;
    },
    { reset: () => (idx = 0) }
  );
}

const getTagName = roundRobinFactor(tags);
const getAttrName = roundRobinFactor(attrs);
const getAttrValueName = roundRobinFactor(attrValues);

function generateJsxTestCase(output, depth) {
  return output(
    getTagName(), // tag
    [
      getAttrName(),
      getAttrValueName(), // 1
      getAttrName(),
      getAttrValueName(), // 2
      getAttrName(),
      getAttrValueName(), // 3
      getAttrName(),
      getAttrValueName(), // 4
    ],
    depth > 0
      ? [
          generateJsxTestCase(output, depth - 1), // 1
          generateJsxTestCase(output, depth - 1), // 2
          generateJsxTestCase(output, depth - 1), // 3
          generateJsxTestCase(output, depth - 1), // 4
          generateJsxTestCase(output, depth - 1), // 5
          generateJsxTestCase(output, depth - 1), // 6
          // generateJsxTestCase(output, depth - 1), // 7
          // generateJsxTestCase(output, depth - 1), // 8
          // generateJsxTestCase(output, depth - 1), // 9
          // generateJsxTestCase(output, depth - 1), // 10
        ]
      : []
  );
}

function outputJSXClassic(tagName, attrs, children) {
  const output = ['h("', tagName, '", {'];

  for (let i = 0; i < attrs.length; ) {
    output.push(attrs[i++]);
    output.push(':');
    output.push('"', attrs[i++], '"');
    if (i < attrs.length) output.push(', ');
  }
  output.push('}');
  if (children.length) {
    output.push(',\n');
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      output.push(
        child
          .split('\n')
          .map((line) => '  ' + line)
          .join('\n'),
        i < children.length - 1 ? ',\n' : '\n'
      );
    }
  }
  output.push(')');
  return output.join('');
}

function outputJSXOptimized(tagName, attrs, children) {
  const output = ['["', tagName, '"'];
  for (let i = 0; i < attrs.length; ) {
    output.push(',');
    output.push(JSON.stringify(attrs[i++]));
    output.push(',');
    output.push(JSON.stringify(attrs[i++]));
  }
  if (children && children.length) {
    output.push(',\n');
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      output.push(
        child
          .split('\n')
          .map((line) => '  ' + line)
          .join('\n'),
        i < children.length - 1 ? ',\n' : '\n'
      );
    }
  }

  output.push(']');
  return output.join('');
}

log('Generating h() function JSX....');

const jsxCodeClassic = new Function(
  'h',
  'return () => ' + generateJsxTestCase(outputJSXClassic, DEPTH)
)(h);

log('Generating array based JSX....');
getTagName.reset();
getAttrName.reset();
getAttrValueName.reset();

const jsxCodeOptimized = new Function(
  'h',
  'return () => ' + generateJsxTestCase(outputJSXOptimized, DEPTH)
)(h);

// document.getElementById('classicJsxOutput').value = jsxCodeClassic.toString();
// document.getElementById('optimizedJsxOutput').value = jsxCodeOptimized.toString();

function renderDOMClassic(parent, jsx) {
  const element = document.createElement(jsx.tag);
  parent.appendChild(element);
  const props = jsx.normalizedProps;
  let children = null;
  props &&
    Object.keys(props).forEach((key) => {
      if (key === 'children') children = props[key];
      else element.setAttribute(key, props[key]);
    });
  children && children.forEach((child) => renderDOMClassic(element, child));
  return parent;
}

function renderDOMOptimized(parent, jsx) {
  const element = document.createElement(jsx[0]);
  appendChild.call(parent, element);
  const length = jsx.length;
  for (let i = 1; i < length; ) {
    const keyOrChildren = jsx[i++];
    if (typeof keyOrChildren === 'string') {
      const value = jsx[i++];
      setAttribute.call(element, keyOrChildren, value);
    } else {
      renderDOMOptimized(element, keyOrChildren);
    }
  }
  return parent;
}

function log(text) {
  //logAppend(text + '\n');
}
function logAppend(text) {
  //document.getElementById('log').textContent += text;
}

function h(tag, props, ...children) {
  return { tag, normalizedProps: { ...props, children } };
}

Test runner

Ready to run.

Testing in
TestOps/sec
Classical JSX
renderDOMClassic(
  document.createElement('div'),
  jsxCodeClassic()
);
ready
Optimized JSX
renderDOMOptimized(
  document.createElement('div'),
  jsxCodeOptimized()
);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.