Lazy Elmish React

Recently I have been doing a fair amount with Fable-React-Elmish, and I have had really good results. Quick time to production, little or no bugs. Overal I have been very happy with the toolchain. Now admittedly the apps I have been producing are small, boring line of business apps. Basically just utilities or forms that essentailly are nothing more than data entry. However the latest application I have had to produce was abit more involved. Basically there is a simple form that has to query a API and then dispaly the data in a table with various levels of grouping. Armed with my recent successful experiences with the Fable-Elmish-React toolset I was reasonably confident I could have something running in a few days. Which I did, and I was pleased with my self.

UNTIL...

I started testing with more realistically sized datasets. And then I found out it was slow, like, really, really, slow. Over a second to render a single key press. This was somewhat a surprise as everything you read about React says how blazing fast the Virtual DOM and rendering are.

In it's first cut I had a view which looked something like the following.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
let view model dispatch = 
    R.div [ClassName "col-md-12"] [
        if model.Data.Length > 0 
        then
            yield R.form [ClassName "form form-inline col-md-12"; Style [PaddingBottom "10px"]] [
                        R.button [ClassName "btn btn-default col-md-1"; 
                                   Disabled (model.PendingChanged.Length = 0);
                                   OnClick (fun x -> x.preventDefault();  dispatch SaveChanges)
                                 ] [unbox "Save Changes"]

                        R.input [ Id "selectedentity"; 
                                  ClassName "form-control"; 
                                  Style[MarginLeft "20px"]; 
                                  Placeholder "Select Entity"; 
                                  Value model.Entity]
                ]
       
        yield R.div [ClassName "col-md-12 table-responsive"] [
                   if model.Data.Length > 0 
                   then
                      yield bootstrapTable [
                          KeyField "id"
                          Data model.Data
                          Columns model.Columns
                          CellEdit (CellEditFactory(
                            [
                              "mode" ==> "click"
                              "afterSaveCell" ==> ignore
                            ] |> createObj))
                      ]
                   else 
                     yield R.h1 [Style [BackgroundColor "#eeeeee"; TextAlign "center"; Height "100%"]] [R.str "No Data!"]
              ]
    ]

However as innocent as this looks it is going to cause react some problems. Why, well the table in this case is going to contain 52 x 31 (1612) elements. That is quiet a lot of DOM elements, and the problem in this instance is that this will get rendered on every single pass. So how do we go about solving this. Well..

TL;DR RTFM - There is a description of this exact issue on the Elmish.React github page.

By default, every time the main update function is called (upon receiving and processing a message), the entire DOM is constructed anew and passed to React for reconciliation. If there are no changes in the model of some component, its view function will under normal circumstances not return a different result. React will then still perform reconciliation and realize that there is no need to update the component's UI. Consequently, when the DOM is sufficiently large or its construction extremely time-consuming, this unnecessary work may have noticeable repercussions in terms of application performance. Thanks to lazy views however, the update process can be optimized by avoiding DOM reconciliation and construction steps, but only if the model remains unchanged.

So lazy views are the answer. Elmish-React provides several lazy view functions. The varients without the With suffix require types with equality constraint. This means that if any property on the model changes then the DOM will be updated. This is often not the behaviour you desire, since it is unlikely you will interact with your UI without changing your model. To this end you have two choices, create a lazyView component and pass the specific property or properties (as a tuple) from the model you are interested in. Or use lazyViewWith which allows us to specify the predicate that decides when we should update the containing DOM elements. In this example we'll use the latter.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
let viewLazy model dispatch = 
    R.div [ClassName "col-md-12"] [
        if model.Data.Length > 0 
        then
            yield R.form [ClassName "form form-inline col-md-12"; Style [PaddingBottom "10px"]] [
                        R.button [ClassName "btn btn-default col-md-1"; 
                                   Disabled (model.PendingChanged.Length = 0);
                                   OnClick (fun x -> x.preventDefault();  dispatch SaveChanges)
                                 ] [unbox "Save Changes"]

                        R.input [ Id "selectedentity"; 
                                  ClassName "form-control"; 
                                  Style[MarginLeft "20px"]; 
                                  Placeholder "Select Entity"; 
                                  Value model.Entity]
                ]
       
        yield lazyViewWith
                 (fun oldM newM -> oldM.Entity = newM.Entity && oldM.Data = newM.Data) 
                 (fun model -> 
                     R.div [ClassName "col-md-12 table-responsive"] [
                           if model.Data.Length > 0 
                           then
                              yield bootstrapTable [
                                  KeyField "id"
                                  Data model.Data
                                  Columns model.Columns
                                  CellEdit (CellEditFactory(
                                    [
                                      "mode" ==> "click"
                                      "afterSaveCell" ==> ignore
                                    ] |> createObj))
                              ]
                           else 
                             yield R.h1 [Style [BackgroundColor "#eeeeee"; TextAlign "center"; Height "100%"]] [R.str "No Data!"]
                      ]
                 ) model
    ]

Simples. Basically we have just wrapped the div that contains the table with the lazyViewWith function, and specified the predicate that helps it decide when to update.

With this in place, the responsiveness of the UI returned and I could breath again. I have to admit it took me way too long to figure this out considering it is actually written on the Elmish React project home page. However on the brightside I learnt a lot about React performance analysis, mainly from this post I suggest, you read this as I found it quiet useful as well as the links at the bottom.

namespace Microsoft.FSharp.Core
type obj = System.Object

Full name: Microsoft.FSharp.Core.obj
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
namespace Microsoft.FSharp.Data
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
module Unchecked

from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T

Full name: Microsoft.FSharp.Core.Operators.Unchecked.defaultof
val unbox : value:obj -> 'T

Full name: Microsoft.FSharp.Core.Operators.unbox
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
tweet-share