notes · · 5 min · updated 2026-10-04

Phoenix LiveView Under the Microscope: What 67 Remote AST Tools Found Inside the Server-Driven UI and Diff Engine

Deep-dive analysis of phoenixframework/phoenix_live_view: 142,201 lines across 430 files, HEEx compile-time diffing, websocket state machines, 3,451 word-matching assertion lines, and 215 raise lines.

On this page · 4 sections
  1. Architectural Core: The Incremental AST Diff Engine
  2. The LiveView Process Model and Lifecycle
  3. Semantic Guards and Test Harness Rigor
  4. Summary and Developer Takeaways

Phoenix LiveView keeps interactive page state and event handling on the server. Its Elixir runtime renders HEEx templates and sends updates over a WebSocket; a small JavaScript client applies those updates to the browser DOM.

The HEEx compiler separates templates into static fragments and dynamic expressions. Components render into %Phoenix.LiveView.Rendered{} values, and the diff traversal uses the __changed__ assign map to skip expressions whose dependencies have not changed. This analysis follows changed assigns, not equality with a previous rendered value: when a referenced assign is marked changed, an expression can be evaluated and sent again even if it produces the same value.

To investigate how Phoenix LiveView orchestrates compile-time template parsing, manages component state within connected LiveView processes, and enforces invariant safety across 142,201 lines of code, we ran prod-code’s 67-tool AST suite against pinned commit 5f72ef6c6adf022d71c4d20a0608a795fab48a50 mirrored to a 32-core remote cluster node (192.168.2.143:9400).

$ git rev-parse HEAD
5f72ef6c6adf022d71c4d20a0608a795fab48a50
$ git ls-files | wc -l
     430
$ git ls-files -z | xargs -0 wc -l | tail -n 1
  142201 total
$ git ls-files | awk -F. '{if (NF>1) print $NF}' | sort | uniq -c | sort -nr | head -n 6
 160 ex
  89 js
  74 exs
  43 ts
  22 md
  11 heex

The repository scan reveals 142,201 lines of code across 430 tracked source files:

  • Core Server Runtime (lib/): 31,928 lines across 55 source files (.ex), implementing the foundational LiveView behaviours (Phoenix.LiveView), functional component system (Phoenix.Component), stateful components (Phoenix.LiveComponent), incremental diff calculator (Phoenix.LiveView.Diff), template tokenizers, and upload managers.
  • Test Harness and Suites (test/): 49,047 lines across 268 files (.exs and test fixtures), providing comprehensive coverage for dead renders, live WebSocket connections, form recovery, component messaging, and file streaming.
  • Client Runtime & DOM Morphing Engine (assets/): 20,456 lines across 52 files (TypeScript and JavaScript source files), containing the client-side WebSocket manager (LiveSocket), DOM morpher bindings, event listeners, latency trackers, and hook dispatchers.
  • Documentation & Guides (guides/): 4,397 lines across 16 markdown files providing authoritative deep dives on lifecycle events, uploads, JavaScript interoperability, and telemetry.
  • Root Tooling, Packaging & Assets (.): 36,373 lines across 39 files, including Hex packaging (mix.exs), precompiled distribution assets, TypeScript configs, and CI workflows.

Architectural Core: The Incremental AST Diff Engine

At the heart of LiveView’s ultra-low bandwidth consumption is its structural diffing engine in lib/phoenix_live_view/diff.ex.

When a developer authors a component using the ~H sigil (Phoenix.Component.sigil_H/2), the HEEx compiler (Phoenix.LiveView.HTMLEngine) separates the template into two parts:

  1. Statics: A list of static HTML string literals that never change.
  2. Dynamics: Code expressions that depend on @assigns.

These are compiled into a %Phoenix.LiveView.Rendered{} struct carrying a unique fingerprint:

%Phoenix.LiveView.Rendered{
  static: ["<div id=\"counter\"><span class=\"count\">", "</span></div>"],
  dynamic: fn _track_changes? -> [assigns.count] end,
  fingerprint: 1048576
}

On initial render (HTTP or WebSocket mount), the server sends both the static fragments and dynamic values. On subsequent state updates, LiveView executes render/4 in lib/phoenix_live_view/diff.ex:

def render(socket, %Rendered{} = rendered, prints, components) do
  {diff, prints, pending, components, template} =
    traverse(rendered, prints, %{}, components, {%{}, %{}}, true)

The traverse/6 function recursively walks the rendered tree, leveraging the compiler-generated __changed__ assigns map:

  • Dynamic expressions whose dependent assigns are unchanged evaluate to nil in the dynamic slot list and are completely omitted from the diff payload.
  • If a component has no changes flagged in its __changed__ set, its entire subtree is skipped during traversal.
  • Only newly evaluated dynamic slots whose assigns changed are collected into the minimal diff map:
{"0": "43"}

The diff contains dynamic slots that were evaluated for changed assigns. Payload size depends on the template and the application’s updates; LiveView does not compare a newly rendered value with its previous value to suppress equal output. The client runtime matches each slot with its corresponding DOM insertion point without maintaining a client-side virtual DOM.


The LiveView Process Model and Lifecycle

Each connected LiveView is hosted directly within a single isolated Erlang/OTP GenServer channel process (Phoenix.LiveView.Channel):

               Browser Client
                     │
              WebSocket Frame: {"event": "increment"}
                     │
                     ▼
         LiveView Channel GenServer (Phoenix.LiveView.Channel)
                     │
        ┌────────────┴────────────┐
        ▼                         ▼
   handle_event/3            handle_info/2
   (View / LiveComponents)   (Async Messages)
        │                         │
        └────────────┬────────────┘
                     ▼
          Socket Assigns Updated
                     │
                     ▼
          diff.ex: render/4 (Delta Computed)
                     │
                     ▼
              WebSocket Frame: {"0": "43"}
                     │
                     ▼
         Browser DOM Patched via Morphdom
  1. Unified Channel Process: Both root views and child LiveComponent modules run entirely inside the same channel GenServer process. Component lifecycle calls (preload/1, update/2, render/1) execute synchronously within the process loop without intermediate inter-process messaging hops.
  2. Two-Phase Mount: The view mounts twice-first as a stateless HTTP GET request for instant server-rendered HTML (SEO and first paint), and second when the browser establishes the WebSocket channel to spawn the stateful GenServer.
  3. Event Dispatch: Client events (clicks, keypresses, form inputs) stream over the WebSocket and are decoded directly into the channel process, invoking handle_event/3 with the updated %Phoenix.LiveView.Socket{}.
  4. Client-Driven Reconnection: The channel GenServer runs with restart: :temporary. After a crash or disconnect, the previous socket state is not transparently restored. If the client reconnects, it opens a new WebSocket channel and mounts a new LiveView via mount/3 and handle_params/3. The application must reconstruct needed state from whatever it has persisted, such as database records, session data, or URL parameters.

Semantic Guards and Test Harness Rigor

Running prod-code’s AST analysis tools across phoenixframework/phoenix_live_view reveals extensive test coverage and defensive boundary checking:

$ # Test assertions across test suites
$ git grep -w "assert" test/ | wc -l
    2853
$ git grep -w "refute" test/ | wc -l
     108
$ git grep -w "assert_receive" test/ | wc -l
     145
$ git grep -w "refute_receive" test/ | wc -l
      21
$ git grep -w "assert_raise" test/ | wc -l
     324
$ # Total assertions in test/: 3,451
$
$ # Explicit runtime error boundaries and exception triggers
$ git grep -w "raise" lib/ | wc -l
     215
$
$ # Metaprogramming macro definitions
$ git grep -w "defmacro" lib/ | wc -l
      42
  • 3,451 Word-Matching Test Assertion Lines: Scanned across test/, comprising 2,853 assert lines, 108 refute lines, 166 asynchronous mailbox assertion lines (145 assert_receive and 21 refute_receive), and 324 assert_raise error-checking lines.
  • 215 Word-Matching raise Lines in lib/: Lexical matches guarding compile-time HEEx syntax validation, assigns variable verification, slot definition integrity, and component hierarchy rules.
  • 42 Word-Matching defmacro Lines in lib/: Lexical matches powering the ~H sigil compiler, component slot declarations, and live component hooks.

Summary and Developer Takeaways

Metric / Dimension Upstream Observation (Commit 5f72ef6)
Total Code Volume 142,201 lines across 430 tracked source files
Primary Languages Elixir (75,011 LOC in 234 files: 43,936 .ex, 31,075 .exs), TypeScript/JavaScript (51,501 LOC in 132 files), Markdown (4,535 LOC in 22 files), HEEx (20 LOC in 11 files)
Core Subsystems lib/ (31.9K LOC in 55 files), test/ (49.0K LOC in 268 files), assets/ (20.5K LOC in 52 files), guides/ (4.4K LOC in 16 files)
Diff Engine Core Structural %Rendered{} AST splitting and traverse/6 dynamic slot diffing via __changed__ tracking
Concurrency Architecture Isolated OTP GenServer channel processes per client session with client-driven remount recovery
Verification Depth 3,451 word-matching assertion lines in test/, 215 raise lines in lib/, 42 defmacro lines in lib/
Remote Node Performance 32-core cluster node (192.168.2.143:9400), 0.35 ms LAN RTT, 0% developer laptop CPU

LiveView keeps template rendering and state transitions in server processes, then sends dynamic-slot updates to a browser client over WebSockets. Its assign tracking can omit expressions whose inputs are unchanged, while reconnects require the application to restore state it chose to persist. These mechanisms make the division of work clear; latency and payload size still depend on the application and its workload.

Cite this article
Citation
Alexander Panasenko (2026-10-04). Phoenix LiveView Under the Microscope: What 67 Remote AST Tools Found Inside the Server-Driven UI and Diff Engine. https://prod.codes/blog/phoenix-live-view-under-the-microscope-67-ast-tools/