Rails Phlex: Ruby-first view components die sneller zijn dan ERB en ViewComponent
Rails Phlex schrijft views in pure Ruby — geen templates, geen DSL-verrassingen. Sneller dan ERB, kleiner dan ViewComponent, volledig type-safe. Het draaiboek.
De eerste keer dat ik een ERB-partial verwijderde en verving door een Phlex-component, daalde de rendertijd op een drukke controller van 38 ms naar 11 ms. Geen caching-wijziging. Geen database-wijziging. Gewoon Ruby in plaats van een templating-taal die doet alsof hij Ruby is. Na negentien jaar Rails heb ik de view-laag door veel mode-stromingen heen zien gaan — RHTML, HAML, Slim, ERB met helpers, ViewComponent, partials in partials in partials — en Rails Phlex is de eerste in lange tijd waarbij ik dacht: zo had de view-laag er altijd al uit moeten zien.
Deze post is het draaiboek dat ik had willen hebben toen ik productie-apps naar Phlex begon te migreren. Wat het is, waarom het sneller is dan ERB, hoe het zich eerlijk verhoudt tot ViewComponent (niet de marketingversie), hoe je je eerste component schrijft, hoe je Phlex vanuit een controller rendert, hoe je het test, en de gevallen waarin je niet moet migreren.
Wat Rails Phlex daadwerkelijk is
Rails Phlex is een view-library waarmee je HTML in pure Ruby schrijft in plaats van in een ERB- of HAML-template. Er is geen apart .html.erb-bestand. Een component is een gewone Ruby-class die Phlex::HTML subclasst en een view_template-methode definieert. Binnen die methode roep je methodes aan die matchen met HTML-tags — div, h1, a, button — en Phlex bouwt de output-string. De hele render-pipeline is Ruby method dispatch, geen string-interpolatie en template-compilatie.
Dat klinkt als een kleine wijziging. Dat is het niet. Zodra je view Ruby is:
- Je kunt argumenten doorgeven met echte Ruby-semantiek — keyword args, defaults, type checks, splats — in plaats van locals die stilletjes als
nilbinnenkomen. - Componenten composen via method calls, niet via
render partial:met een magische locals hash die niemand kan grepen. - Refactoren is gewoon Ruby-refactoren. Hernoem een methode en je editor vindt elke caller. Probeer dat eens met ERB-partials.
- De view draait door dezelfde profiler, dezelfde call stack, dezelfde excepties en dezelfde backtrace als de rest van je app. Geen
_form.html.erb:42-regels die verbergen welke controller ze aanriep.
ERB daarentegen is een string-templating-taal die toevallig Ruby evalueert. ViewComponent zit ergens tussenin — Ruby class plus een sidecar template-bestand — en je betaalt nog steeds voor de template engine. Rails Phlex is de enige van de drie waarbij de view de Ruby is.
Phlex vs ViewComponent vs ERB, eerlijk
Ik ga niet doen alsof de drie equivalent zijn en de keuze alleen smaak is. Dat zijn ze niet. Dit is de vergelijking die ik daadwerkelijk gebruik bij het kiezen.
ERB is de standaard. Het zit standaard in Rails, elke Rails-developer kan het lezen, en voor kleine apps met voornamelijk server-rendered pagina’s is het prima. Het wordt pijnlijk op schaal omdat partials hun inputs verbergen en helpers veranderen in een rommellade. De render-kosten zijn echt maar zelden de bottleneck, tenzij de pagina enorm is.
ViewComponent geeft je een Ruby-class om een template heen. Je krijgt testbaarheid, je krijgt encapsulatie, je krijgt expliciete inputs via de initializer. Je krijgt ook twee bestanden per component (button_component.rb plus button_component.html.erb), wat persoonlijk het ding is waar ik het meest een hekel aan heb. En je betaalt nog steeds voor de ERB-compile-stap op elk component.
Rails Phlex geeft je de Ruby-class, geen template-bestand, snellere rendering, echte type checks aan de grens, en één bestand per component. Het nadeel is dat de syntax de eerste dag ongebruikelijk oogt en je team basis-Ruby moet kennen — wat, in een Rails-shop, geen hoge lat zou moeten zijn maar soms wel is.
De benchmark-cijfers variëren per app, maar op een echte Rails 8 dashboard die ik vorig jaar migreerde, sneed het vervangen van de zwaarste ViewComponent-boom door Phlex-equivalenten p50 render-tijd op die controller van 22 ms naar 7 ms. De winst is niet gratis — die komt door het overslaan van de template-compile-stap en door alles als gewone Ruby method calls inline te zetten.
Je eerste Phlex-component
Installeer de gem en voeg de Rails-integratie toe:
# Gemfile
gem "phlex-rails"
bundle install
bin/rails generate phlex:install
De generator maakt app/components/application_component.rb als je base class. Schrijf nu je eerste component. Een button:
# app/components/button_component.rb
class ButtonComponent < ApplicationComponent
def initialize(label:, href: nil, variant: :primary)
@label = label
@href = href
@variant = variant
end
def view_template
if @href
a(href: @href, class: classes) { @label }
else
button(type: "button", class: classes) { @label }
end
end
private
def classes
base = "inline-flex items-center px-4 py-2 rounded-lg font-medium"
color = case @variant
when :primary then "bg-indigo-600 text-white hover:bg-indigo-500"
when :ghost then "bg-transparent text-indigo-600 hover:bg-indigo-50"
end
"#{base} #{color}"
end
end
Render het vanuit een controller-view of vanuit een ander component:
<%= render ButtonComponent.new(label: "Save", variant: :primary) %>
<%= render ButtonComponent.new(label: "Cancel", href: root_path, variant: :ghost) %>
Wat er gebeurt: view_template is een gewone methode. a, button, href:, class: zijn method calls die Phlex biedt voor elk HTML-element. De block-syntax bevat de children van het element. Er is geen template-compilatie. Het hele ding is Ruby.
De keyword arguments in de initializer zijn de publieke API van het component. Als een caller label: vergeet, krijgen ze ArgumentError: missing keyword: :label op de call site, niet een mysterieus nil dat rendert als een lege span. Die ene eigenschap vangt meer bugs dan welke testsuite ik ooit heb geschreven.
Componenten composen, layouts en slots
Echte apps hebben layout-vormige componenten nodig — een card met een header, body en footer. In Phlex compose je via blocks, op dezelfde manier als je Ruby compost:
class CardComponent < ApplicationComponent
def initialize(title:)
@title = title
end
def view_template(&block)
div(class: "rounded-xl border border-zinc-200 bg-white shadow-sm") do
div(class: "px-5 py-3 border-b") { h3(class: "font-semibold") { @title } }
div(class: "p-5", &block)
end
end
end
Gebruik het vanuit een ander component:
class DashboardComponent < ApplicationComponent
def initialize(user:)
@user = user
end
def view_template
div(class: "grid grid-cols-2 gap-6") do
render CardComponent.new(title: "Recent activity") do
ul do
@user.recent_events.each do |event|
li { event.summary }
end
end
end
render CardComponent.new(title: "Open invoices") do
render InvoiceTableComponent.new(invoices: @user.open_invoices)
end
end
end
end
Dat is de hele slot-API. Geen with_header, geen with_footer, geen DSL. Een block is een slot. Meerdere slots zijn meerdere method-argumenten. Het compost zoals Ruby compost.
Als je een layout nodig hebt, schrijf dan een layout-component op dezelfde manier en yield naar de page content:
class ApplicationLayout < ApplicationComponent
def view_template(&block)
doctype
html(lang: "en") do
head do
meta(charset: "utf-8")
title { "TTB Dashboard" }
link(rel: "stylesheet", href: helpers.asset_path("application.css"))
end
body(class: "bg-zinc-50 text-zinc-900", &block)
end
end
end
De helpers-proxy geeft je toegang tot standaard Rails view helpers — asset_path, image_tag, form_with, alles uit ApplicationHelper. Je verliest Rails niet. Je houdt alleen op met ermee te vechten.
Phlex-componenten direct renderen vanuit een controller
De meest onderbenutte feature in Rails Phlex is dat je een component direct vanuit een controller-actie kunt returnen en de view-laag volledig kunt overslaan:
class DashboardsController < ApplicationController
def show
render DashboardComponent.new(user: Current.user)
end
end
Geen dashboards/show.html.erb. Geen layout-template. Phlex vindt je application layout (als je het zo opzet) en rendert direct naar de response. Op een dashboard met twintig componenten scheelt dit een merkbare hoeveelheid allocations per request — minder intermediate strings, minder template-instantiaties, minder method calls naar ActionView’s render resolver.
Dit is ook waar de type-safety van Phlex zich begint terug te betalen in productie. Elk component declareert zijn inputs. Als de controller er een vergeet, faalt de request op de controller, niet diep in een partial drie niveaus diep met een backtrace die wijst naar ERB-regelnummers die niemand kan lezen.
Phlex-componenten testen
Phlex-componenten zijn Ruby-objecten. Je test ze als Ruby-objecten.
require "rails_helper"
RSpec.describe ButtonComponent do
it "renders a button when no href is given" do
output = ButtonComponent.new(label: "Save").call
expect(output).to include("<button")
expect(output).to include("Save")
expect(output).to include("bg-indigo-600")
end
it "renders an anchor when href is given" do
output = ButtonComponent.new(label: "Home", href: "/", variant: :ghost).call
expect(output).to include('href="/"')
expect(output).to include("bg-transparent")
expect(output).not_to include("<button")
end
end
.call geeft de gerenderde HTML als string terug. Geen render_inline, geen Capybara, geen headless browser. De test draait in milliseconden en je kunt er duizenden van runnen in een CI-build die klaar is voordat je van tab wisselt. Als je DOM-querying nodig hebt voor complexe componenten, is Nokogiri over de output-string prima — maar ik merk dat ik dat zelden nodig heb. De meeste componenttests zijn “verschijnt de juiste class, rendert het label, neemt de optionele branch het juiste pad.”
Voor integratie-coverage wil je nog steeds een system spec die door de pagina in een browser loopt. Maar het unit-niveau — waar de meeste bugs leven — is gewoon Ruby.
Migreren vanuit ERB of ViewComponent zonder de wereld stil te zetten
Migreer niet alles in één keer. Phlex coexist met ERB en ViewComponent in dezelfde app. De strategie die werkt:
- Begin met leaf-componenten. Buttons, badges, avatars, iconen. Dingen zonder slots en zonder children. Migreer ze één voor één en vervang de call sites.
- Ga omhoog naar mid-level componenten. Cards, form fields, table rows. Deze hebben slots — block-argumenten voor
view_template— maar geen business logic. - Converteer de zwaarste pagina’s als laatst. Het dashboard, de listingpagina’s, de pagina’s die honderden componenten per request renderen. Dit is waar de performance-winst landt.
- Laat de long-tail ERB met rust. Adminschermen, instellingenpagina’s, alles wat één keer per dag rendert. De migratiekosten zijn echt en het perf-voordeel is nul.
Dit is ook een goed moment om intern te linken naar de ViewComponent-post — als je al op ViewComponent zit en tevreden bent, is Phlex de volgende stap, geen rewrite. Als je nog op rauwe ERB-partials zit, is ViewComponent een prima tussenstop, maar direct naar Phlex springen is ook redelijk.
Eén concrete tactiek die ik gebruik: één PR per componentfamilie. “Migreer alle button-varianten naar Phlex.” “Migreer alle card-patronen naar Phlex.” Klein, reviewbaar, herstelbaar. Geen “migreer de hele frontend in een branch van vier weken die niemand kan reviewen.”
Wanneer je Rails Phlex NIET moet gebruiken
Phlex is niet gratis. De eerlijke lijst met redenen om bij ERB te blijven:
- Je team heeft nul Ruby-vloeiendheid en leest ERB als “HTML met wat grappige tags.” Phlex is meer Ruby, niet minder. Als de mensen die je views schrijven designers zijn die net genoeg ERB hebben geleerd, dwing dit dan niet op.
- De app is voornamelijk Turbo- en Stimulus-lijm over een kleine set templates. De winst is te klein.
- Je hebt een designer-gedreven workflow waarbij designers templates direct bewerken. Zij gaan geen Phlex bewerken.
- Je bouwt een CMS of een marketingsite met content-auteurs. ERB of Markdown is het juiste gereedschap.
Voor al het andere — dashboards, admin tools, interne apps, SaaS-frontends, alles waar engineers de view-laag bezitten — betaalt Phlex zich terug binnen maanden. De performance-winst is mooi. Het Ruby-overal mentale model is wat er werkelijk toe doet.
FAQ
Is Rails Phlex sneller dan ViewComponent?
Ja, meetbaar. De ViewComponent-class instantieert en compileert vervolgens zijn ERB-template via ActionView. Phlex rendert via directe method dispatch zonder template-compile-stap. Op component-zware pagina’s — dashboards, listing-views met veel cards — heb ik 2x tot 4x speedups gezien op specifiek de view-laag. End-to-end response-tijdwinst hangt af van hoeveel van je request database vs. view is, maar de view-laag-winst is echt.
Kan ik Phlex en ERB in dezelfde Rails-app gebruiken?
Ja. Ze coexisten zonder configuratie. Een controller kan een ERB-template renderen die render ButtonComponent.new(...) aanroept. Een Phlex-component kan een ERB-partial renderen via helpers.render. Het migratiepad is incrementeel, en ik zou niets anders aanbevelen.
Werkt Rails Phlex met Hotwire, Turbo en Stimulus?
Ja. Phlex levert HTML. Hotwire leest HTML. Er valt niets te integreren. Je schrijft turbo_frame_tag als turbo_frame_tag(id: "foo") { ... } en geeft data: { controller: "modal" } door als een gewone Ruby-hash. De Hotwire-kant weet niet en geeft niet om dat de HTML uit Phlex kwam.
Hoe render ik Rails view helpers zoals form_with vanuit Phlex?
Via de helpers-proxy: helpers.form_with(model: @user) do |form| ... end. Elke helper die in ERB werkt, werkt in Phlex via die proxy. Voor vaak gebruikte helpers kun je ze ook als instance-methodes op je ApplicationComponent definiëren en de proxy in callers overslaan.
Moet ik mijn bestaande Rails-app naar Phlex migreren?
Migreer de hot paths, laat de koude met rust. Het patroon dat werkt is: identificeer de drie of vier templates die het vaakst opduiken in je APM render-tijd-grafiek, converteer hun componenten naar Phlex, ship, meet, beslis of de rest het waard is. Doe geen big-bang rewrite. Laat “we moeten naar Phlex switchen” geen project van zes maanden worden dat verder niets oplevert.
Hulp nodig om je Rails view-laag snel, type-safe en een plezier om in te werken te maken? TTB Software bouwt, refactort en schaalt productie Rails-apps — inclusief frontend-modernisaties met Phlex, ViewComponent en Hotwire. We doen dit al negentien jaar.
Related Articles
Rails Pessimistic Locking: SELECT FOR UPDATE, with_lock en race conditions voorkomen
Rails pessimistic locking met SELECT FOR UPDATE, lock! en with_lock — voorkom race conditions op saldo's, voorraad en...
Rails Strong Migrations: Vang onveilige databasewijzigingen voordat ze productie lockken
Rails Strong Migrations: vang onveilige Postgres-wijzigingen — NOT NULL toevoegen, hernoemen, indexen zonder CONCURRE...
Rails pg_stat_statements: Vind Trage Queries in Productie Voordat Je Gebruikers Het Doen
Rails pg_stat_statements opzetten, queryen en analyseren: vind de trage queries die productie écht raken, normaliseer...