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.
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:
- update a record
- 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.
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:
- Making a
View.Cell.V2
that is a simple translation of the v1 code into this v2 form, where everyCell msg -> Cell msg
has anAttr msg
alternative. - Refactoring
View.Cell
to useView.Cell.V2
internally - Either all at once in one mega tedious PR, or piece by piece, replace every instance of
View.Cell
in our application with the directView.Cell.V2
version. - Delete
View.Cell
- Repeat steps 1 through 4 for
Row
as well.