-
Notifications
You must be signed in to change notification settings - Fork 197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add keyedLazy to scope styles for lists of lazy nodes #584
Conversation
@tesk9 if you have time, I'd love to have your review on this too! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One style comment, but overall looks good to me! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I might be misunderstanding something? I'm currently understanding that the slow thing that this PR is trying to resolve is the browser's application of the styles. Is that right?
When I record perf in chrome I'm getting relatively good re-render times. The slowdowns seem to be on the scripting side.
I went to a known-not-fast NRI page and opened some accordions and recorded:
I'm also wondering if there's missing memoization of shared styles in general, outside of the case where you have a container whose children happen to share styles.
src/Html/Styled/Lazy.elm
Outdated
|
||
Some notes about using this function: | ||
|
||
- The key must be a valid HTML id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little surprised by an HTML function with keyed
in the name using an HTML id. I supposed because the Html.Keyed node
function doesn't use HTML ids?
I guess I'm wondering if there's another naming pattern that could avoid confusion with Keyed Nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had originally planned on calling this scoped
and then went with keyed
after talking with Richard. Do you scoped
any better?
The functions would be scopedLazy
... scopedLazy7
then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops missed this comment before, sorry!
I prefer scoped
-- I think it's a bit more clear
src/VirtualDom/Styled.elm
Outdated
Json.Encode.string scope | ||
|
||
|
||
uncurry : (a -> b -> c) -> ( a, b ) -> c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uncurry
and mapOnto
don't feel to me like they belong in this file 🤔 Possibly worth a new file? Although maybe Richard is cool with these being in here..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uncurry
is gone now and mapOnto
is used only in VirtualDom/Styled.elm. Any better suggestions for where it should go? Maybe a new List.Extra file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might inline its uses, rather than having a helper? But I don't feel strongly either way
Yes, exactly. Here's a snapshot of my perf log from chrome for the writing portfolios page when using lazy on some 900 portfolios: Chrome is spending almost 3s "Recalculating Styles"!!! I can't really peek deeper in the internals of what chrome is doing while recalculating styles but I assumed that it was that chrome has to weigh between 900 conflicting styles for each class name
Yeah agreed. I talked about this with Richard when we paired, but I think the ideal situation here would be to patch the Elm core VirtualDom lazy nodes to allow caching some additional JSON alongside the node. Then we could serialize our Dict of styles and get them back out instead of being forced to render the |
Hmm! I guess I'm seeing different behavior than you, then? On staging, with >200 writing pieces in the portfolio, I'm still seeing most time spent in scripting, and very little in rendering: Are you seeing perf degradations when adding additional (all our apps are already wrapped with lazy helpers in Nri.Spa and Nri.Program) lazy wrappers? I wonder if Keyed nodes would prevent the re-renders you mentioned...? |
Yes! Sorry if that wasn't clear. On my branch we are adding a lazy wrapper around each portfolio item card. I should also note that the branch I'm looking at was before pagination (so literally 900 items on the page). Still applies though as we'd like to bump up the number of cards shown per page. We were stuck in a weird place because without lazy on each card, hovering over a tooltip would force a re-render of all of them which cost a lot of time in scripting. Adding lazy to each item cost us instead a lot of time calculating css styles in the browser. |
I was able to reproduce the slowdown in rendering in an Ellie! When I had the top of the application use I guess I'm wondering whether Lazy is the right tool here, when working with a long list of items. I think that, if you're using lazy, you've got ~900 items in the cache, and they're all going to get invalidated when the open tooltip value changes. Mind trying keyed nodes on your branch instead? I think it'll work. (I mean, I don't think perf will be perfect, but I think it'll be better) Plain view |
So cool you could repo this on Ellie!
We're being a little more careful with the lazy args then your 3rd example such that only 1 list item is ever invalidated if the the open tooltip changes. Here's an updated Ellie example that's closer to what we're doing ^ That Ellie example spends about 500ms for me in "Recalculate Style" which is roughly equivalent to what I'm seeing on my branch given that our list items are more much more complex.
I already am using keyed nodes on my branch - it helps some with the dom diffing time, but the full render of each portfolio item node is still not fast when done 900 times. |
Okay! So then if I'm understanding correctly, ideally we'd want a node to have its children keyed in the usual sense and have those children lazily rendered? I'm thinking about having, say, 1000 reorder-able elements on the page. I don't think I could use the Lazy.keyedLazy helper, since the browser still wouldn't actually have keyed the nodes for real. Is that right? I guess I'm a little concerned about user confusion around Lazy.lazy, Lazy.keyedLazy, and Keyed. 🤔 |
Yes exactly!
AFAIK you don't really need to use Totally agreed w.r.t. the potential user confusion about the naming. Did you see my comment above that maybe we could go with |
Or you want to get some perf benefits! Ju's Performant Elm, Part II article I guess my key question at this point is: can we combine Lazy and Keyed directly? Because there will be times when devs have long lists of elements that need to render performantly and are rearrangeable/deletable/etc. It seems to me like the goal is something like this:
Where The other thing I'm thinking about is the last time I last ran into lots and lots of elements and laggy performance that seemed tied to elm-css class name regeneration to some degree, I wasn't working with a list -- I was working with a tree. (Well, lots of trees, and that was the problem 😆)
I think having an id-based descendant selector doesn't work as well for views where your perf issues are coming from depth, rather than breadth, you know? I guess I'm thinking that if we can get the browser to keep track of nodes, that's going to be better for us than if we try to manage when to apply css styles ourselves. For the problem I was having with tree performance, we actually did end up scoping the styles ourselves. This is the reason that noredink-ui's Accordion has a separate I guess I would like for a baked-in elm-css strategy for perf improvements when there are lots of nodes to apply to other situations as well, like when nodes are rearrangeable or when there's a deep tree. All that said: whatever you all decide to do next is cool with me! I learn conservative on lots of changes, but the best way to learn and to actually make improvements happen is to try things out. Thanks for walking me through everything, Micah! I very much appreciate your taking the time to make sure I fully understood your problem and your goals 🙏 |
I've had this same line of thought! We likely always want both, and the both require a unique identifier - might as well have them be the same!
Unfortunately we cannot write this implementation. The VirtualDom lazy is an information black hole - all we get out is the completely opaque We could do something like this:
This is, in fact, what I had in an earlier phase of this API design. One downside is that it forces us to consume another argument to the core Another potential thought along this line that I think I like better would be extending the
We can augment lazyNode : String -> List (Attribute msg) -> (a -> Html msg) -> List ( String, a ) -> Html msg
lazyNode =
Debug.todo ""
lazyNode2 : String -> List (Attribute msg) -> (a -> b -> Html msg) -> List ( String, ( a, b ) ) -> Html msg
lazyNode2 =
Debug.todo "" Curious for your thoughts on that API? It also has the nice side effect of avoiding the need to find a better name 😆
Ohhhhh fascinating. I had not even given a thought to tree based views. I think it could still help this scenario in a couple ways:
In the much longer term, I'm hopeful there's a better light at the end of tunnel. If we could cache data on the lazy node itself and retrieve it then we would not be forced to generate a style node at each lazy call site and could avoid this problem all together. Also, the recent episode of the elm-radio podcast mentioned an idea that we could use an elm-review like tool to traverse all the styles in the application and extract the ones that can be determined statically. This would significantly reduce our burden here and make asset sizes much smaller! |
I like the |
Ok! I wrote up an separate experiment PR that explore what the @tesk9 If you could take a look that would be great! @rtfeldman Looping you back in too as this is a significant change to the API from when you might have last looked at it. If we're all in agreement that the |
I am liking it better! Just curious, does it still need the |
Yes, unfortunately. The I took the following profiles on Chrome and Firefox that might be helpful. These slices are the time it take to open a tooltip on the writing portfolio page with ~900 items. Chrome
FF
The "keyed - not lazy" all show a high amount of time spent generating the view in javascript, but relatively little time for the browser to calculate styles as we're able to share the many of the css classes. The "keyed + lazy" profiles drastically reduce the amount of time spent in javascript generating the view, but we are now generate duplicate styles on each and every node, and the browser spends forever applying all these conflicting styles to nodes. Finally the "keyedLazyNode" profiles use the new functions. We still generate css on every lazy node, but it is scoped to that lazy's nodes ID which significantly cuts back on the time the browser spends calculating those conflicting styles. Why is FF so much better than Chrome? 🤷♂️ But the improvement on Chrome is still drastic. |
As the new branch is performing as expected and we like the API changes, I'm merging its changes into this branch. |
@rtfeldman I think this is ready to merge if you like the changes! |
Cool! @tesk9 do you have time to take a look and give your thoughts? (No worries if not, but I want to double check!) |
Thanks! I think I've said enough 😅 |
Published as 18.0.0! 🎉 Thank you so much for all your excellent work on this @micahhahn, and thanks for the review @tesk9! |
This PR aims to make
lazy
more performant when used with a list of many items.Currently
lazy
is forced to render the styles immediately underneath the root node due to the way the VirtualDom works. This is usually fine, but in cases were we are rendering hundreds of elements with lazy we end up lots of duplicated identical styles. This causes performance issues in the browser as it has to do additional work to figure out which of the identical styles it needs to apply to any given node. (At NRI, we saw a list of ~900 elements with maybe 20 styles each take Chrome around 3 seconds to recalculate styles).This PR mitigates the problem by scoping the generated css at each node with an id given to the root node. This takes the decision of which style to apply away from the browser and allows them to use id selector quick rejection.
We add new functions
keyedLazy
...keyedLazy7
that look very similar to theirlazy
...lazy7
counterparts save for that they admit specifying a key along with the root html element. See the type signature forkeyedLazy
below:keyedLazy : (a -> ( String, Node msg )) -> a -> Node msg
In the library, we'll assign this key as an id on the root node and generate all styles with the id selector as a prefix.
Here's a video of the effect on FF on using normal
lazy
versus usingkeyedLazy
on page from NRI that has ~900 elements using lazy with around 20 styles each.Firefox.Before.Changes.mov
Firefox.after.changes.mov