How it works, performance and limits

css-pp is a compiler that runs in the browser. Knowing what it does on page load helps you decide where it fits.

What happens on page load#

  1. Fetch. css.open() loads the stylesheet with XMLHttpRequest, or reads an inline <style>.
  2. Normalize. It strips comments and protects strings, so text inside quotes is never changed.
  3. Flatten. It expands nesting and moves @media blocks out of rules.
  4. Compile selectors. It turns state and content selectors into attribute selectors and records which elements to watch.
  5. Resolve variables. It substitutes values, runs JavaScript expressions and writes one rule per element for per-element values.
  6. Inject. It writes the result into a <style> element. From here on, the browser's normal CSS engine does all the matching.
  7. Watch. It starts observers only for the features you used, and only on the elements that can match.

The output is plain CSS. You can see it in your browser's developer tools by inspecting the <style> element. This site works the same way: its own stylesheet, /assets/site.css, is css-pp source. See how its size compares with the plain CSS version.

Performance#

Measured in Chrome on a desktop:

Stylesheet Size Processing time
A production theme using variables, mixins and nesting 74 KB about 9 ms
Bootstrap 3, plain CSS 107 KB about 6 ms

Expect a few times longer on low-end phones. After that, the cost is the observers. Those are the same browser APIs you would use in hand-written JavaScript, and they are only created for features you use. Rules that only use plain values or nesting cost nothing after the first pass.

Ways to keep it fast:

  • Set css.defaults.localCache = 1 to render returning visitors from a cached copy instantly.
  • Qualify :contains() with a class or tag (.badge:contains(...), not :contains(...)), so fewer elements need checking.
  • Keep plain CSS that doesn't use any css-pp features in a normal <link rel="stylesheet">.

Flash of unstyled content#

With css.defaults.invisible = 1 (the default), css-pp hides <html> until every stylesheet is processed, so visitors never see raw markup. If you would rather render progressively, set it to 0 and load your critical CSS with a normal <link>.

SEO#

Your HTML and text are untouched; css-pp only produces CSS. Search engines that render pages, such as Google, run the script like any other JavaScript. Write hidden-until-revealed rules as .x:not(:seen) { opacity: 0 }. Crawlers and browsers that don't run the script then skip the rule, so the content stays visible to them.

Content Security Policy#

Variable values are evaluated with eval(), which is what makes JavaScript expressions possible. Under a Content Security Policy without 'unsafe-eval':

  • Plain variable values, mixins, nesting, and the state and content selectors keep working.
  • JavaScript expressions, $[index], width() and height(), and the other built-ins that rely on evaluation do not.

Only process stylesheets you control. A stylesheet that can run JavaScript expressions should be trusted like a script.

Known limitations#

  • Variables can't be used inside @media conditions, such as @media (min-width: $bp).
  • css.set() ignores empty values such as 0 and ''. Pass '0'.
  • $[index] counts across all elements matched by the selector, in document order, not per parent. Use separate selectors to number groups independently.
  • font-size: fit(), height: same and the other built-in values are recalculated when the window resizes and when fonts load, not when only one element changes size. Call css.update_vars() after a script resizes a container.
  • :stuck detects whether the sticky offset is applied. It doesn't tell you which edge the element is stuck to.
  • Debug mode is on by default and throws on mistakes, which helps during development. Turn it off with css.defaults.debug = 0 in production.