wandres.dev
EL FUTURO DE LA REACTIVIDAD · hacia dónde va

Local-first por defecto

Local-first dejó de ser una técnica exótica para editores colaborativos y empezó a comportarse como lo que siempre quiso ser: una capa de infraestructura comparable a la base de datos o a la red de distribución de contenido. Esta lección analiza qué cambia en la arquitectura de estado cuando sincronizar deja de ser una función que escribes y pasa a ser un servicio que contratas: la lectura se vuelve local y síncrona, la escritura se vuelve optimista por construcción, la capa de peticiones adelgaza hasta desaparecer y la cache de servidor —el patrón que ordenó el frontend durante una década— pierde su razón de existir. Después examina la frontera que se difumina entre cliente y servidor, y es honesta con las tres fuerzas que todavía la sostienen: la autorización cuando el cliente guarda los datos, la evolución del esquema cuando miles de réplicas ejecutan versiones distintas, y el tamaño cuando nunca cabe la base entera en el dispositivo.

⏱ 18 min

Hay una pregunta que la industria lleva veinte años respondiendo de la misma forma sin darse cuenta de que era una pregunta: dónde viven los datos. La respuesta por defecto ha sido el servidor, y de ella se derivó todo lo demás —la capa de API, la cache de cliente, los estados de carga, la invalidación, las actualizaciones optimistas y su rollback—. Ninguna de esas piezas resuelve un problema de dominio; todas resuelven el mismo problema de transporte: que el dato está lejos. Local-first invierte el axioma y pone el dato cerca, con la red degradada a un proceso de fondo que hace converger réplicas. Lo nuevo de esta década no es la idea, que ya conociste, sino su categoría: ha dejado de ser algo que implementas y ha empezado a ser algo que instalas. Y cuando una técnica se convierte en infraestructura, lo que cambia no es una librería: cambia qué capas de tu arquitectura dejan de tener sentido.

🎯 Al terminar esta lección sabrás
  • Distinguir local-first como técnica puntual de local-first como capa de infraestructura contratable.
  • Describir qué piezas del frontend moderno pierden su razón de existir cuando la lectura es local y síncrona.
  • Explicar en qué sentido se difumina la frontera cliente y servidor y en qué sentido no puede difuminarse.
  • Evaluar los tres frenos reales de la adopción por defecto: autorización, evolución del esquema y tamaño.

De técnica a capa

Hay un momento en la vida de cualquier capacidad técnica en que deja de ser una hazaña y pasa a ser un supuesto. El almacenamiento redundante lo fue, el cifrado en tránsito lo fue, la entrega geográficamente distribuida lo fue. Todas empezaron siendo proyectos de ingeniería que solo se permitían las empresas grandes y todas terminaron como casillas que se marcan al crear un servicio. Local-first está atravesando ese umbral ahora mismo, y esta lección trata de qué significa eso para la arquitectura de estado que tú diseñas.

Una técnica se reconoce porque la escribes; una infraestructura, porque la declaras. Durante años, tener datos locales que convergen significaba montar a mano un almacén, un registro de cambios, una política de conflictos y un canal de transporte, y ese coste solo se justificaba en productos cuyo valor central era la colaboración. Lo que ha cambiado es que ese trabajo se ha empaquetado: hoy declaras qué subconjunto de datos quieres cerca y el motor se ocupa de traerlo, mantenerlo fresco y devolver tus escrituras.

// declarar en vez de implementar: la forma sustituye a la peticion
const misTareas = sync.shape({
  tabla: 'tareas',
  donde: { proyecto: proyectoId, archivada: false },
})

// leer es sincrono y local, no hay estado de carga en el camino feliz
const pendientes = misTareas.filter(t => !t.hecha)

// escribir es local primero; el motor propaga y reconcilia
misTareas.update(id, t => { t.hecha = true })

El fragmento anterior no es una API concreta sino la forma que comparten los motores actuales, y su rasgo definitorio es la replicación parcial declarada. Nunca cabe la base entera en el dispositivo, así que todo motor necesita un lenguaje para decir qué viaja: una forma de tabla, una consulta, una regla de pertenencia. Esa declaración es el nuevo contrato de datos de la aplicación, y ocupa exactamente el lugar que antes ocupaba la lista de endpoints.

El signo de que algo se ha vuelto infraestructura es siempre el mismo: deja de aparecer en las reuniones de producto. Nadie discute hoy si conviene usar una red de distribución de contenido o si la base de datos debería tener índices; se dan por supuestos y solo se habla de ellos cuando fallan. La sincronización está entrando en esa categoría, y el indicador que lo confirma es que ya se contrata como servicio gestionado en lugar de escribirse.

Hay una consecuencia organizativa de ese cambio que se nota antes que la técnica. Cuando sincronizar era una técnica, el equipo que la implementaba era el equipo de producto y cada decisión se tomaba a la vez que la funcionalidad. Cuando es una capa, la decisión se toma una vez, arriba, y condiciona a todos los equipos durante años; se parece mucho más a elegir base de datos que a elegir librería de estado.

ℹ️
La replicación parcial es la decisión de arquitectura, no el algoritmo de fusión

Es fácil quedarse con la parte vistosa —cómo se fusionan dos ediciones concurrentes— y perder de vista la que de verdad decide si el producto funciona. El algoritmo de convergencia es un problema resuelto y encapsulado; lo que sigue siendo tuyo es delimitar el subconjunto que cada usuario lleva encima. Si declaras de más, pagas ancho de banda, memoria y tiempo de arranque, y expones datos que no debía ver. Si declaras de menos, vuelven las esperas y con ellas todo el andamiaje que querías eliminar. Esa frontera es el equivalente moderno de diseñar bien los índices de una base de datos: invisible cuando está bien y responsable de casi todo cuando está mal.

Qué desaparece cuando leer es local

El efecto más profundo no es de velocidad, es de eliminación de capas. Repasa el andamiaje que has aprendido a construir y pregúntate qué problema resolvía cada pieza.

🔮

La cache de servidor

Existía porque el dato estaba lejos y releerlo era caro. Con una réplica local autoritativa no hay nada que cachear: la copia buena ya está aquí.

El estado de carga

Era el hueco entre la intención del usuario y la respuesta de la red. Si la lectura es síncrona, el hueco desaparece del camino feliz y solo queda en la primera sincronización.

↩️

El rollback optimista

Se necesitaba porque el servidor podía rechazar lo que ya habías pintado. Si la escritura local es válida por construcción y converge, no hay nada que deshacer.

🔁

La invalidación

Era la forma de enterarse de que otro cambió algo. El motor de sincronización te lo trae; el trabajo de decidir qué recargar deja de existir.

A esas cuatro habría que añadir varias más pequeñas que rara vez se nombran y que suman muchísimo código: el deduplicado de peticiones idénticas en vuelo, la revalidación al recuperar el foco de la ventana, el reintento con espera creciente, la paginación como mecanismo de transporte y la serialización del estado para hidratarlo tras el renderizado en servidor. Ninguna de ellas describe una regla de tu producto.

Que cuatro patrones centrales del frontend moderno se evaporen a la vez debería hacerte sospechar algo importante: no eran patrones de estado, eran patrones de distancia. Los aprendimos como si fuesen sabiduría sobre cómo gobernar datos, y en realidad eran técnicas de compensación de latencia. La prueba es que se disuelven en cuanto se elimina la latencia, sin dejar residuo en el modelo de dominio.

Ese criterio —si desaparece al acercar el dato, era transporte— es tan útil que conviene tenerlo a mano incluso sin adoptar nada. Aplicado a una base de código actual, separa en dos montones lo que describe el negocio de lo que describe la red, y ese corte por sí solo suele revelar que las abstracciones que el equipo considera centrales son, en realidad, andamiaje.

Conviene matizar la eliminación en un punto, porque el matiz es la diferencia entre un producto sólido y una demostración. Ninguna de las cuatro piezas desaparece del todo: se retiran del camino feliz y quedan confinadas a dos momentos concretos, la primera sincronización de un dispositivo nuevo y el intervalo en que un subconjunto se amplía por primera vez. Diseñar bien esos dos momentos es el trabajo que sustituye al que antes hacías en cada pantalla.

Lo que sí sobrevive es todo lo del otro eje. Sigues necesitando una fuente única de verdad, sigues necesitando derivar en vez de duplicar, y sigues necesitando una disciplina de mutación, porque una réplica local mal gobernada produce exactamente los mismos bugs que un objeto global mal gobernado. La reactividad tampoco desaparece: una réplica que cambia sola por debajo, sin que nadie lo haya pedido en esta pestaña, exige un grafo de propagación más fino que nunca.

De hecho, la exigencia sobre la reactividad crece en lugar de bajar. Antes, los cambios llegaban en momentos que tú provocabas y podías agrupar; ahora llegan continuamente, en lotes pequeños y desde fuera, y una propagación gruesa que reevalúe media pantalla cada vez que el motor recibe una fila es exactamente la receta de una interfaz que va a tirones. Local-first y grano fino no son dos tendencias independientes: la segunda es un requisito de la primera.

// el estado local ahora cambia por dos causas y ambas propagan igual
// 1. el usuario escribe aqui
// 2. la sincronizacion trae un cambio de otro dispositivo o de otra persona
efecto(() => render(pendientes))   // no distingue el origen, y eso es la gracia

Que la vista no distinga el origen es elegante, pero la interfaz sí debe distinguirlo en algún sitio. Un cambio que aparece bajo el cursor del usuario sin que él lo haya provocado es desconcertante si no se explica, y ese es el motivo por el que los productos colaborativos maduros invierten tanto en presencia, en atribución y en avisos discretos. La transparencia técnica no exime de la honestidad en la interfaz.

Hay además un modo de fallo nuevo que la arquitectura clásica no tenía. Con una réplica local autoritativa, un error de escritura no produce una petición fallida que se reintenta: produce un dato mal que ya está en el dispositivo del usuario y que la sincronización va a propagar diligentemente a todos los demás. La validación deja de ser un guardián en la puerta del servidor y tiene que estar también en el momento de la escritura local.

La frontera que se difumina y la que no

Cuando la lectura es local y la escritura converge, la pregunta de si un dato es de cliente o de servidor pierde sentido. El mismo registro está en los dos sitios, ninguna copia es más real que la otra y la diferencia entre pedir y sincronizar se vuelve una cuestión de dirección y de momento. Esa es la disolución real: el eje que estructuró la arquitectura del frontend durante veinte años se revela como un artefacto de la red.

flowchart LR
subgraph antes
  C1[cliente sin datos] --> A1[capa de API]
  A1 --> S1[servidor con la verdad]
end
subgraph ahora
  C2[replica local autoritativa] <--> M[motor de sincronizacion]
  M <--> S2[replica de servidor]
end
style S1 fill:#f9e2af,color:#11111b
style C2 fill:#a6e3a1,color:#11111b
style S2 fill:#a6e3a1,color:#11111b

Fíjate en que el cambio de dibujo no es cosmético. En el esquema clásico hay una flecha que va y vuelve, y todo el diseño gira alrededor de esa ida y vuelta: cuánto tarda, qué se muestra mientras tanto, qué pasa si falla. En el esquema con motor de sincronización hay dos réplicas del mismo rango y un proceso que las hace converger, y la pregunta central deja de ser cuánto tarda la ida y vuelta para pasar a ser qué subconjunto vive en cada lado.

Pero hay una parte de esa frontera que no puede difuminarse, y confundirla con la otra es el error de diseño más grave de esta transición. El servidor deja de ser el dueño del dato y sigue siendo el único árbitro de la confianza. Todo lo que requiere una autoridad que el usuario no controle —quién puede ver qué, qué escrituras son legítimas, qué numeración es única, qué cobro se ejecuta una sola vez— permanece del lado del servidor por razones que ninguna mejora de sincronización va a cambiar.

Los tres frenos que siguen no son objeciones filosóficas al modelo, sino los tres sitios concretos donde hoy se atasca un equipo que lo intenta. Conviene conocerlos antes de empezar, porque los tres se manifiestan tarde, cuando ya hay usuarios reales con datos reales, y ninguno se arregla cambiando de motor.

Ahí está el freno número uno de la adopción por defecto. Autorizar es fácil cuando toda lectura pasa por un servidor que puede decir que no; es difícil cuando el cliente ya tiene los datos encima y puede estar semanas sin conectarse. El permiso deja de ser una comprobación en el momento de leer y se convierte en un criterio de pertenencia sobre lo que se sincroniza, más una validación de las escrituras cuando por fin llegan. Y revocar un acceso ya no borra un dato: pide un dato de vuelta, que es otro problema.

Esa asimetría produce una regla de diseño que conviene grabarse: el dato puede acercarse todo lo que quieras, pero la decisión de confianza no viaja con él. Puedes replicar una tabla entera en el portátil de un usuario y seguir necesitando que el servidor firme, numere o autorice ciertas operaciones. Confundir la copia con la autoridad es la forma moderna de confiar en el cliente, un error tan viejo como la web.

El segundo freno es la evolución del esquema, y es el que más equipos subestima porque no duele el primer año. En el mundo con servidor autoritativo, una migración es un evento con un antes y un después. Con miles de réplicas ejecutando versiones distintas de la aplicación durante meses, una migración es un periodo largo en el que dos formatos coexisten y tienen que fusionarse entre sí sin perder información. Eso empuja el diseño hacia esquemas aditivos, campos opcionales y lectores tolerantes, una disciplina que el frontend nunca tuvo que aprender y que los sistemas distribuidos conocen bien.

Renombrar un campo, que en el modelo clásico es una migración de una tarde, se convierte aquí en una operación en tres fases repartidas a lo largo de meses: escribir en los dos nombres, esperar a que la práctica totalidad de las réplicas se actualice, y solo entonces retirar el viejo. Es exactamente el protocolo que se usa para cambiar el formato de mensajes entre servicios, trasladado al frontend.

Ese cambio tiene una consecuencia práctica incómoda: la autorización pasa de ser código a ser también configuración de la capa de sincronización, y las dos tienen que decir lo mismo. Una regla que existe en el servidor pero no en la declaración de replicación produce datos en dispositivos donde no debían estar; una que existe en la declaración pero no en la validación de escrituras produce un cliente que puede escribir lo que no puede leer. Mantener ambas alineadas es un problema real de mantenimiento.

El tercero es el tamaño. Los metadatos de la convergencia y la historia de ediciones crecen con el uso, y sin una recolección segura de lo antiguo el dispositivo termina cargando años de pasado para pintar el presente. Es un problema activo y en camino de resolverse, pero hoy es la razón práctica por la que muchos productos declaran subconjuntos pequeños y aceptan que ciertas consultas sigan yendo al servidor.

Los tres frenos comparten una propiedad que conviene notar: ninguno es un problema de reactividad ni de estado de cliente. Son problemas de sistemas distribuidos —control de acceso con réplicas desconectadas, compatibilidad de formatos entre versiones concurrentes, compactación de historia— y llevan décadas estudiados en un campo que el frontend apenas ha mirado. Que la solución venga de ahí, y no de una librería de interfaz, es la mejor pista sobre qué conviene leer para adelantarse.

Mientras tanto, la adopción realista no es un salto sino una franja. Un producto puede empezar declarando local solo el subconjunto que el usuario edita y consulta continuamente, dejando el resto en el modelo clásico, y ampliar esa frontera a medida que gana confianza. Esa convivencia es el estado normal de las cosas durante bastantes años, y saber trazar la línea entre ambos mundos es hoy más valioso que dominar cualquiera de los dos por separado.

⚠️
Por defecto no significa para todo

La palabra por defecto describe hacia dónde se inclina la decisión cuando no hay razones en contra, no una obligación universal. Hay dominios donde la copia local autoritativa es directamente inaceptable: cuando el dato es enorme frente al dispositivo, cuando la verdad debe ser única e inmediata como en inventario o en reservas, cuando el cálculo es demasiado caro para el cliente, o cuando la sensibilidad del dato hace que tenerlo en reposo en un portátil sea el riesgo principal. En esos casos la arquitectura clásica no es deuda técnica, es la respuesta correcta. Lo que sí cambia es la carga de la prueba: hasta ahora había que justificar por qué llevarse los datos al cliente, y a partir de ahora habrá que justificar por qué dejarlos lejos.

La mitad de tu arquitectura no es arquitectura: es compensación de latencia

Este es el corte que conviene llevarse de todo el nivel. Coge tu aplicación y separa cada pieza de su capa de estado en dos montones según una única pregunta: ¿existiría esta pieza si el dato estuviese en memoria, aquí, ahora mismo? La cache de servidor no existiría. El estado de carga no existiría. El espinner, el esqueleto, el reintento con espera creciente, la actualización optimista y su rollback, la invalidación por etiquetas, el deduplicado de peticiones en vuelo, la revalidación al recuperar el foco: nada de eso existiría. Todo ese montón, que probablemente es la mitad del código de estado que has escrito en tu carrera y casi todo el contenido de las ofertas de trabajo, no es conocimiento sobre cómo gobernar datos. Es una industria entera de compensación de latencia, construida con enorme talento sobre un supuesto que fue cierto durante veinte años y está dejando de serlo. En el otro montón queda lo que sobreviviría intacto: la fuente única de verdad, la derivación en vez de la duplicación, la propagación fina de los cambios, la disciplina sobre quién puede mutar y bajo qué reglas, el modelado que hace irrepresentable lo inválido, y la frontera de confianza que nunca fue una cuestión de distancia sino de autoridad. Ese segundo montón es el que estudia este track, y no es casualidad que sea el que no se mueve. Aprende a hacer ese corte cada vez que evalúes una herramienta nueva, porque te dice al instante si te está vendiendo una solución a un problema esencial o una compensación más elegante para un problema que el hardware y la infraestructura están a punto de retirar. Las compensaciones caducan cuando cambia el supuesto que las hizo necesarias; los invariantes del estado no caducan nunca.

⚔️ Separa dominio de distancia en tu propia aplicación
  1. Lista todas las piezas de tu capa de estado y clasifícalas en dos montones: las que existirían con el dato en memoria y las que solo existen porque está lejos.
  2. Toma una pantalla completa y escribe la declaración de replicación parcial que necesitaría: qué filas, con qué condición y para qué usuarios.
  3. Reformula tres reglas de autorización actuales como criterios de pertenencia sobre lo que se sincroniza más una validación de escritura en el servidor.
  4. Diseña un cambio de esquema tuyo de forma aditiva y describe cómo se comportaría una réplica con la versión antigua durante tres meses.
  5. Estima el tamaño real del subconjunto que un usuario típico llevaría encima y decide qué consultas dejarías fuera y por qué.
  6. Elige una parte de tu producto donde local-first sería un error y defiende la elección con un argumento de autoridad, tamaño o inmediatez, no de costumbre.