Novedades de Ruby 3.2: YJIT llega a producción
El 25 de diciembre de 2022, como manda la tradición, salió Ruby 3.2. Si en la serie hemos visto versiones de promesas (todas las entregas, en la etiqueta ruby), esta fue la versión de los cumplimientos: el año en que YJIT dejó de ser un juguete y pasó a ser el motivo por el que actualizas Ruby. Y de propina, dos o tres cosas que hoy uso a diario sin acordarme siquiera de cuándo llegaron.
YJIT deja de ser un experimento
La nota de prensa lo decía sin rodeos: "YJIT is no longer experimental". El JIT que el equipo de Shopify había estrenado en 3.1 llegaba reescrito en Rust, probado durante más de un año en cargas de producción reales, y con soporte para x86-64 y arm64: Apple M1/M2, AWS Graviton, hasta una Raspberry Pi 4.
Los números oficiales: un 41 % más rápido que el intérprete (media geométrica sobre yjit-bench), y respecto al YJIT de 3.1, más veloz y con un tercio del overhead de memoria. Esto último era lo importante para mí: en 3.1 activé YJIT en un proyecto y el consumo de RAM me hizo desactivarlo esa misma semana. En 3.2, con la memoria del JIT asignada de forma perezosa y un code GC que libera páginas al llegar al límite, por fin compensaba.
Lo activabas con --yjit (o RUBY_YJIT_ENABLE=1) y punto. En mis aplicaciones Rails la mejora real rondaba bastante menos que ese 41 % de los benchmarks —el cuello de botella de una app típica sigue siendo la base de datos—, pero era gratis: sin tocar una línea de código, tiempos de respuesta visiblemente mejores. Por debajo, además, YJIT aprovechaba las object shapes nuevas del intérprete y una invalidación de constantes más fina. Fontanería invisible que notabas sin saber por qué.
Regexp.timeout: el fin del miedo al ReDoS
Todo el que lleve años con Rails ha vivido esto: una validación con una regex inocente, un usuario (o un bot) que envía una cadena diseñada con mala idea, y un worker de Puma clavado al 100 % de CPU durante minutos. El ReDoS de toda la vida.
Ruby 3.2 lo atacó por dos frentes. Primero, reescribió el matching con memoización: según las notas oficiales, en torno al 90 % de las expresiones pasan a resolverse en tiempo lineal. Su propio ejemplo es elocuente: /^a*b?a*$/ contra 50.000 aes tardaba 10 segundos en 3.1 y 0,003 segundos en 3.2.
Y para el 10 % restante, la red de seguridad:
Regexp.timeout = 1.0
/^a*b?a*()\1$/ =~ "a" * 50_000 + "x"
# => Regexp::TimeoutError al cabo de un segundo
Con posibilidad de excepción puntual por regex (Regexp.new(patron, timeout: Float::INFINITY)) y un Regexp.linear_time? para auditar las tuyas. Puse Regexp.timeout en un initializer de todos mis proyectos aquella misma semana, y ahí sigue en 2026. Es de esas features sin glamur que te ahorran una página de incidencias.
La clase Data: value objects de serie
Llevábamos década y media abusando de Struct para crear value objects, con sus setters indeseados y sus sorpresas. Ruby 3.2 trajo por fin una clase pensada para eso: Data, inmutable por diseño.
Measure = Data.define(:amount, :unit)
d = Measure.new(100, "km") # posicional
w = Measure.new(amount: 50, unit: "kg") # o con keywords
w.with(amount: 40) # => #<data Measure amount=40, unit="kg">
w.amount = 99 # => NoMethodError
Sin setters, con #with para derivar copias y con soporte de pattern matching vía deconstruct_keys. En código Rails es oro para todo lo que no es un modelo: coordenadas, rangos de fechas, resultados de servicios. Donde antes metía una gema de value objects o un Struct con keyword_init: true y cara de disculpa, ahora hay un Data.define de una línea. De todo el 3.2, es lo que más veces he escrito desde entonces.
Argumentos anónimos: la delegación se completa
Ruby 3.1 había introducido el bloque anónimo (def foo(&) = bar(&)). En 3.2 el trío se completó: * y ** sin nombre también se pueden reenviar:
def log_and_create(*, **)
logger.info("creando registro")
Registro.create!(*, **)
end
Para métodos que solo hacen de passthrough —decoradores, wrappers de instrumentación, esa capa de servicios que todos tenemos— es delegación sin ceremonia: ni inventas nombres como args y opts que no usas, ni arrastras el ... cuando solo quieres reenviar una parte.
Ruby en el navegador: WASI y WebAssembly
La feature más espectacular del anuncio era el port de CRuby a WebAssembly/WASI: un binario de Ruby corriendo en el navegador o en un edge serverless, con Asyncify para suplir lo que a WASI le falta (Fiber, excepciones, GC) y un VFS para empaquetar una aplicación entera en un solo .wasm. El playground de TryRuby pasó a ejecutar CRuby de verdad en tu pestaña.
Retrospectiva honesta: jamás lo he usado para nada serio, y en mi entorno no conozco a nadie que sí. Pero sería injusto llamarlo humo: abrió la puerta a playgrounds, documentación ejecutable y experimentos que antes exigían un servidor. Anécdota para el rails-eros medio, semilla para otros.
SyntaxSuggest y errores que ayudan
Ruby 3.2 integró la gema dead_end como SyntaxSuggest: ante un end de más o de menos, el error deja de ser un críptico "unexpected end-of-input" y pasa a señalarte el bloque exacto donde falta la palabra clave. Y error_highlight aprendió a subrayar el argumento concreto que provoca un TypeError o un ArgumentError.
Suena menor. No lo es: es la diferencia entre veinte segundos y cinco minutos cada vez que te comes un end a las siete de la tarde. La calidad de vida también es una feature. En el mismo saco: Set pasó a estar disponible sin require "set".
Retrospectiva desde 2026
Ruby 3.2 cerró su ciclo de vida el pasado marzo, así que ya se puede juzgar con el partido acabado. Lo que importó: YJIT en producción (el punto de inflexión de toda la era 3.x del rendimiento), Data, Regexp.timeout y las regexes lineales. Lo que se quedó en anécdota: WASI para el común de los mortales, y un MJIT reimplementado en Ruby puro que ya olía a despedida —de eso hablaremos en la siguiente entrega—.
Pocas versiones han envejecido tan bien. Si 3.0 fue la promesa y 3.1 el ensayo, 3.2 fue la primera versión de Ruby moderno que recomendé desplegar en producción sin ninguna nota al pie. Las notas oficiales están aquí, y el detalle exhaustivo, como siempre, en rubychanges.