tmbb
I’m writing a pretty basic application where users will be able to create “projects” for data entry purposes. A user can create a “project”, which contains a number of “tables”, where each table has a number of “columns” and columns can be of type choice, in which case each table will have a number of “choices”.
To simplify implementation and because I think that is a genuinely good UI, I want to be able to edit everything in the same page. I have the fields for the project, then I use (a customized version of) to create an editable list of tables, then another <.inputs_for/> to create a list of columns for each table, and then a list of choices for each column.
I expect each project to have 1-2 tables, and each table to have up to 60 columns, with some columns having up to 10-20 choices. For these numbers, the number of elements in the webpage is about 10.000. This creates a 4 levels deep stack of neste inputs, which seems like LiveView can’t optimize in any way. The result is that when I edit any of the hundreds of input components in the form, the changeset is recomputed and a rather large message is sent to Morphdom on the client. The elixir code doesn’t struggle at all with any of this, but Morphdom takes up to 1500ms to patch the updates (I’ve gotten these numbers from logs enabled by liveSocket.enableProfiling();).
Is this the kind of performance one is expected to get? Is there any way I can try to restrict which parts of the HTML dom morphdom actually tries to patch? I could break the tables and columns into different pages (with different liveviews, even), but that is not how I want the page to look like… One of the problem of breaking things up is that you lose the ability to render errors that depend on parts of the form interacting with each other (say we create two tables with the same name: my approach makes it trivial to tag that as an error in the changeset and display it to the user using the normal Phoenix components).
I’m honestly very surprised that morphdom’s performance is so bad on what doesn’t look like a very big page (~10.000 HTML elements, < 200 input elements). It seems like I’ve bought into the hype of thinking that LiveView would scale to moderately sized dynamic webpages, when in fact all examples I see of actual LiveViews are render extremely small web pages… Although Elixir definitely scales up to lots of events, the javascript seems like a serious bottleneck!
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixir-ls
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
olivermt
Throw everything into live components. Little bit more memory, much better diffs.
kevinschweikert
See also Any suggestions on a HTML/CSS/JS TreeView that fits into the LiveView environment. The use case was different but the performance optimisation should be the same
rvnash
A while back I was having a problem with an app of mine similar to this. I was rendering a very large list of small elements, and a few items in that list were changing. This was causing a complete re-render. Someone suggested using LiveView streams, and that did the trick. In the end I wrote a little function to take the before and after lists, and come up with the set of inserts/deletes to the stream. Look at this thread and the function at the end.
bartblast
Try adding stable HTML
idattributes to your table containers and cells. I’m not sure exactly how LiveView will treat this underneath, but it may help both LiveView’s diffing and Morphdom’s DOM reconciliation process, even when dealing with large updates.garrison
To expand on the above answers (which are all valid), the problem you’re running into is that LiveView is not capable of properly diffing collections. So when you do something like
<div :for={row <- @rows} />, LiveView has to send down the entire list of rows if a single one is changed.If you wrap your rows in LiveComponents, LiveView is able to diff them individually, so only one row will be sent when it’s updated. If you need further optimization you could then also wrap the columns in LiveComponents.
The LiveComponents obviously have some overhead but they are in-process so they share memory with the parent LiveView (the assigns won’t be copied).
You can also look into using Streams but I wouldn’t recommend that for this use-case unless you really need to optimize performance. LiveComponents will be enough here, I think.
garrison
Unfortunately I don’t think this will make much difference. And the diffs on the wire would still be large anyway.
tmbb
I did try this, and as I expected I didn’t get any benefit. Having stable IDs doesn’t really make much of a difference because Morphdom has to inspect the children anyway to see if anything has changed. I did look up whether there were any attributes I could use to tell Morphdom that the children of that node didn’t change, but there aren’t. What one could do would be to use (for example) the
phx-ignoreattribute and set it conditionally if the children haven’t changed. I did not try that yet, as that would require inspecting changesets for changes. And anyway, IDK exactly how to do that if the changeset has already been converted to a form. But I guess looking that up would be the next step.sodapopcan
Was following your long thread about streams and whatnot and I think this was mentioned there too, but is this really true? Because assigns are shared why would there be any additional overhead? Like, there’s a single additional BEAM file created per LiveComponent model, but that isn’t something I would consider “overhead”? Is there something else I’m missing? I curious as I don’t have the best grasp on internals here and want to learn. I’ve always found way the docs are worded to be a bit confusing as well: To paraphrase: “Don’t pass assigns wholesale because they are all passed even if some aren’t used but also it doesn’t matter because memory is shared”
It makes sense from a maintainability standpoint but not from a performance one (again, unless I’m wrong).
tmbb
Hm… It turns out this doesn’t seem to work because if we set the
phx-update="ignore"attribute at any point, conditional on the fact that the changeset for that “subform” doesn’t have any changes, Phoenix will keep ignoring it when the subform does have changes in the future.garrison
Well first you may have noticed that I’m essentially borrowing that line from the docs
But in a general sense, any code has overhead. What I (and the docs) am trying to clarify is that LiveComponents have much less overhead than embedding a child LiveView, which is what most people probably think LiveComponents are when they see the name.
Elixir is functional (immutable) and data structures like maps are essentially what are called “persistent data structures”, where the old version of the map stays around somewhere and the new version borrows from it. Since everything is immutable this is easier in some ways and harder in others. I’m not an erlang core dev obviously (and there are some lurking around on here), so I don’t want to speak past my expertise. But the point I’m making is:
If you have a map
%{foo: "bar", hello: "world"}and then you callMap.put(...)on it, you get back a new map. But the contents of the old map are not (necessarily) copied, they’re usually just kept around as references. In practice this is more complicated, obviously.That doesn’t mean the new map has no overhead, but it’s less than you might naively think. When you pass your data around to all of the LiveComponents the same thing is happening, and so there is a bit of overhead (keeping track of all those
assignsmaps and so on), but it’s not literally duplicating everything.Of course, if you used a child LiveView (which has its own process), then you would be literally duplicating everything.
I have read some of the code recently (I think you know why…), but I don’t have a perfect grasp on it either. But essentially, there is some bookkeeping going on - have a look at
diff.exandchannel.exand look for LiveComponent bits, that’s where I found some of it.There is also some extra code to handle them on the client. I found it but I’ve already forgotten where…