How to do CSS in elm?

Hi everyone!

Just finished reading the last post about native code in 0.19, and I was wondering something. Right now @tibastral and me were working on CSS in elm with one package (elm-bodybuilder/elegant), and we wrote some Native code (approximately 20 lines I think) to improve rendering speed.

We tried a lot of approaches, staying purely in elm, but performances weren’t good enough. We ended up with the idea of VirtualCss handling every CSS stuff related, and it was amazing (though not called like this in the package, it is the exact idea). In the future, with the release of 0.19, how would we be able to deal with this? CSS in JS is hot stuff right now, and removing the painful CSS from elm is really so good.

I also tried the elm-css way, but it’s not the way I want to follow. It’s not really about merging CSS directly into elm, but rather to write typed-check CSS and compile it to CSS stylesheet (and it’s awesome if you want to manage stylesheets — personally I don’t).

So it’s an open question, how would we be able to do CSS in elm in the future without native code?
If anyone has an idea it would be cool. @evancz if you have some thoughts about it, I think it would be awesome.
Thanks! :slight_smile:

PS : Should it be posted into elm-dev instead of here? Don’t really understand the differences… ^^’

1 Like

Do you have some sample code showing this sub-optimal performance?

From my own experiments, rendering CSS to string and mounting the resulting text in a style element in the head (via a port) is fast enough.

Hi.

We were doing something like this:

myRedClass : List (String, String)
myRedClass =
  [ ("background-color", "red") ]

view : Model -> Html Msg
view model =
  Html.div 
    [ Html.div [ Html.Attributes.style myRedClass ] [ Html.text "Hello world!" ]
    , Html.div [ Html.Attributes.style myRedClass ] [ Html.text "Hello world too!" ]
    ]

I don’t know how to extract the CSS and throw to JS via a port…
Actually, we built a package to abstract CSS, which means we had something like this:

myStyles : Style
myStyles =
  Style.box [ Box.border [ Border.thickness 1 ] ]

view : Model -> Html Msg
view model =
  Html.div [ Html.Attributes.class <| computeStyles myStyles ] []

We wanted to decrease computation time took by computeStyles. So we ended up with VirtualCss: you just give it the style object, and do some caching on styles, avoiding computing it each times, and pushing the styles as atomic classes in the User Agent directly (with CSSOM, insertRule).

The idea is really to stop dealing with CSS, but rather writing styles directly on HTML nodes, which gets compiled to faster CSS.

The easiest way would be to have module (page modules) function like css : Model -> String (equivalent to view) and compose the relevant styles in the top level update. You then send the String to JS where you have it installed in the head style.

In the view code you only use classes to switch styles.

As I said, I found this approach fast enough when I tried it. This is why I was asking for an actual example where this sub-optimal performance can be seen.

I apologize for the digression, but did I miss something? If so, where was the announcement?

Sorry, was using Tomorrow in the sense of “in a near future”. I modified it to avoid problem. :slight_smile:

This used to be true, but it’s the opposite today. Check out the latest docs!

It’s all done at runtime in plain Elm now - there are no separate stylesheets anymore (or Native for that matter).

2 Likes

Thanks for the answer, that’s good to know! I looked at your code and immediately got few questions. I think we ended up with the same algorithm, so I’m curious.

What’s happening when you more than 400 nodes on the page? Isn’t the rendering time too long? You are parsing the same styles over and over again at each rendering right?

We tried a solution like this:

  • Write your HTML as styled nodes, and store styles in the node.
  • Render the view function.
  • Parse the resulting tree and extract the styles.
  • Compute the styles and put them in a style node.
  • Add the style node at the top of the DOM, add the view after this, and send everything to the runtime.

That was a perfect algorithm, perfectly working, and we even did some caching on styles to avoid recomputing styles twice (with a Dict). But we got a time rendering issue with large pages. When just writing some pages, it wasn’t a problem, because we had approximately
500ms for a rendering, and it was good. But we wanted to do some animations. We wanted 60 fps, so we needed at most 16ms in theory for each rendering. So we changed our strategy:

  • Write your HTML nodes as usual.
  • Write the style in VirtualCss.style, which performs side effect in JavaScript.
  • The style get computing if not computing previously (maintain a cache of computed styles), and pushed into the User Agent using CSSOM.insertRule().
  • The corresponding classes names are returned from the function, and they are put into the HTML via Html.Attributes.class.
  • The corresponding HTML is returned from the view and passed to the runtime.

This way, you avoid the two tree exploration. The only one needed is the runtime exploration. For huge pages, we ended up with 30ms of rendering. That changed everything to be honest.

That’s why I think it’s really important to talk about CSS and CSS in elm. Performances matter, and it’s not usable for me if a page took more than 200ms to render…

I would be glad to help in this or have a constructive discussion about how we should achieve this in the language.

I’m more than happy to see that @rtfeldman ended up with similar solutions, and to see that the idea of handling styles in elm is becoming more popular! :grin:

An example showing the problem would be a good step in my perspective.

Create an realistic example that shows the issue and with that example, multiple approaches can be explored.

Make the example heavier than it needs to be. Show how pre-rendered CSS is handling that example significantly better than runtime rendered stuff.

I am super curious to see a SSCCE showing this 500ms penalty and I am confident that with an example, members of the community could point out alternative strategies that would improve things considerably.

1 Like

You’re right, I was on my smartphone, so a little bit difficult to make proper links sometimes.

Here’s the modern, correct example: Blog with CSSOM
Here’s the older version: Blog without Native Code

The second version is not way slower, but the slow feeling is there. On more charged web pages, the difference is way more pronounced.

As I can see, the older version uses position: relative for the animation, and the example with CSSOM uses transform: translate which should generally be much more faster. I think this is the real reason for the performance difference between two examples.

1 Like

No it’s not the reason. The transform doesn’t change anything in terms of performances (look at the detailed breakdown inside the chrome dev tools please)

Seems like you’re right and it’s not playing that huge role in this particular case, but I think it would be a good a idea to modify the old example to use transform instead if it’s possible, just to be sure that it’s really not the reason.

Also, I’ve noticed that elegant uses class-per-declaration approach to render styles. It would be interesting to recreate that example with elm-css (which uses hashes instead) and see how it compares.

But insertRule is definitely the winner in any case, and I’m too very interested to see VirtualCss in Elm.

Hi,

Without optimizations, I also have some performance issues with several hundred nodes and elm-css using Html.Styled.

Here is an example quite close in complexity to a real list view in my app:

https://ellie-app.com/6rrjk87H4a1/1 (desktop only, move the mouse over the list)

This is very sluggish even on a powerful computer.
I put 500 items to exagerate the issue, and each one has 4 divs, so around 2000 nodes in this example.
The list styles are actually a little more complex in the app, therefore the latency can be felt even with a few tenths of items.

Here is the version using a CSS stylesheet without any noticeable latency:

https://ellie-app.com/74MhgFpvfa1/1

The best optimization I can think of is to use Html.Styled.Lazy.lazy on viewItem, but this would be quite ugly in this case without lazy4 and more (which are not available in 0.18). It would be handy to have a lazy function that can be applied only on the List Style or on an Html Attribute msg.

Edit: I get around 200ms for requestAnimationFrame using Html.Styled vs around 5ms without.

1 Like

@dmy The good news are that elm-css is not yet optimized, so there is still
some work that can be done to improve performance (I’m not sure whether it would be enough for your case though). If I understand correctly, styles are compiled twice – the first time to calculate hashes, and the second time to build a style node (@rtfeldman please correct me if I’m wrong).

I’ve simplified your example a bit to try it – and it’s still slow https://ellie-app.com/nzW2xBHRFa1/0.

But the interesting thing is that if you look into the dev tools, you can see that the browser now spends most of the time on rendering, while in the original example it spends it on scripting. I’m not sure what’s happening, but I think the reason might be a large number of style nodes in the DOM.

Thank you @edkv

Indeed, using lazy with reusable views is a pain without lazy4 & co., this has a lot of impact on models and view configs and AFAICT it is sometimes barely possible at the reusable view function top level without storing ugly lists of parameters in the model.

It is actually very fast on Firefox Quantum, maybe thanks to their new style renderer, but still slow on Chrome. So the situation will most likely also improve from the browsers side.

I think I will use a few Foreign styles (only those rendered a lot) until the performance has improved enough for my use case.

Anyway, hopefully this example has illustrated the question asked by @ghivert and contributed to the discussion.

There are three major performance optimizations I want to do for elm-css:

  1. Move the compatibility checks to a phantom type, so they no longer have runtime overhead.
  2. Hash things more efficiently. Plenty of room for improvement there.
  3. Use insertRule. This is on Evan’s radar, and he’s talked through some API ideas with me. style-elements would benefit from insertRule too.

In the vein of Make it Work, Make it Right, Make it Fast, the current elm-css API has gotten positive responses on its working and working right, so it makes sense to start shifting focus to performance!

6 Likes

Performance really interest me :slight_smile: . Is there any way to know a bit more about the API ideas you are talking about?

So, some additional thoughts on performance in addition/to elaborate on what Richard mentioned.

Style elements now uses an atomic css approach to generating style classes, which means one property per class, and the name of the class is esssentially the value of the property.

So, for background-color: rgb(0,0,0,1), we render

.bg-0-0-0-1 { background-color: rgb(0,0,0,1); }

This also allows us to reduce the sheer number of css rules we have to render by running a deduplication step. This is a big gain because we can skip the work of translating everything into strings, and we can also help the browser skip work by not giving it more things to parse and figure out.

This also allows us to have a more targeted hash-like thing in a super fast way.

I also found that it’s really beneficial to separate styles into a static sheet and a dynamic sheet. Move absolutely everything you can into a static sheet instead of rendering on every frame.

3 Likes

It was very preliminary - basically just looking at articles on how to get the best CSS perf at runtime, concluding that insertRule was the fastest, thinking about ways that could work with elm-lang/html, etc.