Skip to content

Instantly share code, notes, and snippets.

@erlandsona
Created December 6, 2022 13:59
Show Gist options
  • Save erlandsona/0e7da27eaa24aba2b1c2fdd1a66d976a to your computer and use it in GitHub Desktop.
Save erlandsona/0e7da27eaa24aba2b1c2fdd1a66d976a to your computer and use it in GitHub Desktop.
Builder vs Attributes performance w/ Benchmarks

Rows and Cells

The styleguide has the documentation on how to use our Row and Cell modules. So if you are just trying to get going using or understanding the thinking behind Row and Cell, open the frontend in your browser, press Cmd + Shift + period ('.'), and then click "styleguide" and then "grid".

In this document I want to talk about some of the implemtnation details.

Performance Problem

We have noticed that rendering the full note list is kind of slow. Going from a blank page to a full 100 note long note list in the frontend takes a perceivably long amount of time. Somewhere around 200-500ms if I remember correctly.

I looked into why this was, and it turns out that the code is taking a long time to calculate how the view should look. This is unusual. Usually performance problems in browsers come down to the browser itself taking a long time to update and style html nodes.

So there is something unusual in our Elm code that is extremely costly. Here is what is going on.

The most computationally expensive things you can do in Elm is:

  1. update a record
  2. call a function

We do that everywhere. Like in every step of every view function. Thats just how our pattern of view components works, where we have like a Button msg record and then lots of Button msg -> Button msg functions.

The Cell msg record is huge with ~30 fields. And every function like Cell.shrink is consuming and constructing a new record. The most expensive kind of operation that can happen is happening constantly.

Solution

I think we need a V2 for Cell and Row. That, almost the same thing (indeed, maybe V1 can be refactored to using V2 internally), except the instead of using records and functions, it uses a new Attr msg type and lists respectively. So this..

    Cell.fromString authorFullName
        |> Cell.pad (Padding.right rightPadding)
        |> Cell.withFontWeight Font.mediumWeight
        |> nameSizing
        |> Cell.when (enableUnreadIndicator && unread) Cell.withBoldFont
        |> Cell.centerY

should become this..

    Cell.fromString authorFullName
        [ Cell.pad <| Padding.right rightPadding
        , Cell.withFontWeight Font.MediumWeight
        , nameSizing
        , Cell.when (enableUnreadIndicator && unread) Cell.withBoldFont
        , Cell.centerY
        ]

and internally instead of this

type alias Cell msg =
    { width : Width
    , renderBehavior : RenderBehavior
    , onClick : Maybe msg  
    , children : List (Html msg)
    }
    
fromString : String -> Cell msg 
fromString str =
    { emptyCell | children = [ H.text str ] }
    
toHtml cell =
    let 
        styles =
            [ case cell.width of ..
            , case cell.renderBehavior of ..
            ]
            
        attrs =
            [ case cell.onClick of ..
            ]

we do this

type alias Cell msg =
    { attrs : List (Attr msg)
    , children : List (Html msg)
    }
    
type Attr msg 
    = OnClick msg
    | RenderBehavior RenderBehavior
    | Width Width
    | When Bool (Attr msg) 
    | None
    
fromString : String -> List (Attr msg) -> Cell msg 
fromString string attrs =
    { attrs = attrs, children = [ H.text string ] }
    
toHtml cell =
    let 
        styles =
            List.map attrToStyles cell.attrs 
                |> List.concat
                |> Css.batch
                
        attrs =
            List.map attrsToHtmlAttrs cell.attrs
                |> List.concat
            

I benchmarked this in this ellie app, and the second approach seems to be a speed up of 10x: https://ellie-app.com/cTzZK8j5rRSa1

So, I think whenever we need to fix these performance problems, the steps could be:

  1. Making a View.Cell.V2 that is a simple translation of the v1 code into this v2 form, where every Cell msg -> Cell msg has an Attr msg alternative.
  2. Refactoring View.Cell to use View.Cell.V2 internally
  3. Either all at once in one mega tedious PR, or piece by piece, replace every instance of View.Cell in our application with the direct View.Cell.V2 version.
  4. Delete View.Cell
  5. Repeat steps 1 through 4 for Row as well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment