Current section

Files

Jump to
excessibility lib telemetry_capture analyzers component_rerender.ex
Raw

lib/telemetry_capture/analyzers/component_rerender.ex

defmodule Excessibility.TelemetryCapture.Analyzers.ComponentRerender do
@moduledoc """
Detects unnecessary component re-renders.
When components are present and renders fire without assign changes,
child components are likely re-rendering unnecessarily.
## Thresholds
- Warning: 3+ wasted renders with components present
- Critical: >50% of renders wasted with components present
## Output
%{
findings: [%{
severity: :warning,
message: "4 stable components across 8 renders, but only 2 had assign changes...",
events: [3, 4, 6, 7, 8],
metadata: %{component_count: 4, wasted_renders: 5, total_renders: 7}
}],
stats: %{render_efficiency: 0.29}
}
"""
@behaviour Excessibility.TelemetryCapture.Analyzer
@wasted_render_min 3
def name, do: :component_rerender
def default_enabled?, do: false
def requires_enrichers, do: [:component_tree]
def analyze(%{timeline: []}, _opts), do: %{findings: [], stats: %{}}
def analyze(%{timeline: timeline}, _opts) do
render_events =
Enum.filter(timeline, fn event ->
String.contains?(event.event, "render") and Map.has_key?(event, :component_count)
end)
max_components =
render_events
|> Enum.map(&Map.get(&1, :component_count, 0))
|> Enum.max(fn -> 0 end)
if max_components == 0 or length(render_events) < 2 do
%{findings: [], stats: %{}}
else
wasted =
Enum.filter(render_events, fn event ->
changes = Map.get(event, :changes, %{})
map_size(changes) == 0
end)
total = length(render_events)
wasted_count = length(wasted)
efficiency = if total > 0, do: (total - wasted_count) / total, else: 1.0
findings = build_findings(wasted, wasted_count, total, max_components)
%{
findings: findings,
stats: %{render_efficiency: Float.round(efficiency * 1.0, 2)}
}
end
end
defp build_findings(wasted, wasted_count, total, component_count) do
if wasted_count < @wasted_render_min do
[]
else
wasted_ratio = wasted_count / total
severity = if wasted_ratio > 0.5, do: :critical, else: :warning
wasted_sequences = Enum.map(wasted, & &1.sequence)
[
%{
severity: severity,
message:
"#{component_count} stable components across #{total} renders, but only #{total - wasted_count} had assign changes — #{wasted_count} renders may be unnecessary. Consider LiveComponent with explicit update/2",
events: wasted_sequences,
metadata: %{
component_count: component_count,
wasted_renders: wasted_count,
total_renders: total,
wasted_ratio: Float.round(wasted_ratio * 1.0, 2)
}
}
]
end
end
end