virtual-dom icon indicating copy to clipboard operation
virtual-dom copied to clipboard

Performance optimization for CSS

Open rtfeldman opened this issue 8 years ago • 7 comments

Background

One way to use elm-css is to generate a CSS string and then call node "style" [] [ text cssStylesGoHere ] to render a <style> with the relevant styles inside. Besides elm-css, style-elements uses this <style> technique as well.

Using this technique, you can use hover (unlike if you use the style attribute) and can also dynamically update styles on the fly, e.g. when animating. (You cannot update them on the fly if you use .css files.)

This is the only technique allowing you to both use things like hover and describe arbitrary animations, making it objectively the most powerful way to manage styles in the browser. (That said, style attributes are simpler to use, and .css files can be downloaded in parallel and then cached.)

Problem

If the CSS string inside text cssStylesGoHere is very large, and you want to use it dynamically for animations, the browser has to re-parse the entire string and re-build the CSSOM on every frame. This means every new style you introduce, even if it is unrelated to animation, still slows down every animation you have. Worse, these slowdowns occur when they are least tolerable: during animations.

In present-day Elm, we can improve this by breaking the <style> into several smaller ones (which is something glamor does, but apparently for different reasons), but IE9 doesn't support more than 31 <style> tags in a single document, so there is a ceiling on how helpful this can be if we want to support older browsers. We can also use multiple text nodes instead of regenerating one big one.

Modern JS libraries which use this overall technique have tried multiple <style> elements, each with multiple text nodes inside them. They have discovered that there is a much faster approach, especially for animations.

Solution

Since IE9, <style> elements have supported the insertRule and deleteRule methods, which introduce new rules to stylesheets much faster than changing the text nodes inside them. Once created, the resulting CSSRule instances can also be directly mutated on the fly - again, much faster than mutating text nodes.

Glamor saw a "10-1000x speedup" using insertRule over the "multiple text nodes inside one <style>" approach.

The JSS folks found a way to make this especially fast for animations, by creating styles with insertRule and then mutating them when animating, which is evidently the fastest possible way to do arbitrary animations in the browser. (As the end of the article says, "That’s it, the cat is out of the bag. No need to wait for React Fiber to get high perf animations.") They have a perf demo to demonstrate the technique's benefits compared to older approaches.

Other Notes

  • Styles added by insertRule are not editable using browser devtools. The Chromium team indicated that this is a known issue that they will not fix, saying "We don't have any plans to support editing of CSSOM-based rules in DevTools." Glamor addresses this by using the "multiple text nodes inside one <style>" approach in development builds, and insertRule in production (which they call "speedy mode").
  • The source code for Glamor has comments documenting some browser inconsistencies to watch out for in practice.
  • Pete Hunt's JSXStyle has a much simpler implementation, which does not attempt to address these cross-browser inconsistencies. (Perhaps they aren't necessary anymore?)
  • Glamor was going to garbage-collect unused rules but decided against it.
  • Here's an article on using insertRule that I found helpful.
  • This is not a direct apples-to-apples comparison of insertRule vs not (since it's comparing two libraries, one which uses it and one which doesn't), and who the hell knows what their methodologies were, but here's a graph.

rtfeldman avatar Aug 14 '17 04:08 rtfeldman

Thanks for the issue! Make sure it satisfies this checklist. My human colleagues will appreciate it!

Here is what to expect next, and if anyone wants to comment, keep these things in mind.

process-bot avatar Aug 14 '17 04:08 process-bot

  1. In my own work with node "style" I have discovered that putting the styles in a style node in the head solved a lot of the performance problems I was having without a need to resort to insertRule. Chrome did warn me about a drop in performance when putting style nodes inside the body but I could not reproduce the warning consistently (extract it from my own code).

  2. The need to mutate the styles from anywhere in the code (JS) might be more performance sensitive than smartly mutating few head style nodes and resting on the ability of the Elm Architecture to generate the needed CSS textual information. A real world demo that shows performance issues would be very useful in exploring solutions that take advantage of the Elm Architecture.

  3. Beginning January 12, 2016, support for all IE versions below 11 has ended. The marketshare of bellow IE 11 browsers is very small and decreasing.

pdamoc avatar Aug 14 '17 05:08 pdamoc

@rtfeldman, great write up, thank you! :D

Based on the insertRule docs it looks like quite a lot of things can go wrong if it's not used exactly correctly. It seems like it needs to be paired with something like elm-css to ensure that @ rules get put in the right place and such.

To @pdamoc's point about putting things in the <head>, perhaps the way to go is to have the extra special program function that lets you change the <title> and one big <style>. I wonder if we can get a dramatically simplified version of elm-css to be the basis for any library that wants to mess with that. Perhaps the abc of a rule like abc { ... } could be used as keys to manage where things get inserted and removed.

evancz avatar Aug 14 '17 19:08 evancz

I agree on all counts!

I think the basic rules necessary to avoid crashes can be implemented using a single union type with a handful of constructors. Both elm-css and style-elements could compile to that representation.

On Mon, Aug 14, 2017, 3:10 PM Evan Czaplicki [email protected] wrote:

@rtfeldman https://github.com/rtfeldman, great write up, thank you! :D

Based on the insertRule docs https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule it looks like quite a lot of things can go wrong if it's not used exactly correctly. It seems like it needs to be paired with something like elm-css to ensure that @ rules get put in the right place and such.

To @pdamoc https://github.com/pdamoc's point about putting things in the

, perhaps the way to go is to have the extra special program function that lets you change the and one big <style>. I wonder if we can get a dramatically simplified version of elm-css to be the basis for any library that wants to mess with that. Perhaps the abc of a rule like abc { ... } could be used as keys to manage where things get inserted and removed. <p>— You are receiving this because you were mentioned. <p>Reply to this email directly, view it on GitHub <a href="https://github.com/elm-lang/virtual-dom/issues/112#issuecomment-322281160" rel="nofollow" target="_blank">https://github.com/elm-lang/virtual-dom/issues/112#issuecomment-322281160, or mute the thread <a href="https://github.com/notifications/unsubscribe-auth/ABCxwHfwraa6DSFegnnwAzPKYn2P_jLTks5sYJuXgaJpZM4O179T" rel="nofollow" target="_blank">https://github.com/notifications/unsubscribe-auth/ABCxwHfwraa6DSFegnnwAzPKYn2P_jLTks5sYJuXgaJpZM4O179T . </style>

rtfeldman avatar Aug 14 '17 19:08 rtfeldman

Actually, one problem with doing it in a separate function from view is that style-elements (which works by having the end user build up an intermediate representation that is later compiled to a Html value including style info) would need to call the user-defined function twice.

Currently it only needs to be called once because emitting a Html value inside view is sufficient to specify both the necessary DOM and CSSOM.

rtfeldman avatar Aug 14 '17 19:08 rtfeldman

Just chiming in here, I've been experimenting with something like this in: https://github.com/gdotdesign/elm-html-styles and I'm using it in a project, seems to work well, but I would suggest to use JSS instead of witing it from scratch because they have solved a lot of issues that came up and it's API is pretty good.

Having something like this embedded in the language would be pretty good.

What I had in mind is to have this type signature: node : String -> List (Attribute msg) -> List Style -> List (Html msg) -> Html msg where the Style contains the styles for that node.

gdotdesign avatar Aug 15 '17 09:08 gdotdesign

I would love to have that also for https://github.com/elm-bodybuilder/elegant ! Because I have a CSSOM here, and it would be certainly more performant !

tibastral avatar Aug 17 '17 14:08 tibastral