wandres.dev
RENDIMIENTO Y SOLID 2.0 · lo que viene

De 1.x a 2.0: qué se rompe, qué se mantiene y cómo migrar

El inventario honesto del salto de versión mayor. Qué permanece idéntico —el modelo mental, los signals, el control flow, el JSX—, qué cambia de forma observable —batching automático, timing de efectos afinado, createAsync ascendido al núcleo, el store con projections—, y la estrategia de migración que hace el salto barato: adoptar hoy los patrones que ya son 2.0, migrar por hojas con la capa de compatibilidad, y no reescribir lo que no se rompe.

⏱ 16 min

Un cambio de versión mayor asusta con razón: la palabra breaking evoca reescrituras y noches en vela. El salto de Solid 1.x a 2.0 es de los benignos, y esta lección explica por qué con precisión de inventario. La mayoría de tu código —componentes, JSX, signals, control flow— cruza sin tocar una línea, porque el modelo mental no cambia; lo que se mueve es el runtime debajo, y solo aflora en un puñado de comportamientos observables. Saber exactamente cuáles son, y adoptar desde hoy los patrones que ya hablan el idioma de 2.0, convierte una migración temida en un trámite incremental que puedes hacer por partes y sin drama.

🎯 Al terminar esta lección sabrás
  • Distinguir lo que se mantiene idéntico de lo que cambia de forma observable.
  • Entender el batching automático y el timing de efectos afinado como cambios concretos.
  • Reconocer createAsync ascendido al núcleo y el store con projections.
  • Aplicar una estrategia de migración incremental por hojas con capa de compatibilidad.

Lo que se mantiene: el modelo mental intacto

Empecemos por la buena noticia, que es la mayor parte. El modelo mental de Solid no cambia en 2.0: el componente sigue corriendo una vez, la reactividad sigue siendo de grano fino, no aparece un virtual DOM ni un ciclo de render. createSignal, createMemo, createEffect conservan su forma y su semántica de lectura. El control flow —Show, For, Index, Switch, Dynamic, Portal— se usa igual. El JSX compila igual, y las reglas de oro —leer llamando, no destructurar props, onCleanup atado al dueño— siguen vigentes palabra por palabra. Suspense y ErrorBoundary son las mismas fronteras, ahora alimentadas por más del grafo.

Esto no es casualidad ni suerte: es consecuencia de que 2.0 reescribe el runtime, no la API de superficie. La reactividad por debajo es otra —con nodos que saben estar pendientes, como viste en el capítulo del runtime—, pero la piel que tocas como programador se preservó a propósito, porque la coherencia del modelo era ya su mayor activo. Migrar Solid no es reaprender Solid.

// Este componente es byte por byte idéntico en 1.x y en 2.0
function Contador() {
  const [n, setN] = createSignal(0);
  const doble = createMemo(() => n() * 2);
  return (
    <Show when={n() >= 0} fallback={<p>negativo</p>}>
      <button onClick={() => setN(n() + 1)}>{doble()}</button>
    </Show>
  );
}

Si la mayor parte de tu base de código se parece a esto —signals, memos, control flow, JSX—, la mayor parte de tu base de código no cambia. Ese es el tamaño real de la migración: no el de la reescritura del motor, sino el del puñado de puntos donde el motor nuevo se comporta de forma observable.

Lo que cambia: el inventario de rupturas

Las rupturas existen, pero son pocas y localizadas. Conviene tenerlas en una tabla mental antes que en la sorpresa de un bug.

Área 1.x 2.0 Impacto
Batching Manual con batch en varios casos Automático por el planificador batch pasa a redundante
Timing de efectos Semántica de flush con matices Planificado y más predecible Efectos que dependían del orden fino
Datos async createResource como norma createAsync ascendido al núcleo Cambia el patrón por defecto
Store Store clásico Reescrito con projections API de estado derivado async
Transiciones Añadido sobre el runtime Parte del núcleo Semántica más uniforme

Ninguna de estas filas te obliga a reescribir un componente entero. La mayoría son cambios que simplifican —quitas un batch que ya no hace falta, escribes createAsync en vez de orquestar un recurso— y las que requieren cuidado son las de timing, porque un código que se apoyaba en el orden exacto de ejecución de efectos en 1.x puede observar un orden distinto en 2.0. Ese es el punto donde conviene tener pruebas.

Hay una asimetría reconfortante en esta tabla: casi todas las rupturas se resuelven borrando código, no escribiéndolo. Migrar aquí no es el trabajo habitual de un salto mayor —reescribir para adaptarse a APIs nuevas más ceremoniosas— sino su opuesto: retirar el andamiaje que el runtime nuevo vuelve innecesario. Cada batch que quitas, cada tríada de signals de carga que colapsas en un createAsync, cada comprobación de orden que sustituyes por una dependencia declarada, deja el código más corto y más claro que antes. Un salto de versión mayor que adelgaza tu base de código en vez de engordarla es una rareza que conviene aprovechar sin recelo.

// 1.x: recurso mas comprobaciones de estado repartidas por el JSX
const [datos] = createResource(id, cargar);
// en la vista: datos.loading, datos.error, guardias contra undefined

// 2.0: derivacion declarativa; carga y error suben a las fronteras
const datos = createAsync(() => cargar(id()));
// carga -> Suspense ; error -> ErrorBoundary ; valor -> datos()
⚠️
El código frágil al timing es el que hay que auditar, no el idiomático

Si un efecto tuyo funcionaba solo porque se ejecutaba justo antes o después de otro, sin que esa dependencia estuviera declarada, era frágil ya en 1.x y 2.0 lo destapará. La cura no es pelear con el nuevo planificador sino declarar la dependencia que faltaba: usar on para hacer explícito qué dispara el efecto, o un memo intermedio que ordene el flujo por datos y no por casualidad de scheduling. El batching automático y el flush planificado premian el código que declara sus dependencias y castigan el que dependía de un orden accidental. Migrar es una buena excusa para pagar esa deuda.

Batching automático y timing de efectos

Merece detenerse en el cambio más visible en el día a día: el batching automático. En 1.x, varias escrituras sueltas podían disparar varias propagaciones, y usabas batch para agruparlas en una sola. En 2.0 el planificador agrupa por defecto: escribas donde escribas dentro de un mismo tick, la propagación ocurre una vez al final. batch no desaparece —sigue exportándose por compatibilidad— pero se vuelve, en la práctica, un no-op que ya no necesitas escribir.

// 1.x: sin batch, dos propagaciones; con batch, una
batch(() => {
  setNombre("Ada");
  setApellido("Lovelace");
});

// 2.0: el planificador agrupa solo; batch sobra
setNombre("Ada");
setApellido("Lovelace"); // una sola propagacion al final del tick

El corolario en efectos es que su ejecución se vuelve más predecible y menos sensible a cuántas escrituras la precedieron. Un efecto que en 1.x podía correr dos veces ante dos escrituras seguidas, en 2.0 corre una vez con el estado ya consolidado. Esto es casi siempre lo que querías; el único código que lo nota mal es el que —de nuevo— contaba con el orden o la frecuencia exacta de ejecución en lugar de con sus dependencias declaradas.

Conviene subrayar qué no cambia entre las utilidades de control fino, para que la migración no siembre miedos infundados. untrack sigue leyendo sin suscribirse, on sigue declarando dependencias explícitas con su opción defer, y getOwner con runWithOwner siguen gobernando el árbol de dueños igual que antes. Lo que se vuelve redundante es solo batch, porque su trabajo lo asume el planificador; el resto del vocabulario de reactividad fina permanece intacto y con la misma semántica. La regla mental es que 2.0 te quita una herramienta que ya no necesitas, no te obliga a aprender diez nuevas.

// on y untrack no cambian: siguen siendo la forma de controlar el rastreo
createEffect(on(fuente, (v) => registrar(v), { defer: true }));
const instantanea = untrack(() => contador()); // lee sin suscribir, igual en 2.0

Estrategia de migración: incremental y por hojas

La migración no se hace de golpe ni se hace a ciegas. Se hace incremental, por hojas y apoyada en la capa de compatibilidad que 2.0 ofrece para que 1.x y los patrones nuevos convivan durante la transición. El plan tiene un orden natural.

Primero, adopta hoy lo que ya es 2.0: escribe datos con createAsync y query en vez de con createEffect más signals de estado, y deja de apoyarte en batch donde el agrupamiento automático lo hará por ti. Cada componente que escribas así hoy es uno que no tendrás que tocar mañana. Segundo, cuando el salto llegue, migra por hojas: convierte primero los componentes de hoja —los que nadie depende de ellos— y sube por el árbol, porque un fallo en una hoja es fácil de aislar. Tercero, fija versiones y prueba el timing: bloquea la versión durante la migración, ejecuta tu suite y presta atención especial a los efectos que dependían del orden, que son los únicos candidatos reales a romperse.

El proceso se apoya en herramienta, no solo en disciplina. El equipo de Solid publica una guía de migración con la lista exacta de cambios y, donde es posible, codemods que reescriben los patrones mecánicos —los que se pueden transformar sin criterio humano— dejándote a ti solo los que requieren juicio. La capa de compatibilidad permite además que un mismo proyecto tenga módulos ya migrados conviviendo con módulos aún en 1.x, de modo que no hay un día del big bang en que todo debe funcionar a la vez. Migras un trozo, lo pruebas, lo despliegas, y sigues; el riesgo se reparte en muchos pasos pequeños en lugar de concentrarse en uno grande.

flowchart LR
A[hoy en 1.x] -->|adopta createAsync y evita batch| B[codigo ya idiomatico 2.0]
B -->|capa de compatibilidad| C[migra por hojas]
C -->|fija version y prueba timing| D[app en 2.0]
style A fill:#f9e2af,color:#11111b
style B fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b

La regla que gobierna todo el proceso es simple y liberadora: no reescribas lo que no se rompe. La tentación en un cambio mayor es aprovechar para reescribir todo con el estilo nuevo; resístela. Lo que funciona en 1.x y no toca ninguna de las filas de la tabla de rupturas seguirá funcionando en 2.0 sin cambios, y reescribirlo solo introduce riesgo sin beneficio. La migración barata es la que toca lo mínimo: los pocos puntos que de verdad cambian, y ni uno más.

La disciplina de tocar lo mínimo también protege tu historial de versiones: un diff pequeño y quirúrgico se revisa, se prueba y se revierte con facilidad si algo falla, mientras que una reescritura masiva mezcla la migración con cambios de estilo y vuelve imposible saber qué introdujo un bug. Separa siempre la migración de la refactorización estética; son dos trabajos distintos y juntarlos multiplica el riesgo de ambos sin ganar nada.

🧱

Se mantiene

Modelo mental, signals, memos, control flow, JSX y las reglas de oro. La piel que tocas se preservo a proposito.

🔁

Batching automático

El planificador agrupa escrituras solo. batch pasa de necesario a redundante y se conserva por compatibilidad.

⏱️

Timing afinado

Efectos mas predecibles. El unico codigo en riesgo es el que dependia del orden accidental, no del declarado.

🌿

Migra por hojas

Adopta hoy lo que ya es 2.0, sube desde las hojas con la capa de compatibilidad, y no reescribas lo que no se rompe.

ℹ️
2.0 llega por etapas, no en un big bang

El salto no es una fecha en la que todo cambia a la vez. Buena parte de 2.0 se incubó y publicó antes en Solid Router —createAsync, query, action—, de modo que quien usa esos primitivos ya vive en el futuro por adelantado. La estrategia del equipo es que el ecosistema pruebe las piezas nuevas en producción antes de que el núcleo las absorba, lo que hace que la migración se sienta como una serie de pasos pequeños y ya rodados, no como una reescritura de golpe. Consulta la guía de migración oficial para el estado exacto de cada pieza en el momento en que migres, porque los detalles finos se estabilizan hasta el último tramo.

La migración es barata porque Solid separó lo que ves de lo que corre

La lección estratégica de este salto trasciende a Solid y habla de cómo se diseñan bibliotecas que duran. Un cambio de versión mayor es doloroso cuando la API de superficie está soldada a la implementación, porque entonces mejorar el motor obliga a cambiar el volante, y cambiar el volante obliga a cada usuario a reaprender a conducir. Solid pudo reescribir su runtime entero —introducir nodos con estado, batching automático, un planificador consciente de lo asíncrono— y aun así dejar casi intacta la superficie, porque esa superficie nunca fue un reflejo directo del motor sino un contrato deliberadamente estable por encima de él. createSignal no promete cómo se propaga un cambio; promete que leer devuelve el valor vigente y escribir lo actualiza. Mientras esa promesa se cumpla, el motor debajo puede reescribirse de arriba abajo sin que tu código lo note. Ese desacople entre lo que ves y lo que corre es lo que hace la migración barata, y es una decisión de arquitectura que Solid tomó desde el principio al hacer de los primitivos funciones simples con contratos mínimos en vez de objetos con superficie ancha. La consecuencia para ti como profesional es doble. Primero, práctica: puedes adoptar 2.0 sin miedo, porque el coste está acotado a un inventario corto y no a una reescritura. Segundo, formativa: cuando diseñes tus propias abstracciones —primitivos, stores, componentes—, recuerda que la estabilidad de una API no está reñida con la evolución de su motor si mantienes el contrato estrecho y la implementación oculta. Solid acaba de darte, en vivo, la demostración de que se puede cambiar todo por dentro sin romper nada por fuera, y esa es una lección de ingeniería que vale mucho más que la lista de qué se rompe.

⚔️ Planea tu migración
  1. Recorre tu código y marca cada uso de batch; decide cuáles se vuelven redundantes con el agrupamiento automático de 2.0.
  2. Busca un efecto que funcione por el orden en que corre y no por sus dependencias declaradas; reescríbelo con on para hacer explícita la dependencia.
  3. Convierte un createEffect con signals de carga y error en un createAsync con Suspense, y confirma que ya es código 2.0 idiomático.
  4. Dibuja el árbol de componentes de un módulo y ordena una migración por hojas, nombrando cuál convertirías primero y por qué.
  5. Elige tres componentes que no tocan ninguna fila de la tabla de rupturas y argumenta por qué reescribirlos sería añadir riesgo sin beneficio.