Skip to content

Optimize Rendered iodata materialization#4232

Merged
josevalim merged 1 commit into
phoenixframework:mainfrom
preciz:optimize-engine-iodata
May 19, 2026
Merged

Optimize Rendered iodata materialization#4232
josevalim merged 1 commit into
phoenixframework:mainfrom
preciz:optimize-engine-iodata

Conversation

@preciz
Copy link
Copy Markdown
Contributor

@preciz preciz commented May 11, 2026

Avoid building an intermediate reversed list when converting Phoenix.LiveView.Rendered to iodata.

The previous implementation accumulated [dynamic, static | acc] pairs and called Enum.reverse/1 at the end. This builds the same iodata directly in render order:

[static_head, recur_iodata(dynamic_head) | recur_iodata(static_tail, dynamic_tail)]

Benchmark results:

input new avg old avg result new memory old memory
flat 10 108 ns 129 ns old 1.20x slower 320 B 576 B
flat 100 685 ns 828 ns old 1.21x slower 3.13 KB 6.28 KB
flat 1000 6.88 us 8.30 us old 1.21x slower 31.25 KB 44.45 KB
nested 100 0.85 us 1.20 us old 1.42x slower 4.69 KB 9.72 KB
Mix.install([{:benchee, "~> 1.5"}])

defmodule B do
  defmodule R do
    defstruct [:static, :dynamic]
  end

  def build(n, nest \\ nil) do
    s = Enum.map(0..n, &"<span data-i=\"#{&1}\">")
    d = for i <- 1..n, do: if(nest && rem(i, nest) == 0, do: build(5), else: "#{i}")
    %R{static: s, dynamic: fn false -> d end}
  end

  def old(%R{static: s, dynamic: d}), do: old(s, d.(false), [])
  def old(x), do: x
  defp old([s | st], [d | dt], acc), do: old(st, dt, [old(d), s | acc])
  defp old([s], [], acc), do: Enum.reverse([s | acc])

  def new(%R{static: s, dynamic: d}), do: new(s, d.(false))
  def new(x), do: x
  defp new([s | st], [d | dt]), do: [s, new(d) | new(st, dt)]
  defp new([s], []), do: [s]
end

inputs =
  Map.new([{"flat 10", 10, nil}, {"flat 100", 100, nil}, {"flat 1000", 1000, nil}, {"nested 100", 100, 10}], fn {name, n, nest} ->
    r = B.build(n, nest)
    unless IO.iodata_to_binary(B.old(r)) == IO.iodata_to_binary(B.new(r)), do: raise(name)
    {name, r}
  end)

Benchee.run(
  %{"old accumulator + reverse" => &B.old/1, "new body recursion" => &B.new/1},
  inputs: inputs,
  time: 3,
  warmup: 1,
  memory_time: 1
)

Comprehension uses it like this already above:

[static_head, dynamic_head | to_iodata(static_tail, dynamic_tail)]

Avoids intermediate list reversal by using body recursion for list construction, reducing memory allocations.
@SteffenDE SteffenDE requested a review from josevalim May 19, 2026 16:32
@josevalim josevalim merged commit 41570e8 into phoenixframework:main May 19, 2026
8 checks passed
@josevalim
Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants