Regexp vs Parser

JavaScript performance comparison

Test case created by Jamie Hill

Info

Comparing a parser written with regexp's with a proper parser.

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 unknown unknown
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. Here’s a list of current revisions for this page:

0 comments

Add a comment