surface-ui / surface

A server-side rendering component library for Phoenix
https://surface-ui.org
MIT License
2.08k stars 150 forks source link

Way more diff data transfered over the wire when using Surface component than plain LV even if rendered html is very similar #615

Open DaTrader opened 2 years ago

DaTrader commented 2 years ago

Describe the bug

Can't tell if this is a bug, more likely a flaw or technical debt, but I hope it's not the intended behavior.

Altering a SurfaceBulma.Button disabled attribute (only) produces way too much data and I don't think this has anything to do with the component itself, but rather the rendering engine.

Please note that in both cases (Surface and plain LiveView) the provided code is part of a component in a prepended list of such components, thus only the changed component is sent in each case.

1. Plain LiveView

Template code

<button phx-click="add_favorite" disabled={@card.favorite?} size="large" phx-target={@myself}>Add to Favorites</button>
<button phx-click="remove_favorite" disabled={!@card.favorite?} size="large" phx-target={@myself}>Remove from favorites</button>

Rendered html

<button phx-click="add_favorite" size="large" phx-target="1">Add to Favorites</button>
<button phx-click="remove_favorite" size="large" phx-target="1" disabled="">Remove from favorites</button>

Diffs when @card.favorite? toggles:

{
  "2": {
    "5": {
      "d": [
        [
          2
        ]
      ]
    },
    "8": "3"
  },
  "c": {
    "2": {
      "4": " disabled",
      "7": ""
    }
  }
}

2. SurfaceBulma.Button

Template code:

<Button click="add_favorite" disabled={@card.favorite?} size="large">Add to Favorites</Button>
<Button click="remove_favorite" disabled={!@card.favorite?} size="large">Remove from favorites</Button>

Rendered html:

<button phx-click="[[&quot;push&quot;,{&quot;event&quot;:&quot;add_favorite&quot;,&quot;target&quot;:1}]]" type="button" class="button is-large">
    Add to Favorites
  </button>
<button phx-click="[[&quot;push&quot;,{&quot;event&quot;:&quot;remove_favorite&quot;,&quot;target&quot;:1}]]" type="button" class="button is-large" disabled="">
    Remove from favorites
  </button>

Diffs when @card.favorite? toggles:

{
  "2": {
    "5": {
      "d": [
        [
          1
        ]
      ]
    },
    "8": "1"
  },
  "c": {
    "1": {
      "3": {
        "0": {
          "0": {
            "0": {
              "0": {
                "0": " phx-click=\"[[&quot;push&quot;,{&quot;event&quot;:&quot;add_favorite&quot;,&quot;target&quot;:1}]]\"",
                "1": "",
                "2": " type=\"button\"",
                "3": "",
                "4": "",
                "5": "",
                "6": " class=\"button is-large\""
              }
            }
          }
        }
      },
      "4": {
        "0": {
          "0": {
            "0": {
              "0": {
                "0": " phx-click=\"[[&quot;push&quot;,{&quot;event&quot;:&quot;remove_favorite&quot;,&quot;target&quot;:1}]]\"",
                "1": "",
                "2": " type=\"button\"",
                "3": "",
                "4": " disabled",
                "5": "",
                "6": " class=\"button is-large\""
              }
            }
          }
        }
      }
    }
  }
}

How to reproduce it

Suffice changing LiveView code to using SurfaceBulma.Button component (or vice-versa).

The behavior you expected

The size of the diffs should be virtually identical, especially given that nothing except for disabled changes and the rendered html code has the same number of elements in both cases i.e. 1 per button.

Your Environment

Surface: v0.7.4 LiveView: v0.17.9 Elixir: v1.13.4

djgoku commented 1 year ago

Surface: v0.8.4 LiveView: 0.17.12 Phoenix: 1.6.15 Elixir: v1.14.1

I have attached a chromium websocket logs for a simple LiveView app and the same application but converted to Surface.

Here is the index.ex for the Surface application. I will not be sharing the LiveView application index.ex since it implements the same exact behaviors as the Surface application:

defmodule BinaryNogginWeb.EventLive.Index do
  alias Surface.Components.Link
  use BinaryNogginWeb, :surface_view

  alias BinaryNoggin.Events
  alias BinaryNoggin.Events.Event
  alias BinaryNogginWeb.Live.Modal

  alias BinaryNogginWeb.Router.Helpers, as: Routes

  alias Surface.Components.LivePatch
  alias Surface.Components.LiveRedirect

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :events, list_events())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Event")
    |> assign(:event, Events.get_event!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Event")
    |> assign(:event, %Event{})
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Events")
    |> assign(:event, nil)
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    event = Events.get_event!(id)
    {:ok, _} = Events.delete_event(event)

    {:noreply, assign(socket, :events, list_events())}
  end

  defp list_events do
    Events.list_events()
  end
end

index.html.heex:

<h1>Listing Events</h1>

<%= if @live_action in [:new, :edit] do %>
  <.modal return_to={Routes.event_index_path(@socket, :index)}>
    <.live_component
      module={BinaryNogginWeb.EventLive.FormComponent}
      id={@event.id || :new}
      title={@page_title}
      action={@live_action}
      event={@event}
      return_to={Routes.event_index_path(@socket, :index)}
    />
  </.modal>
<% end %>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Description</th>
      <th>Capacity</th>

      <th></th>
    </tr>
  </thead>
  <tbody id="events">
    <%= for event <- @events do %>
      <tr id={"event-#{event.id}"}>
        <td><%= event.name %></td>
        <td><%= event.description %></td>
        <td><%= event.capacity %></td>

        <td>
          <span><%= live_redirect "Show", to: Routes.event_show_path(@socket, :show, event) %></span>
          <span><%= live_patch "Edit", to: Routes.event_index_path(@socket, :edit, event) %></span>
          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: event.id, data: [confirm: "Are you sure?"] %></span>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<span><%= live_patch "New Event", to: Routes.event_index_path(@socket, :new) %></span>

index.sface

<h1>Listing Events</h1>

{#if @live_action in [:new, :edit]}
  <Modal return_to={Routes.event_index_path(@socket, :index)}>
    <BinaryNogginWeb.EventLive.FormComponent action={@live_action} title={@page_title} event={@event} id={@event.id || :new} return_to={Routes.event_index_path(@socket, :index)} />
  </Modal>
{/if}

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Description</th>
      <th>Capacity</th>

      <th></th>
    </tr>
  </thead>
  <tbody id="events">
    {#for event <- @events}
      <tr id={"event-#{event.id}"}>
        <td>{event.name}</td>
        <td>{event.description}</td>
        <td>{event.capacity}</td>

        <td>
          <LiveRedirect label="Show" to={Routes.event_show_path(@socket, :show, event)} />
          <LivePatch label="Edit" to={Routes.event_index_path(@socket, :edit, event)} />
          <Link label="Delete" to="#" opts={phx_click: "delete", phx_value_id: event.id, data: [confirm: "Are you sure?"]}/>
        </td>
      </tr>
    {/for}
  </tbody>
</table>

<span><LivePatch label="New Event" to={Routes.event_index_path(@socket, :new)} /></span>

With everything implemented above the LiveView application sends this message over the websocket (Phoenix.LiveView.render/1):

1034 characters in size

[
  "4",
  "4",
  "lv:phx-FyfZq0b_pvX_hQSF",
  "phx_reply",
  {
    "response": {
      "rendered": {
        "0": "",
        "1": "",
        "2": {
          "0": "",
          "1": "",
          "2": "<a data-phx-link="patch" data-phx-link-state="push" href="/events/new">New Event</a>",
          "s": [
            "<h1>Listing Events</h1>\\n",
            "\\n<table>\\n  <thead>\\n    <tr>\\n      <th>Name</th>\\n      <th>Description</th>\\n      <th>Capacity</th>\\n\\n      <th></th>\\n    </tr>\\n  </thead>\\n  <tbody id="events">\\n",
            "\\n  </tbody>\\n</table>\\n\\n<span>",
            "</span>"
          ]
        },
        "s": [
          "<main class="container">\\n  <p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info">",
         "</p>\\n\\n  <p class="alert alert-danger" role="alert" phx-click="lv:clear-flash" phx-value-key="error">"
          "</p>\\n",
          "\\n</main>"
        ],
        "t": "Listing Events"
      }
    },
    "status": "ok"
  }
]

This is what is sent to the websocket from our Surface View (1577 characters):

[
  "4",
  "4",
  "lv:phx-FyfZguaRFuSzkANh",
  "phx_reply",
  {
    "response": {
      "rendered": {
        "0": " phx-click=\"lv:clear-flash\"",
        "1": "",
        "2": " phx-click=\"lv:clear-flash\"",
        "3": "",
        "4": {
          "0": {
            "s": [
              ""
            ]
          },
          "1": "",
          "2": {
            "0": "",
            "1": "",
            "2": " data-phx-link-state=\"push\"",
            "3": " href=\"/events/new\"",
            "4": {
              "0": "New Event",
              "s": [
                "",
                ""
              ]
            },
            "s": [
              "<a",
              "",
              " data-phx-link=\"patch\"",
              "",
              ">",
              "</a>\\n"
            ]
          },
          "s": [
            "<h1>Listing Events</h1>\\n\\n",
            "\\n\\n<table>\\n  <thead>\\n    <tr>\\n      <th>Name</th>\\n      <th>Description</th>\\n      <th>Capacity</th>\\n\\n      <th></th>\\n    </tr>\\n  </thead>\\n  <tbody id=\"events\">\\n    ",
            "\\n  </tbody>\\n</table>\\n\\n<span>",
            "</span>\\n"
          ]
        },
        "s": [
          "<main class=\"container\">\\n  <p class=\"alert alert-info\" role=\"alert\"",
          " phx-value-key=\"info\">",
          "</p>\\n\\n  <p class=\"alert alert-danger\" role=\"alert\"",
          " phx-value-key=\"error\">",
          "</p>\\n\\n  ",
          "\\n</main>\\n"
        ],
        "t": "Listing Events"
      }
    },
    "status": "ok"
  }
]

1577 - 1034 = 543 characters

This is pretty significant differences.

If I did everything correctly this should be a nearly 1 to 1 comparision.

chromium-add-event-without-validation.har.txt chromium-add-event-surface-take-without-validation.har.txt

elliotb commented 1 year ago

I notice that there is a recently merged (but unreleased) PR relating to optimising diffs for static props: https://github.com/surface-ui/surface/pull/665