Skip to content

Allow LiveViews to be adopted (take 2) #4317

Description

@josevalim

One of the issues with LiveView is the double render when going from dead render to live render. Today the lifecycle is roughly:

  1. HTTP request renders the page
  2. The HTTP request process runs mount/3 and render/1
  3. The browser establishes a LiveView connection
  4. A new LiveView process is started
  5. mount/3 runs again

This proposal introduces adoptable LiveViews.

In a nutshell, instead of terminating the LiveView after the dead render, Phoenix starts the LiveView process during the initial request and keeps it alive for a short adoption window. When the WebSocket (or long polling transport) connects, it adopts the existing LiveView process rather than creating a new one. This eliminates the double data loading and should decrease latency, without requiring user state to be transferred between processes or nodes.

Goals

  • Eliminate duplicate mount work between dead and connected renders
  • Reduce connected-render latency
  • Avoid transferring arbitrary LiveView state between processes
  • Work correctly in clustered deployments
  • Require no LiveView changes from application developers

Non-Goals

This proposal does not attempt to solve disconnection recovery. If the client disconnects, LiveView doesn't know if the client has received the last message or not. So in order for reconnections to work, we would need to change LiveView server to keep a copy of all responses and only delete them when the client acknowledges it. That's a separate problem.

We also don't plan to add caching as part of this discussion. Caching is a valid tool to address some of the problems outlined here but it is also almost fully orthogonal, so it can be solved separately.

Lifecycle

When rendering a LiveView page, Phoenix starts the LiveView process, executes mount/3, renders the page, and keeps the process alive. The rendered page includes a signed reference that identifies the LiveView process.

When the browser establishes a transport connection, it sends the signed LiveView reference, Phoenix locates the existing LiveView, attaches to it, and pushes the current diff to the client.

Cluster Support

The LiveView process remains on the node where it was originally started. If the transport connects to the same node, the connection is local and no additional hops occur.

If the transport connects to a different node, transport messages are forwarded to the original LiveView process. To do so, we will likely need to add a feature that allows remote channels to be adapted in Phoenix Channel's source. This is likely where the bulk of the work will be.

We likely want to push some of this to the transport level too as different transports may choose different optimizations:

  • WebSocket: the WebSocket terminates on the node that receives the connection and communicates with the LiveView through a local or remote channel.

  • Long polling: the transport may choose to colocate channel and transport processes to avoid unnecessary cluster hops.

These should be transport-specific and transparent to applications (as transports themselves are transparent).

Why not transfer state?

A common alternative is to create a new LiveView process and transfer assigns or socket state. This works on a single node but not necessarily across nodes because ETS tables, NIF resources, etc. may not be transferable across nodes. Application-specific state may depend on node-local resources too.

Even worse, those sort of pitfalls would only show up in clustered deployments, so unlikely to be spotted during development.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions