Inicio / Artículos

Cómo evolucionó la ingeniería frontend, de los estilos a la escalabilidad de sistemas

La ingeniería frontend evolucionó de HTML y CSS hacia componentes React, caché, herramientas para monorepos y capacidades de observabilidad. Qué cambió en cada etapa y por qué ocurrió.

Editor: REACTAPP.TOP10 min de lectura2240 palabras

Fuentes: I Thought Frontend Was Colors and Fonts. Then I Shipped to a Million Users.

Las herramientas han evolucionado, pero nuestro modo de pensar ha evolucionado aún más.

Si le preguntas a alguien qué significa el desarrollo frontend, por lo general escucharás la misma respuesta: HTML, CSS, JavaScript. Colores, fuentes, imágenes, diseño.

Ese es un buen punto de partida. Simplemente no es donde termina la historia.

Lo que comenzó como la creación de páginas web se ha convertido en el diseño de sistemas completos que deben mantenerse rápidos, precisos y trazables mientras miles de personas interactúan con ellos al mismo tiempo.

Las herramientas evolucionaron siguiendo este camino. Pero lo que cambió aún más fue el conjunto de preguntas que aprendimos a hacernos.

Aquí está el recorrido completo.

1. Comenzó con HTML, CSS y JavaScript

La web inicial mantenía las cosas simples.

HTML se encargaba de la estructura. CSS se ocupaba de la presentación. JavaScript añadía un poco de interactividad encima.

La misión se resumía en una sola oración:

Crear una página que se vea bien y funcione correctamente.

La mayoría de las páginas eran estáticas. Los servidores entregaban HTML completo, los navegadores lo renderizaban y el propio navegador casi no contenía lógica.

Con el tiempo, las páginas asumieron más responsabilidades. Los formularios comenzaron a validar la información mientras los usuarios escribían. El contenido se actualizaba sin necesidad de recargar toda la página.

JavaScript siguió asumiendo más tareas y nunca devolvió ninguna de ellas.

  1. De páginas a aplicaciones

Los sitios web se convirtieron en aplicaciones. Eso significó interfaces dinámicas, llamadas a APIs, formularios con múltiples pasos, flujos de autenticación y datos almacenados en el navegador entre una interacción y otra.

Esto reveló un problema que las técnicas antiguas no estaban diseñadas para resolver.

Cuando manipulas el DOM directamente, la pantalla visible se convierte en tu única fuente de verdad. Un mismo dato puede existir en una variable, dentro de un nodo del DOM y en una respuesta del servidor. Nada garantiza que estos tres elementos estén alineados.

Si actualizas uno y olvidas los demás, terminarás con un error que solo aparece tras una secuencia específica de clics.

Lo que se necesitaba era una forma de declarar cómo debería verse la interfaz y delegar la tarea de determinar las operaciones reales en el DOM a otro elemento.

3. React: pensar en componentes

El verdadero avance de React no fue su sintaxis, sino el modelo mental que la sustenta.

UI = f(state)

Describes la pantalla para un estado determinado, y el framework calcula el conjunto más pequeño de actualizaciones en el DOM necesarias para lograrlo.

En lugar de escribir element.appendChild(...), se escribe lo que debe ser cierto.

La unidad básica de trabajo pasó a ser el componente en lugar de toda la página. Piense en un botón individual, una tarjeta que muestra un producto, un diálogo emergente o un paso en el proceso de pago. Cada uno de estos tiene su propio marcado, su propio estilo y su propio comportamiento.

Aquí está lo que aporta este enfoque:

Reutilización. Construya un componente una vez y úselo en docenas de lugares.

Razonamiento local. Puede comprender un componente sin tener que leer toda la aplicación.

Practicabilidad de pruebas. Proporciónele parámetros, verifique la salida; no se necesita navegador.

Composibilidad. Los componentes pequeños se combinan para formar otros más grandes.

La pregunta central pasó de "¿Cómo creo esta página?" a "¿Qué componentes forman esta aplicación?"

4. Next.js: de la interfaz de usuario a la arquitectura

React, utilizado exclusivamente en el lado del cliente, conllevaba ciertos costos.

El navegador primero descarga una estructura HTML vacía. Luego carga un paquete de JavaScript bastante grande. Después ejecuta ese paquete y finalmente obtiene los datos reales. Solo después de todo esto el usuario ve algún contenido.

Eso está bien en una computadora portátil potente. Pero en un teléfono de gama media con señal débil, significa tener que mirar una pantalla en blanco durante varios segundos. Además, los rastreadores de búsqueda podrían terminar indexando nada útil en absoluto.

Frameworks como Next.js permitieron a los desarrolladores controlar dónde y cuándo se genera realmente el HTML.

SSG. La renderización se realiza en el momento de la compilación, y un archivo estático se sirve desde un CDN. Es la opción más rápida, aunque el contenido permanece inalterado hasta la siguiente compilación.

ISR. El contenido sigue siendo estático, pero se regenera en segundo plano después de un período de tiempo definido. Se obtiene velocidad a nivel estático con contenido que se mantiene razonablemente actualizado.

SSR. La renderización se realiza en el servidor para cada solicitud. El contenido siempre está actualizado, pero el servidor debe realizar trabajo adicional en cada ocasión.

CSR. La renderización se realiza íntegramente en el navegador. Esto es adecuado para pantallas protegidas por inicio de sesión, donde la indexación no es importante.

La idea clave: se elige según cada ruta, no para toda la aplicación al mismo tiempo.

Una página de marketing se beneficia del SSG. Una página de producto se beneficia del ISR. Un panel de control se beneficia del CSR.

Aquí es donde también entra en juego la hidratación. El servidor envía HTML y luego el cliente vuelve a ejecutar el árbol de componentes para conectar los manejadores de eventos. Visualmente, la página parece lista antes de que realmente sea interactiva.

Los Componentes del Servidor de React y la transmisión en flujo existen principalmente para cerrar esa brecha. El HTML se envía tan pronto como está disponible, y se envía menos JavaScript para las partes de la página que nunca estuvieron destinadas a ser interactivas.

En esta etapa, el trabajo frontend deja de ser simplemente “la capa de interfaz”. Las decisiones sobre la estrategia de renderizado, el enrutamiento y el caché se convierten en decisiones arquitectónicas, y son los ingenieros frontend quienes las toman.

5. Luego llegó la escalabilidad

Desarrollar una aplicación es un desafío. Desarrollar una que atienda a millones de usuarios es un desafío completamente diferente.

Las preguntas que se plantean comienzan a cambiar:

¿Qué debería ocurrir cuando una API de nivel inferior se ralentiza en lugar de fallar de manera limpia?

¿Cómo logran seis equipos separados entregar código al mismo repositorio sin interferirse entre sí?

¿Cómo se implementa un cambio solo en el 1% de los usuarios y se retira en menos de un minuto si surge algún problema?

¿Cómo se comporta la aplicación en un teléfono Android de tres años en un lugar donde nunca se ha probado?

¿Cuál de las implementaciones de ayer es la responsable del aumento repentino de errores de hoy?

Estas son preguntas a nivel de sistema. Simplemente surgen dentro del navegador.

6. La velocidad no es un detalle opcional, es una expectativa básica

Una interfaz elegante que tarda seis segundos en renderizarse es como si estuviera rota. Los usuarios perciben la demora antes incluso de poder identificar qué está mal.

La disciplina de rendimiento actual se basa en la medición en lugar de la intuición. Core Web Vitals brindan a los equipos un lenguaje común para hablar sobre la velocidad.

LCP (Largest Contentful Paint) marca el momento en que el contenido principal se vuelve visible. El objetivo es mantenerlo por debajo de 2,5 segundos.

INP (Interaction to Next Paint) mide cuán rápido reacciona la interfaz cuando alguien toca o hace clic. El objetivo es mantenerlo por debajo de 200 milisegundos.

CLS (Cumulative Layout Shift) mide cuánto se mueven los elementos mientras la página se estabiliza. El objetivo es obtener una puntuación inferior a 0,1.

Cada técnica de optimización se relaciona con una de estas métricas.

Dividir el código y cargarlo de forma perezosa. Envíe solo el JavaScript que realmente necesita la ruta actual, no todo el paquete de la aplicación. Esto mejora tanto LCP como INP.

Ajuste de imágenes. Utilice formatos modernos, defina el tamaño correctamente y siempre indique la anchura y la altura de antemano para que nada se mueva bruscamente. Esto ayuda a mejorar los indicadores LCP y CLS.

Gestión intencionada de fuentes. Cargue las fuentes por adelantado, aplique font-display: swap y reduzca el conjunto de caracteres al que realmente se utiliza. Esto aborda el problema del CLS.

Distribución a través de un CDN. Servir los datos desde un servidor geográficamente cercano al usuario beneficia prácticamente a todos los indicadores al mismo tiempo.

Alejar las tareas pesadas del hilo principal. El navegador cuenta con un único hilo que se encarga tanto de la renderización como de sus scripts. Una tarea que dura 300 milisegundos congela cada clic durante ese mismo tiempo. De aquí provienen las ventajas del INP.

El principio subyacente, expresado de forma sencilla:

Ofrezca la mejor experiencia utilizando la menor cantidad posible de recursos.

Una advertencia: ajuste las configuraciones basándose en lo que realmente haya medido, no en suposiciones. Las cifras obtenidas desde la computadora portátil de un desarrollador en un entorno de laboratorio suelen diferir de lo que experimentan los usuarios reales en el terreno, y son los datos del campo los que reflejan la realidad.

7. Caché: evite volver a calcular lo que ya ha resuelto

A medida que aumenta el tráfico, generar la misma respuesta una y otra vez se vuelve un desperdicio. La caché existe para que no tenga que pagar ese costo dos veces por el mismo trabajo.

Cada solicitud pasa por varias capas, y cada una representa una oportunidad para acortar el proceso.

Browser → CDN → Application / ISR → Redis → Database

Cuanto más cerca esté del usuario el lugar desde donde se sirve la respuesta, más económico y rápido será.

No obstante, cuanto más cerca esté algo del usuario en la caché, más difícil resulta volver atrás si está incorrecto.

Esa tensión define toda esta práctica.

¿Qué debe estar en la caché? Datos estables, compartidos entre usuarios y cuya generación es costosa. Nunca nada específico del usuario, a menos que la clave de caché tenga en cuenta al usuario individual.

¿Cuánto tiempo debe permanecer? Períodos de vencimiento cortos son más seguros, pero brindan menos beneficios. Períodos largos son rápidos, pero conllevan más riesgos.

¿Cómo se vacía? Las opciones incluyen vencimiento basado en el tiempo (TTL), eliminaciones impulsadas por eventos (activadas al publicar) o nombres de archivo hasheados según su contenido, de modo que la invalidación ni siquiera es necesaria.

¿Qué sucede cuando falla una búsqueda? Si una clave ampliamente utilizada expira y un gran número de solicitudes llegan al backend simultáneamente, se produce una avalancha. Para evitarlo, utilice un mecanismo de bloqueo o sirva la versión obsoleta mientras una sola solicitud se encarga de actualizarla.

El patrón que vale la pena memorizar es stale-while-revalidate. Devuelva de inmediato la versión almacenada en caché, actualícela silenciosamente en segundo plano, y el siguiente visitante recibirá el contenido actualizado. Los usuarios experimentan mayor velocidad, y usted logra la corrección con un breve retraso.

Preste atención al costo real de cometer un error aquí. Un caché dañado no solo ralentiza las operaciones, sino que también puede mostrar precios incorrectos, nombres erróneos o incluso datos de otra persona.

El caché parece una técnica de rendimiento, pero en realidad funciona como una decisión relacionada con la corrección.

8. Estructuración para el crecimiento

Cuando varios equipos trabajan dentro de una misma base de código, la forma en que está organizada se vuelve tan importante como la velocidad con la que funciona.

Límites entre módulos. Decida qué se expone públicamente, qué permanece interno y qué dependencias están permitidas.

Contratos para APIs. Deben estar tipados y versionados, de modo que un cambio en el backend afecte únicamente al proceso de compilación y no al sitio en línea.

Dónde reside el estado. El estado del servidor, el estado de la URL y el estado local del cliente son categorías fundamentalmente distintas. La mayoría de los errores relacionados con el estado surgen al tratar una categoría como si fuera otra.

Aislamiento de fallos. Los límites de errores garantizan que un widget defectuoso no arruine toda la página.

Lanzamiento gradual de cambios. Banderas de funcionalidad, despliegues canario y la posibilidad de revertir instantáneamente.

La mentalidad cambia de simplemente hacer que una función funcione a asegurarse de que esa función no dificulte cada cambio futuro.

9. Código compartido en un monorepo

Una vez que varias aplicaciones dependen del mismo encabezado, de los mismos tokens de diseño y de las mismas herramientas de autenticación, copiar y pegar código deja de ser sostenible.

Un monorepo permite a los equipos compartir componentes de interfaz, sistemas de diseño, funciones utilitarias, configuraciones y definiciones de tipos: una única fuente de verdad canónica, con los cambios aplicándose de forma atómica en todos los paquetes.

No obstante, compartir conlleva un costo. Cada paquete compartido se convierte en una dependencia, y cada dependencia es, en efecto, una cola: la actualización de un equipo puede retrasar el lanzamiento de otro.

El objetivo real es el reuso sin acoplamiento estricto.

Una heurística útil: comparta elementos que rara vez cambian y significan lo mismo sin importar dónde se utilicen, como tokens de diseño, un componente Botón o definiciones de tipo.

Evite compartir cualquier cosa que incluya la lógica empresarial específica de un equipo. Duplicar código suele ser más económico que recurrir a una abstracción compartida incorrecta.

Los microfrontends llevan esta idea aún más lejos, permitiendo que los equipos publiquen de forma independiente a costa de una mayor complejidad en tiempo de ejecución y dependencias duplicadas. Este equilibrio puede tener sentido en una gran organización con límites claros entre equipos. Fuera de ese contexto, rara vez es rentable.

10. Monitoreo del sistema en producción

A gran escala, frases como “funciona en mi máquina” o “funcionó bien en staging” dejan de tener mucho sentido.

Lo que realmente se necesita es tener visibilidad sobre cómo se comporta la aplicación en producción, en dispositivos reales y en tiempo real.

Rastreo de errores combinado con mapas de origen subidos, de modo que el rastro de la pila apunte a su código fuente real en lugar de a código minificado y sin sentido.

Monitoreo de usuarios reales para capturar los Core Web Vitals de usuarios genuinos, desglosados por dispositivo, país y calidad de conexión.

Registro estructurado vinculado a un ID de solicitud que puede rastrearse desde el navegador hasta cada servicio backend al que accede.

Alertas relacionadas con síntomas específicos. Notifique a las personas cuando las tasas de error o la latencia superen un umbral, y no simplemente cuando el uso de CPU de un servidor aumente drásticamente.

Rastreo de lanzamientos. Etiqueta cada error con el despliegue específico que lo introdujo, de modo que responder a “¿qué cambió?” tome segundos en lugar de horas.

Las preguntas a las que esta configuración debería permitirte responder son:

¿Dónde exactamente están teniendo problemas los usuarios? ¿Qué llamada a la API está afectando negativamente la experiencia? ¿Qué ruta es lenta y para qué segmento de usuarios? ¿El último despliegue empeoró algo en comparación con antes?

El código del frontend solía ser la parte más difícil de observar en toda la estructura. Ya no tiene por qué ser así.

Lecturas relacionadas