Closed mveytsman closed 4 years ago
Thanks for the detailed report! :heart: Awesome job! :100:
Note the docs already say this:
When using
phx-update
, a unique DOM ID must always be set in the container. If using "append" or "prepend", a DOM ID must also be set for each child.
So we are clear to adopt solution 1. Although we should probably warn if we are discarding an actual tag, as users may be surprised to why their elements are not appearing on the page (no need to warn for text nodes). If you can add a test (we should have a reasonable coverage for this that you can copy and adapt), even better!
Thanks! I'll add a test & a warning to my PR.
Environment
Issue
I found what I think is a pretty major DOM memory leak when using
phx-update="prepend"
andphx-update="append"
along with a list comprehension.TL;DR When using
phx-update="append"
/phx-update="prepend"
in a container containing a comprehension, for every element updated we will add an extra TextNode containing only whitespace. This means our DOM will grow unbounded with every element updated.Below I include a sample LiveView demonstrating the issue, and discussion of what's going on. I also have a PR fixing the bug ready to go, but am creating this issue separately as I'm not sure if my approach to fixing the issue is the right one.
By the way, I do not believe this is related to https://github.com/phoenixframework/phoenix_live_view/issues/978
Demonstration
Consider this sample LiveView. We render 20 divs inside a comprehension with
phx-update="append"
on the parent. Each div has a unique id. We can click a button to randomly update one of the divs, and another button to update all of them.To demonstrate the issue we also include the following hook which prints out the number of childnodes of the container on each update:
Here's my console after I click "update a random div" 4 times and then click "update all" 4 times:
I found this can have a significant impact on performance when I have a non-trivial number of elements. For example, on my computer when I set
length
to 1000 and enable profiling, updating a random div will take ~100ms. If I click "update all" 20 times I end up with ~20k childnodes and ~1000ms for a full patch.Discussion
To make things clearer, I set
length=2
for the following html samples.Here's what's going on:
1) The EEX comprehension as written above will generate newlines around the divs:
This means that our initial DOM tree looks like this
This is why in the console log screenshot I included above our initial mount had 41 childNodes, when we only had 20 div children.
2) When doing
phx-update="append"
/phx-update="prepend"
we don't discard nodes during the morphdom patching:https://github.com/phoenixframework/phoenix_live_view/blob/254bd86f708dd1ea7087270b4f1ade59bfa6609c/assets/js/phoenix_live_view.js#L1353-L1355
Then, after we're done patching we re-order them back to their original order:
https://github.com/phoenixframework/phoenix_live_view/blob/254bd86f708dd1ea7087270b4f1ade59bfa6609c/assets/js/phoenix_live_view.js#L1425-L1439
So, after the socket is mounted and we do a re-render, we will end up pushing the divs to the top and have the following DOM tree:
3) If I'm updating just the div with id 0, I'm patching in this tree:
We didn't discard any nodes, so before re-arranging divs, my tree looks like this:
Then I do the post morphdom re-ordering and I get
This is how we grew from 3 TextNodes to 4. If we update N elements we will add N extra empty TextNodes!
Solutions
I'm come up with a few ways to fix this, and I've made a PR for the one I think is cleanest. I'm relatively new to this project, so I'm not sure if I'm missing something obvious solution-wise, and would appreciate feedback on my approach / if there's a better one!
1) Only skip discarding elements in
phx-update="append"/"prepend"
when they have ids. I think this is the cleanest way to do this. 2) Prevent adding new nodes if they are whitespace-only TextNodes 3) Prune whitespace-only text nodes from the container before we do the post-morphdom re-arranging. 4) Prune whitespace-only text nodes from inside comprehensions, similar to how we do it inside components: https://github.com/phoenixframework/phoenix_live_view/blob/254bd86f708dd1ea7087270b4f1ade59bfa6609c/assets/js/phoenix_live_view.js#L304-L313