Tweaks to rnocache (v9)

Revision 9 of this benchmark created by Charlie on


Description

In jQuery 1.6.x and previous the .html() function uses an variable called rnocache for determining problematic html tags for both caching and for the use of innerHtml. The problem is that these two purposes share very little overlap, and conflating them results in not using innerHtml in places where it's perfectly safe, and much faster. A fix for this has gone into jQuery 1.7.

For discussion and bug see:

To implement the test, I extend jquery 1.7 with a .html2() function which is a direct copy of the version of .html() 1.7 directly before the patch which addresses this issue.

In 1.7 there is a new variable,

rnoInnerhtml = /<script|<style/i

which I use in place of rnocache within .html(). This change allows me to then copy a select box using innerHTML rather than .empty().append() which results in some rather large speed increases.

Preparation HTML

<script src="https://code.jquery.com/jquery-1.7b1.js"></script>
<script>
var $17pre = jQuery.noConflict();
</script>

<script src="https://code.jquery.com/jquery-1.7b1.js"></script>
<script>
var $17post = jQuery.noConflict();
</script>

<script src="https://code.jquery.com/jquery-1.7b1.js"></script>
<script>
var $17noWrap= jQuery.noConflict();
</script>

<div>Original: <div id="source"><select id="test_select" name="test_select">
        <option label="1">0</option>
        <option label="1">1</option>
        <option label="2">2</option>
        <option label="3" selected="selected">3</option>
        <option label="4">4</option>
        <option label="1">5</option>
        <option label="2">6</option>
        <option label="3">7</option>
        <option label="4">8</option>
        <option label="1">9</option>
</select>
</div></div>
Following two selects should be identical:
<div>Unoptimized copy: <div id="destination"></div></div>
<div>Optimized copy:<div id="destination2"></div></div>
<div>Optimized copy:<div id="destination3"></div></div>
<script>
  (function($) {
  
  var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
        rleadingWhitespace = /^\s+/,
        rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
        rtagName = /<([\w:]+)/,
        rtbody = /<tbody/i,
        rhtml = /<|&#?\w+;/,
        rnocache = /<(?:script|object|embed|option|style)/i,
        // checked="checked" or checked
        rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
        rscriptType = /\/(java|ecma)script/i,
        rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/,
        wrapMap = {
                option: [ 1, "<select multiple='multiple'>", "</select>" ],
                legend: [ 1, "<fieldset>", "</fieldset>" ],
                thead: [ 1, "<table>", "</table>" ],
                tr: [ 2, "<table><tbody>", "</tbody></table>" ],
                td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
                col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
                area: [ 1, "<map>", "</map>" ],
                _default: [ 0, "", "" ]
        },
        safeFragment = (function() {
                var nodeNames = (
                        "abbr article aside audio canvas datalist details figcaption figure footer " +
                        "header hgroup mark meter nav output progress section summary time video"
                ).split( " " ),
                safeFrag = document.createDocumentFragment();
  
                if ( safeFrag.createElement ) {
                        while ( nodeNames.length ) {
                                safeFrag.createElement(
                                        nodeNames.pop()
                                );
                        }
                }
                return safeFrag;
        })();
  
  wrapMap.optgroup = wrapMap.option;
  wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
  wrapMap.th = wrapMap.td;
  
  // IE can't serialize <link> and <script> tags normally
  if ( !jQuery.support.htmlSerialize ) {
        wrapMap._default = [ 1, "div<div>", "</div>" ];
  }
  
   $.fn.html = function(value) {
  
                if ( value === undefined ) {
                        return this[0] && this[0].nodeType === 1 ?
                                this[0].innerHTML.replace(rinlinejQuery, "") :
                                null;
  
                // See if we can take a shortcut and just use innerHTML
                } else if ( typeof value === "string" && !rnocache.test( value ) &&
                        (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
                        !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
  
                        value = value.replace(rxhtmlTag, "<$1></$2>");
  
                        try {
                                for ( var i = 0, l = this.length; i < l; i++ ) {
                                        // Remove element nodes and prevent memory leaks
                                        if ( this[i].nodeType === 1 ) {
                                                jQuery.cleanData( this[i].getElementsByTagName("*") );
                                                this[i].innerHTML = value;
                                        }
                                }
  
                        // If using innerHTML throws an exception, use the fallback method
                        } catch(e) {
                                this.empty().append( value );
                        }
  
                } else if ( jQuery.isFunction( value ) ) {
                        this.each(function(i){
                                var self = jQuery( this );
  
                                self.html( value.call(this, i, self.html()) );
                        });
  
                } else {
                        this.empty().append( value );
                }
  
                return this;
  
   };
  })($17pre);
  
  
  (function($){
  
  
  
  var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
        rleadingWhitespace = /^\s+/,
        rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
        rtagName = /<([\w:]+)/,
        rtbody = /<tbody/i,
        rhtml = /<|&#?\w+;/,
        rnocache = /<(?:script|object|embed|option|style)/i,
        // checked="checked" or checked
        rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
        rscriptType = /\/(java|ecma)script/i,
        rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/,
        wrapMap = {
                option: [ 1, "<select multiple='multiple'>", "</select>" ],
                legend: [ 1, "<fieldset>", "</fieldset>" ],
                thead: [ 1, "<table>", "</table>" ],
                tr: [ 2, "<table><tbody>", "</tbody></table>" ],
                td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
                col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
                area: [ 1, "<map>", "</map>" ],
                _default: [ 0, "", "" ]
        },
        safeFragment = (function() {
                var nodeNames = (
                        "abbr article aside audio canvas datalist details figcaption figure footer " +
                        "header hgroup mark meter nav output progress section summary time video"
                ).split( " " ),
                safeFrag = document.createDocumentFragment();
  
                if ( safeFrag.createElement ) {
                        while ( nodeNames.length ) {
                                safeFrag.createElement(
                                        nodeNames.pop()
                                );
                        }
                }
                return safeFrag;
        })();
  
  wrapMap.optgroup = wrapMap.option;
  wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
  wrapMap.th = wrapMap.td;
  
  // IE can't serialize <link> and <script> tags normally
  if ( !jQuery.support.htmlSerialize ) {
        wrapMap._default = [ 1, "div<div>", "</div>" ];
  }
  
   $.fn.html = function(value) {
  
                if ( value === undefined ) {
                        return this[0] && this[0].nodeType === 1 ?
                                this[0].innerHTML.replace(rinlinejQuery, "") :
                                null;
  
                // See if we can take a shortcut and just use innerHTML
                } else if ( typeof value === "string" && !rnocache.test( value ) &&
                        (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
                        !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
  
                        value = value.replace(rxhtmlTag, "<$1></$2>");
  
                        try {
                                for ( var i = 0, l = this.length; i < l; i++ ) {
                                        // Remove element nodes and prevent memory leaks
                                        if ( this[i].nodeType === 1 ) {
                                                jQuery.cleanData( this[i].getElementsByTagName("*") );
                                                this[i].innerHTML = value;
                                        }
                                }
  
                        // If using innerHTML throws an exception, use the fallback method
                        } catch(e) {
                                this.empty().append( value );
                        }
  
                } else if ( jQuery.isFunction( value ) ) {
                        this.each(function(i){
                                var self = jQuery( this );
  
                                self.html( value.call(this, i, self.html()) );
                        });
  
                } else {
                        this.empty().append( value );
                }
  
                return this;
  
   };
  
  
  
   $.fn.clean = function( elems, context, fragment, scripts ) {
  console.log('clean()');
  var checkScriptType;
  
  context = context || document;
  
  // !context.createElement fails in IE with an error but returns typeof 'object'
  if ( typeof context.createElement === "undefined" ) {
  context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
  }
  
  var ret = [], j;
  
  for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
  if ( typeof elem === "number" ) {
  elem += "";
  }
  
  if ( !elem ) {
  continue;
  }
  
  // Convert html string into DOM nodes
  if ( typeof elem === "string" ) {
  if ( !rhtml.test( elem ) ) {
  console.log('not rhtml');
  elem = context.createTextNode( elem );
  } else {
  // Fix "XHTML"-style tags in all browsers
  elem = elem.replace(rxhtmlTag, "<$1></$2>");
  
  // Trim whitespace, otherwise indexOf won't work as expected
  var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
  wrap = wrapMap[ tag ] || wrapMap._default,
  depth = wrap[0],
  div = context.createElement("div");
  
  // Append wrapper element to unknown element safe doc fragment
  if ( context === document ) {
  // Use the fragment we've already created for this document
  safeFragment.appendChild( div );
  } else {
  // Use a fragment created with the owner document
  createSafeFragment( context ).appendChild( div );
  }
  
  // Go to html and back, then peel off extra wrappers
  div.innerHTML = wrap[1] + elem + wrap[2];
  
  // Move to the right depth
  while ( depth-- ) {
  div = div.lastChild;
  }
  
  // Remove IE's autoinserted <tbody> from table fragments
  if ( !jQuery.support.tbody ) {
  
  // String was a <table>, *may* have spurious <tbody>
  var hasBody = rtbody.test(elem),
  tbody = tag === "table" && !hasBody ?
  div.firstChild && div.firstChild.childNodes :
  
  // String was a bare <thead> or <tfoot>
  wrap[1] === "<table>" && !hasBody ?
  div.childNodes :
  [];
  
  for ( j = tbody.length - 1; j >= 0 ; --j ) {
  if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
  tbody[ j ].parentNode.removeChild( tbody[ j ] );
  }
  }
  }
  
  // IE completely kills leading whitespace when innerHTML is used
  if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
  div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
  }
  
  elem = div.childNodes;
  }
  }
  
  // Resets defaultChecked for any radios and checkboxes
  // about to be appended to the DOM in IE 6/7 (#8060)
  var len;
  if ( !jQuery.support.appendChecked ) {
  if ( elem[0] && typeof (len = elem.length) === "number" ) {
  for ( j = 0; j < len; j++ ) {
  findInputs( elem[j] );
  }
  } else {
  findInputs( elem );
  }
  }
  
  if ( elem.nodeType ) {
  ret.push( elem );
  } else {
  ret = jQuery.merge( ret, elem );
  }
  }
  
  if ( fragment ) {
  checkScriptType = function( elem ) {
  return !elem.type || rscriptType.test( elem.type );
  };
  for ( i = 0; ret[i]; i++ ) {
  if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
  scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
  
  } else {
  if ( ret[i].nodeType === 1 ) {
  var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType );
  
  ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
  }
  fragment.appendChild( ret[i] );
  }
  }
  }
  
  return ret;
  }
  
  })($17noWrap);
  
  
  
  var template = jQuery("#source").html();
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
using innerHTML optimized regex
$17post("#destination").html(template);
ready
using nocache (no optimization)
$17pre("#destination2").html(template);
ready
a different tweak
$17noWrap("#destination3").html(template);
ready

Revisions

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

  • Revision 1: published by Charles McNulty on
  • Revision 7: published by Charles McNulty on
  • Revision 8: published by Charlie on
  • Revision 9: published by Charlie on