Phoenix 1.7.2 released

Posted on March 20th, 2023 by Chris McCord


Phoenix 1.7.2 is out! This minor release includes a couple features worth talking about. Let’s get to it!

Easier Tailwind opt-out

Phoenix 1.7 included tailwindcss by default along with a new form component-based architecture. This makes it super easy to opt-out of Tailwind in favor for another tool or custom CSS, but there were still a few manual steps. Namely, you needed to remove the tailwind.config.js file and the dev :watchers configuration. We introduced a mix phx.new --no-tailwind flag to let folks skip these steps when generating a new project.

Channel and LiveView Socket Draining

Cold deploys at high scale for stateful apps like channels or LiveView can be challenging to orchestrate. Deploys often involve custom tuning your deploy tools or proxies to roll out the release slowly to avoid overloading the new servers with a thundering herd of traffic. Phoenix 1.7.2 introduces a new channel socket drain feature, which orchestrates a batched, staged drain process on application shutdown. Draining is enabled by default and you can configure the shutdown from the socket macro in your endpoint.

From the docs:

  @doc """
  ...
  * `:drainer` - a keyword list configuring how to drain sockets
    on application shutdown. The goal is to notify all channels (and
    LiveViews) clients to reconnect. The supported options are:

    * `:batch_size` - How many clients to notify at once in a given batch.
      Defaults to 10000.
    * `:batch_interval` - The amount of time in milliseconds given for a
      batch to terminate. Defaults to 2000ms.
    * `:shutdown` - The maximum amount of time in milliseconds allowed
      to drain all batches. Defaults to 30000ms.

  For example, if you have 150k connections, the default values will
  split them into 15 batches of 10k connections. Each batch takes
  2000ms before the next batch starts. In this case, we will do everything
  right under the maximum shutdown time of 30000ms. Therefore, as
  you increase the number of connections, remember to adjust the shutdown
  accordingly. Finally, after the socket drainer runs, the lower level
  HTTP/HTTPS connection drainer will still run, and apply to all connections.
  Set it to `false` to disable draining.
  """

If you’re not operating at high scale, you don’t need to to worry about this feature, but it will be there and waiting when you need it. If you’re already operating at high scale, this feature should make deploys less stressful on your infrastructure or allow you to ditch cloud-specific configuration.

JS.exec

We introduced a new JS command for executing an existing command on the page located on another DOM element. This greatly optimizes payloads in certain cases that otherwise involved shoving the same duplicated command in the DOM multiple times. For example, the default modal in your core_components.ex module previously contained a duplicated command for closing the modal in multiple places:

  def modal(assigns) do
    ~H"""
    <div
      id={@id}
      phx-mounted={@show && show_modal(@id)}
      phx-remove={hide_modal(@id)}
    >
      <div id={"#{@id}-bg"}>
      <div>
        <div class="flex min-h-full items-center justify-center">
          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
            <.focus_wrap
              id={"#{@id}-container"}
              phx-window-keydown={hide_modal(@on_cancel, @id)}
              phx-key="escape"
              phx-click-away={hide_modal(@on_cancel, @id)}
    """
  end

We had the same command for phx-window-keydown and phx-click-away, and a similar command on phx-remove. These hide the modal and invoke the caller’s own command both on ESC and when clicking outside the modal, and transition the modal when the user navigates away. This produces a sizable payload when the JS commands contain things like transitions with their own classes and timing values. We can cut the payload by more than half by specifying the command only once and telling LiveView where it can execute it. The new modal looks like this:

  def modal(assigns) do
    ~H"""
    <div
      id={@id}
      phx-mounted={@show && show_modal(@id)}
      phx-remove={hide_modal(@id)}
      data-cancel={JS.exec(@on_cancel, "phx-remove")}
    >
      <div id={"#{@id}-bg"}>
      <div>
        <div class="flex min-h-full items-center justify-center">
          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
            <.focus_wrap
              id={"#{@id}-container"}
              phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
              phx-key="escape"
              phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
    """
  end

We use JS.exec to execute the data-cancel attribute where we share the hide_modal command specified in phx-remove. We have the same behavior as before, but now it’s only sent a single time, instead of three times.

Happy coding!

–Chris