Regexp vs Parser

JavaScript performance comparison

Test case created by Jamie Hill

Preparation code


      
      <script>
Benchmark.prototype.setup = function() {
  var css = '';
  for (var i = 100; i > 0; i--) {
    css += "h1 {\n  color: red;\n  }\n\n  .some-class {\n  color: #ff0;\n  background: #f00;\n\n  .nested {\n  color: blue;\n\n  .another {\n    background: pink;\n  }\n  }\n  }\n\n"
  }
  
  var Rule,
      openRe = /((([\*\@\#\.\w\d])([\s\*\@\#\.\w\d\-\,\>\:\=\"\~\^\$\(\)\+]*))\{)\s*/,
      closeRe = /\s*\}/;
  
  Rule = (function() {
    function Rule(source, selector, start, declarationStart, end, declarationEnd) {
      this.source = source;
      this.selector = selector;
      this.start = start;
      this.declarationStart = declarationStart;
      this.end = end || null;
      this.declarationEnd = declarationEnd || null;
      this.nested = [];
    }
  
    return Rule;
  }());
  
  function regexParser(css) {
    var length = css.length, stack = [], context, nested, match, open, close, rule, rules;
    
    this.cssText = css;
    this.rules = rules = context = [];
  
    while (css) {
      open = css.indexOf('{');
      close = css.indexOf('}');
    
      if (open >= 0 && (close == -1 || open < close) && (match = css.match(openRe))) {
        css = css.slice(open + 1);
        rule = new Rule(this.cssText, match[2].trim(),
                        length - css.length - match[1].length, length - css.length);
        context.push(rule);
        stack.push(context);
        context = rule.nested;
      } else if (close >= 0 && (open == -1 || close < open) && (match = css.match(closeRe))) {
        css = css.slice(close + match.length);
        context = stack.pop();
        rule = context[context.length - 1];
        if (rule) {
          rule.end = length - css.length;
          rule.declarationEnd = rule.end - match.length;
        } else {
          console.log('Closing unopened rule');
        }
      } else {
        css = null;
      }
    }
  
    // Pop remaining
    while (stack.length) {
      context = stack.pop();
      rule = context[context.length - 1];
      rule.end = rule.declarationEnd = length;
      console.log("Unclosed rule: '" + rule.selector + "'");
    }
  
    return rules;
  }
  
  function realParserWithCase(css) {
    var stack = [], rules = [], context = rules, state = 'before-selector',
        buffer = 0, index = 0, rule, char, start;
  
    this.cssText = css;
    this.rules = rules;
  
    while (char = css.charAt(index++)) {          
      switch(char) {
      case ' ': case '\t': case '\r': case '\n': case '\f':
        if (state === 'selector') { buffer++; }
        break;
      case '{':
        if (state !== 'selector') { break; }
        start = index - buffer - 1;
        rule = new Rule(css, css.slice(start, index - 1).trim(), start, index);
        context.push(rule);
        stack.push(context);
        context = rule.nested;
        state = 'before-selector'
        buffer = 0;
        break;
      case ';':
        if (state !== 'selector') { break; }
        state = 'before-selector';
        buffer = 0;
        break;
      case '}':
        if (state !== 'before-selector') { break; }
        context = stack.pop();
        rule = context[context.length - 1];
        if (rule) {
          rule.end = index;
          rule.declarationEnd = index - 1;
        } else {
          console.log('Closing unopened rule');
        }
        state = "before-selector";
        buffer = 0;
        break;
      default:
        if (state === 'before-selector') { state = 'selector'; }
        buffer++;
        break;
      }
    }
    
    // Pop remaining
    while (stack.length) {
      context = stack.pop();
      rule = context[context.length - 1];
      rule.end = rule.declarationEnd = css.length;
      console.log("Unclosed rule: '" + rule.selector + "'");
    }
  
    return rules;
  }
  
  function realParserWithIf(css) {
    var stack = [], rules = [], context = rules, state = 'before-selector',
        buffer = 0, index = 0, rule, char, start;
  
    this.cssText = css;
    this.rules = rules;
  
    while (char = css.charAt(index++)) {          
      if (char === ' ' || char === '\t' || char === '\r' || char === '\n' || char === '\f') {
        if (state === 'selector') { buffer++; }
      } else if (char === '{') {
        if (state === 'selector') {
          start = index - buffer - 1;
          rule = new Rule(css, css.slice(start, index - 1).trim(), start, index);
          context.push(rule);
          stack.push(context);
          context = rule.nested;
          state = 'before-selector'
          buffer = 0;
        }
      } else if (char === ';') {
        if (state === 'selector') {
          state = 'before-selector';
          buffer = 0;
        }
      } else if (char === '}') {
        if (state === 'before-selector') {
          context = stack.pop();
          rule = context[context.length - 1];
          if (rule) {
            rule.end = index;
            rule.declarationEnd = index - 1;
          } else {
            console.log('Closing unopened rule');
          }
          state = "before-selector";
          buffer = 0;
        }
      } else {
        if (state === 'before-selector') { state = 'selector'; }
        buffer++;
      }
    }
    
    // Pop remaining
    while (stack.length) {
      context = stack.pop();
      rule = context[context.length - 1];
      rule.end = rule.declarationEnd = css.length;
      console.log("Unclosed rule: '" + rule.selector + "'");
    }
  
    return rules;
  }
  
  function realParserWithLookup(css) {
    var stack = [], rules = [], context = rules, state = 'before-selector',
        buffer = 0, index = 0, rule, char, start,
        open = {
          selector: function() {
            start = index - buffer - 1;
            rule = new Rule(css, css.slice(start, index - 1).trim(), start, index);
            context.push(rule);
            stack.push(context);
            context = rule.nested;
            state = 'before-selector'
            buffer = 0;
          },
          "before-selector": function() {}
        },
        close = {
          "before-selector": function() {
            context = stack.pop();
            rule = context[context.length - 1];
            if (rule) {
              rule.end = index;
              rule.declarationEnd = index - 1;
            } else {
              console.log('Closing unopened rule');
            }
            state = "before-selector";
            buffer = 0;
          },
          selector: function() {}
        },
        endRule = {
          "before-selector": function() {},
          selector: function() {
            state = 'before-selector';
            buffer = 0;
          }
        };
  
    this.cssText = css;
    this.rules = rules;
  
    while (char = css.charAt(index++)) {          
      switch(char) {
      case ' ': case '\t': case '\r': case '\n': case '\f':
        if (state === 'selector') { buffer++; }
        break;
      case '{':
        open[state]();
        break;
      case ';':
        endRule[state]();
        break;
      case '}':
        close[state]();
        break;
      default:
        if (state === 'before-selector') { state = 'selector'; }
        buffer++;
        break;
      }
    }
    
    // Pop remaining
    while (stack.length) {
      context = stack.pop();
      rule = context[context.length - 1];
      rule.end = rule.declarationEnd = css.length;
      console.log("Unclosed rule: '" + rule.selector + "'");
    }
  
    return rules;
  }

};

Benchmark.prototype.teardown = function() {
  Rule = null;

};
</script>

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in CCBot 2.0.0 / Other 0.0.0
Test Ops/sec
Regexp Parser
regexParser(css);
pending…
Real Parser with "case"
realParserWithCase(css);
pending…
Real Parser with "if"
realParserWithIf(css);
pending…
Real Parser with Lookup
realParserWithLookup(css);
pending…

Compare results of other browsers

Revisions

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

0 Comments