There is a live form, with some inputs and a select with large number of options (about 200). They are transferred, the form is rendered. Now the form is validated on user input. Let’s say the user types some text into one of the text inputs, the form gets validated, and… resends the whole set of select options even if there was NO change to the select backed field. I would at least partially understand the need to resend stuff if the selected option was changed - right, then the selected attribute needs to be repositioned and maybe it is not yet optimised so as to do such precise cuts. But nothing is changed in the select.
The select in question is rendered in heex as follows:
When testing I already tried hoisting options to assigns, eliminating all non-assign-based variables from the whole template - no change. How do you handle such cases? Or what am I doing wrong here?
Ah, probably because of the Phoenix.HTML.Form.options_for_select call, which is one of a few remnants of the pre-liveview era. You could probably port this one to a more heex approach, especially if you do not support all the possible permutations supported by options_for_select. With keyed comprehensions you should be able to bring that down.
this stopped resending all the options all the time (easily generating megabytes range of traffic on an even mildly complex form) but there is still a caveat there. It doesn’t send the full set of options but still keeps sending hundreds of nodes (one for every option) just to set/reset the selected attribute. All that huge list consists of empty strings for all options but one. Plus the encoding overhead of course.
If I understand correctly the LV sources, this would have to be changed on the LV change tracking level where it would have to keep track of selected option and on change send only two nodes - one empty string for option to be reset and one "selected” for the option to receive this attribute. Unless anyone has a better idea I could possibly analyse this deeper and maybe craft a PR or so, but it would be good to first know for sure if that’s the right – and also maintainers’ acceptable – direction.
For now I built a somewhat hacky workaround where I don’t set any option to be selected but pass a data- attribute instead and have a small hook catching it and setting the select value accordingly. Altogether it brought the validation payload size on this one particular form down from over 20KiB to about 2KiB. So in other words this works, but it doesn’t feel right.
I’m not sure you can get rid of all the empty strings then. In the end them being empty means LV already knows there’s nothing to update. With gzip enabled this should also be compressed quite well.
The way I understand it is the reason they are all empty is not that “there is nothing to update” but rather that all the options are “reset” from potentially being selected, except the one which is in fact selected. What it sends are empty string for all but one option, which receives the non-empty "selected" string instead. How I further (admittedly with only a shallow analysis) understand this is that there is no track of what was selected in order to “reset” (with empty string) only this particular one (or none - if the selection didn’t change).
With most of them being repetitive It does, but still seems “wrong”. And the fact that I was able to work this around and have only the relevant data- attribute being sent, and only on actual change to the select makes it feel even more “wrong”
That only works because you have intimate knowledge of what those random strings do. LV doesn’t have that luxury. It needs to iterate the whole list of options if any of the options changed. This was true wherever you iterate over a list of data for a while. Nowadays with :key you can give more hints how to optimize, which afaik only helps LV to send less data, but still not nothing per item.
If you want to go even further with optimizing lists you’d need to opt for streams, which allow you do be more granualr with the updates at the expense of a more complex server side.
I think it is because selected is assigned the value of a function call, not an assign. Try extracting the option to another function component with selected={@selected}?
def render(assigns) do
~H"""
<%!-- i don't know if key works or is even needed on a function component --%>
<.my_option :for={{label, value} <- @options} value={value} selected_value={@value} value={value} label={label}/>
"""
end
attr :label, :string, required: true
attr :value, :string, required: true
attr :selected_value, :string, required: true
defp my_option(%{selected_value: selected_value, value: value} = assigns) do
assigns = assign(:selected, option_selected?(selected_value, value)
~H"""
<option value={@value} selected={@selected}>{@label}</option>
"""
end