Why is .link navigate={@path} faster than push_navigate(to: path)?

I am totally baffled…

While this is lightning fast:

<.link navigate={@path} class={["w-full leading-10 inline-block", is_nil(@icon) && "pl-12"]}>
  [...]
</.link>

The following is very slow:

<div phx-click={JS.push("navigate", value: %{path: @path})}>
  [...]
</div>

The handler is:

@impl true
def handle_event("navigate", %{"path" => path}, socket) do
  if path == socket.assigns.current_path do
    {:noreply, socket}
  else
    {:noreply, socket |> assign(current_path: path) |> push_navigate(to: path)}
  end
end

No error is logged but I see the navigation sidebar remounting which should not happen because it is sticky.

Just tried to avoid consecutive page navigates while hitting the same link over and over again

Am I missing something obvious?

Edited: And I am talking about fractions of a second compared to 5+ seconds

I can’t reproduce, both are very snappy for me. Of course I didn’t try with a sticky LiveView on the page. Maybe it’s something to do with that? If you remove it does it fix it?

@sodapopcan Thank you for testing! And indeed…

As soon as I set sticky to false it is working like a breeze:

<%= live_render(@socket, BackendWeb.Live.Components.Navbar,
  id: "workspace-sidebar-container",
  session: %{"locale" => @locale,
    "current_user" => @current_user,
    "current_path" => @nav_active_path
    },
  sticky: false,
  container:
    {:aside,
      class: "fullscreen:hidden flex-shrink flex-grow-0 bg-slate-200 dark:bg-slate-600"}
) %>

Though I need the navigation sidebar being sticky so that the accordion and a few other things are keeping the state.

To be honest: there should be no difference if I am using .link or push_navigate

There shouldn’t no. I’m not sure as I’ve never actually used a nested LiveView before and don’t have the cycles to play around with it right now. The only other thing I can think of is to try calling the event something other than “navigate” as perhaps that is conflicting somehow, though I have no idea why that would only be a problem when there is a nested LV. Is there anything in weird in the network tab or JS console? Does the nested LV take a while to load on its own? Again, I don’t know much about the mechanics of sticky LVs but I would think that if anything changes in the session variables that it would cause a re-render, no? I honestly don’t know!

I posted why I think this might be happening on your github issue, but in case you’re stuck in prod and really need to use push, you could try targeting your push event at the main phx liveview.

This means you’ll also need to include the event handler in all your (main) views. You can do this pretty simply by including it in your MyAppWeb#live_view quoted block, which gets inserted when you include use MyAppWeb, :live_view in your modules.

I think you can just use [data-phx-main] as the selector, unless you have some custom setup going on, or you may wish to explicitly set an id/container on the main view.

In your navbar:

phx-click={JS.push("navigate", target: "[data-phx-main]", value: %{path: path})}

In your MyAppWeb

def live_view(opts \\ []) do
  quote do
    def handle_event("navigate", ...) do ... end
  end
end

I dont think this is a great solution, but I am curious if it fixes your problem which would probably confirm what I wrote on the issue.

You may also need to dispatch an additional “update the navbar state” event to the navbar to update the “current link”, depending on your setup.

The slowdown came from the sticky navigation LV getting reloaded if using push_navigate.

Switched to a hook for now to add some logic on the server side:

<.link id={"link-#{UUID.uuid1()}"} navigate={@path} phx-hook="LinkClicked">
   [...]
</.link>
1 Like

Hey @Terbium-135 can you elaborate on the id={"link-#{UUID.uuid1()}"} bit? This will generate a new UUID every time the page renders, which will remove and then re-add the dom element. This seems probably undesirable. In general, you should avoid any function calls in your views that have side effects or side causes like generating random numbers or doing say DateTime.utc_now(). Those should all be driven from assigns.

2 Likes
  • A unique ID is a requirement for an element with a hook attached using phx-hook.
  • These DOM elements are only used in the navigation sidebar - which is sticky. Liveviews navigated to are shown in another part of the screen. So there is no reload of the sticky sideview happening unless the user does a full page reload.

Is it still desireable to use assigns?
I don’t see any side effects here. But I might be wrong

How about interpolating the @path assign instead of the UUID.uuid1() to create an id that is both unique and static?

LiveView uses those ids to “keep tabs” and intelligently optimize across re-renders/mounts/navigation events so when they’re generated dynamically within the template itself, LiveView’s “loses track”.

1 Like

You’re confusing a reload with a re-render. Any render will generate a new UUID which unloads and then remounts the hook. If you want to use a uuid to generate the DOM id then that’s fine, just do so in mount on the Elixir side, set as an assign, and use that in the template. Generating the UUID itself in the template will result in constantly unmounting and remounting the hook.

1 Like