Any disadvantage in always using Html.lazy?

The actual transformation if pursuing this is that it would take your old view functions view: Model -> Html Msg and convert them to say oldView: Model -> Html Msg so that it could introduce the new view function:

view : Model -> Html Msg
view model =
    Html.lazy oldView model

This isn’t actually interesting at the top level since I think the Elm runtime already won’t re-render the view if the model hasn’t changed, but consider applying it anywhere a separate view function had been created. Or more aggressively, consider auto-inferring dependencies in the view code and introducing lazy nodes automatically for Html generating expressions.

The question of whether this pays off comes down to whether it saves enough on render and diff logic (including memory allocation) to make up for the overhead of introducing all of those extra lazy nodes in the DOM tree. That depends on how stable the nodes are. If the parameters for a node change frequently, then that node and all of its parents are a lose to make lazy. If the parameters are relatively stable, it’s a win.

While I doubt — at least in the absence of profiling information or a lot of program analysis — that the compiler could detect which nodes have relatively stable parameters, this does point to one optimization that the compiler could do: if a subtree in a view expression is constant, then it should be broken out to a module level expression generating a lazy node. The lazy part is to help with initialization time. If something is really simple, it could obviously just be built directly at initialization without the need for lazy.

Mark

1 Like

This is what I was trying to suggest in my OP. Have the compiler auto insert Html.lazy(N) everywhere that it is possible to do so.

Side note: Common mistakes that I see people make when using Html.lazy:

• Partially binding the parameters to the view function passed to Html.lazy generally as a way to get around limits on the number of parameters. For example:

Html.lazy4 (viewFn arg0 arg1) arg2 arg3 arg4 arg5

The partial function application creates a new function on each invocation of the code and hence this function will not match the function from the previous render and the lazy will just introduce virtual DOM overhead.

• Constructing records to deal with the above case:

Html.lazy recordBasedViewFn
    { arg0 = arg0
    , arg1 = arg1
    , arg2 = arg2
    , arg3 = arg3
    , arg4 = arg4
    , arg5 = arg5
    }

Now we are generating a new record on every render and hence will fail the identity test used by lazy virtual DOM nodes.

• Related to the preceding: If you use a Model/ViewData approach in which the model is converted to view data which is then used to render — an approach also known, I believe, as “selectors” — then you have the same issue assuming that the view data is built on each render since the view datas from one render to the next will fail an identity match.

Mark

2 Likes

Are you sure it is failing the identity match? I can not reproduce the behaviour of constructing a new record.

---- Elm 0.19.0 ----------------------------------------------------------------
Read <https://elm-lang.org/0.19.0/repl> to learn more: exit, help, imports, etc.
--------------------------------------------------------------------------------
> a = {x = 3}
{ x = 3 } : { x : number }
> b = {x = 3}
{ x = 3 } : { x : number }
> x = a == b
True : Bool

Lazy relies on reference equality on the JS side (===), so even though Elm shows them as structurally equal they will be considered unequal on the JS side.

For some types, like strings and ints, they are compared by value on the JS side and work nicely with the lazy functions.

Here’s the part in virtual-dom where it has the logic

1 Like

Makes sense, as the reference check is just comparing two numbers, so the lazy check never grows in cost as the data structure gets more complex.

Partly why I ask if it would make sense to just do it all the time anyway - the CPU branch prediction may work out well on this kind of check, that seems likely to be biased to go one way or the other most of the time.

You also have to wonder if a full structural equality check would be so bad here either? The assumption is that building the real DOM is much more expensive than the work the code does to build the virtual one, and that kind of implies that processing the data structure is also much cheaper than building the real DOM. I guess there would be cases where it works out really bad.

It definitely seems worth exploring! Maybe lazy should be a feature you have to opt out of rather than opt into.

I got burned by this one recently. I tried to use a lambda as the first parameter in Html.Lazy.lazy to circumvent some problems I created that broke my lazy rendering. But a lambda means a new function is created every render. I didnt know that Html.Lazy checks even for reference equality of the function thats passed into it; but it does.

You don’t need lazy in order to avoid updating the real DOM, that’s what the VDOM diffing is for. With lazy you just skip recomputing your function and doing that VDOM diff.

Structural equality checking would be much more intuitive and beginner friendly compared to the current reference equality, and because of that I suspect there’s a good reason why Evan didn’t use it? Even though it’s usually very fast for immutable data structures, I’m guessing it was still too slow to run on every update?

When you put it like that, I can see there may be more work involved in a full structural comparison versus the code update. For example, if the code just added 1 item to a list - that will be less work than the full structural comparison of 2 lists. So yeah, I guess a quick reference check might well be the best option.

Correct me if I am wrong here, but what I take away from this is that the pattern outlined in the following post is pretty useless as this will result in the creation of a new ViewModel with every render?

Html.lazy works by memoization, meaning that you’re using less CPU power at the cost of more memory usage. It’s also worth nothing that while reference equality is very cheap, it’s still something that is being done in addition to running the view function in the case where the arguments fails the equality test.

For view functions which are likely passed different arguments each time they’re called, you’re just wasting memory and adding overhead.

For view functions which are likely to be given the same argument every time they’re called, but are cheap to run, you’re using more memory for little benefit.

Html.lazy is an optimization and like all optimization, it involves trade offs.

Lazy should only be used when it’s known to have a good effect, and that is pretty hard for a compiler to know.

7 Likes

Does it actually memoize all calls to view or just the most recent one?

Suppose I had: type alias Model = Int, and view was called with the models 1 then 2 then 3 and then back to 1 again. On that last call, would the VDom for the first view call with the argument 1 be recovered from memory?

As far as I understood it this is not the case; Html.lazy just remembers the most recent view call and model, and compares with the current model to see if the VDom needs to be rebuilt. So in the example above, the most recent model was 3 and the current is 1, so the VDom would be re-built. Building it is only going to be skipped if it is called twice in a row with the same argument, for example 1 and then immediately after 1 again.

Never actually read the code to confirm this though…

It defeats Html.lazy, you are correct. I would not say the pattern is completely useless as a result of that though as many applications get perfectly good performance without Html.lazy.

No, you’re right. It only stores the latest call. But this still adds up if you’re, say, applying lazy to every element in a List.

In the case of a List I would say there is a good chance that between views what is likely to happen is that a smaller number of items are added/removed to the list, and most of them stay the same. Since its the same item, it occupies the same memory. It would certainly be interesting and informative to make an example UI that does exactly this - and of course a variation that changes all the items in the list too (and compare the performance in both time and memory of that against just doing lazy against the whole list).

Either way, I think you are right, there are always going to be some pathological cases that make inserting Html.lazy everywhere automatically not such a good idea.

Evan suggests adding lazy to each item in long lists:
https://guide.elm-lang.org/optimization/lazy.html

It can also be useful to use lazy in long lists of items. In the TodoMVC app, it is all about adding entries to your todo list. You could conceivably have hundreds of entries, but they change very infrequently. This is a great candidate for laziness! By switching viewEntry entry to lazy viewEntry entry we can skip a bunch of allocation that is very rarely useful. So the second tip is try to use lazy nodes on repeated structures where each individual item changes infrequently.

I think when you do that it’s also important to use Html.Keyed.node for the list?

1 Like

Thanks for the link to the guide, lots of stuff added since I last looked at it. Html.keyed is described on the following page:

https://guide.elm-lang.org/optimization/keyed.html

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.