jhildenbiddle / css-vars-ponyfill

Client-side support for CSS custom properties (aka "CSS variables") in legacy and modern browsers

Home Page:https://jhildenbiddle.github.io/css-vars-ponyfill

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Option to skip formatting CSS

helgenlechner opened this issue · comments

Thank you for this great library! We've been using it for a couple years now. I've been doing some performance debugging recently and noticed that quite a lot of time is spent on "sanitizing" the CSS. parseCss alone takes around 2.5 seconds in our case. Since we always roll out minified CSS, all the cleaning and trimming in the function selector, for example, doesn't result in a change. I've tried simply skipping that step and was able to reduce the time needed by parseCss by over 50%.

I believe that minifying CSS is quite a common use case these days. What do you think about adding an option to the ponyfill that would skip all these sanitizing steps? Maybe it could even be recommended as a best practice in the ReadMe, since this would improve runtime performance considerably.

I'm happy to work on this, since the improvements would be worthwhile for our projects. Do you perhaps have a sense of what parts of the code could be skipped with this option?

Thank you for the kind words, @helgenlechner. Much appreciated, and happy to hear the ponyfill has worked well for you.

The behavior in the selector() function you are referring to is used to detect individual selectors within in a comma-separated list of selectors. Sanitized CSS is a side effect, not the goal. There are definitely optimizations that can be done though.

First, I've copied the selector() source below and added comments to explain each mutation:

// Match selector
const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/);

if (m) {
    return m[0]
        .trim()
        // Remove comments
        .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
        // Replace comma in comma-separated lists with marker
        .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
            return m.replace(/,/g, '\u200C');
        })
        // Create array from comma-separated list of selectors
        .split(/\s*(?![^(]*\)),\s*/)
        // Restore comma in comma-separated lists
        .map(function(s) {
            return s.replace(/\u200C/g, ',');
        });
}

Here are a few CSS selector examples that would trip up the parser without the code above:

html,
/* :root, */
body {
 ...
}

.foo,
.bar, /* Comment, with a comma */
.baz {
  ...
}

body[attr="foo,bar"] { ... }

One issue with the selector() code above is that it processes all selectors the same way. Performance can be improved by implementing simple checks first to determine if more complex mutations need to be done. For example, if a simple test for /* within the selector string fails, we don't need execute a complex regular expression to replace all comments in a selector. The end result is that complex selectors may take bit longer, but simple selectors will be processed much faster. I've implemented these changes in branch 160:

function selector() {
whitespace();
while (css[0] === '}') {
error('extra closing bracket');
}
// Match selector
const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/);
if (m) {
let selector = m[0].trim();
let selectorItems;
const hasComment = /\/\*/.test(selector);
if (hasComment) {
// Remove comments
selector = selector.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '');
}
const hasCommaInQuotes = /["']\w*,\w*["']/.test(selector);
if (hasCommaInQuotes) {
// Replace comma in comma-separated lists with marker
selector = selector.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
return m.replace(/,/g, '\u200C');
});
}
const hasMultipleSelectors = /,/.test(selector);
// Create array of selectors
if (hasMultipleSelectors) {
// From comma-separated list
selectorItems = selector.split(/\s*(?![^(]*\)),\s*/);
}
else {
selectorItems = [selector];
}
if (hasCommaInQuotes) {
// Restore comma in comma-separated lists
selectorItems = selectorItems.map(function(s) {
return s.replace(/\u200C/g, ',');
});
}
return selectorItems;
}
}

Feel free to check out the branch, run npm build, and test the results of the optimizations with your project. For reference, I ran a few rudimentary tests locally and saw a 50% increase in selector() performance. If all goes well, I'm happy to merge these changes and publish an update.

Hi @jhildenbiddle! Looks like a great solution! I'm getting a nice performance improvement as well. Nice that a new option isn't needed either. Thank you for the quick help!

@helgenlechner --

Published as 2.4.6. The feature branch has been removed, so be sure to update your package.json accordingly.