anandthakker / doiuse

:bomb: Lint CSS for browser support against caniuse database.
MIT License
1.24k stars 51 forks source link

Extremely slow when processing non-trivial selectors with `--browsers "ie 6"` #87

Closed alanhussey closed 5 years ago

alanhussey commented 6 years ago

I'm building an "unsupported browser" landing page, so I'm using stylelint-no-unsupported-browser-features to make sure I don't rely on unsupported features. However, when I run stylelint with browsers set to "ie >= 6", it takes so long to run that I've never been patient enough to let it finish. Changing that to "ie >= 7" allows it to complete in a reasonable amount of time (~2 seconds).

Digging in, I've found a minimal repro:

.message .call-to-action a:visited {}

Running doiuse on that file:

time node ./cli.js --browsers "ie 6" repro.css

real    14m54.509s
user    14m48.816s
sys     0m3.060s

time node ./cli.js --browsers "ie 7" repro.css

real    0m0.756s
user    0m0.434s
sys     0m0.089s

For comparison, .message .call-to-action :visited {} (the same selector, no a) takes about half the time for "ie 6", and .message .calltoaction :visited {} (the same selector, but no - or a) takes about a 1/10th of the time.

Digging a little deeper, commenting out all of the matchOutsideOfBrackets selectors for the css-sel2 feature brings the total execution time down to less than 1 second. Removing all but 1 of the matchOutsideOfBrackets selectors brings execution time down to less than 4 minutes:

real    3m43.478s
user    3m41.736s
sys     0m0.878s

This is the part where I get stuck. I don't know much about regex performance, but those regexes have a lot of punctuation, which seems not helpful.

This may be related to #44.

alanhussey commented 6 years ago

After some more investigation, I think this performance issue can be avoided by updating matchOutsideOfBrackets to return a function that first checks if the given regex matches the selector string at all before testing the full regexp that it constructs.

// features.js
function matchOutsideOfBrackets(pat) {
  if (!(pat instanceof RegExp)) {
    throw new TypeError('matchOutsideOfBrackets expects a RegExp');
  }
  var fullPat = new RegExp('^(' + pats.brackets + '?' + pats.nobrackets + '*)*' + pat.source)
  return function match(str) {
    return pat.test(str) && fullPat.test(str)
  }
}

This brings my repro down to <2s, which seems much more reasonable.

If you'd like, I'm happy to submit this as a PR.

anandthakker commented 6 years ago

👍 seems like a reasonable fix to me. PR gladly accepted!