Qué resuelven los frameworks web y cómo prescindir de ellos (Parte 1)
Publicado: 2022-03-10Recientemente me interesé mucho en comparar marcos con JavaScript estándar. Comenzó después de cierta frustración que tuve al usar React en algunos de mis proyectos independientes, y con mi reciente y más íntimo conocimiento de los estándares web como editor de especificaciones.
Estaba interesado en ver cuáles son los puntos en común y las diferencias entre los marcos , qué tiene para ofrecer la plataforma web como una alternativa más eficiente y si es suficiente. Mi objetivo no es criticar los marcos, sino comprender los costos y beneficios, determinar si existe una alternativa y ver si podemos aprender de ella, incluso si decidimos usar un marco.
En esta primera parte, profundizaré en algunas características técnicas comunes entre los marcos y cómo las implementan algunos de los diferentes marcos. También analizaré el costo de usar esos marcos.
Los marcos
Elegí cuatro marcos para analizar: React, que es el dominante en la actualidad, y tres contendientes más nuevos que afirman hacer las cosas de manera diferente a React.
- Reaccionar
“React hace que sea sencillo crear interfaces de usuario interactivas. Las vistas declarativas hacen que su código sea más predecible y más fácil de depurar”. - SolidJS
"Solid sigue la misma filosofía que React... Sin embargo, tiene una implementación completamente diferente que renuncia al uso de un DOM virtual". - Esbelto
“Svelte es un enfoque radicalmente nuevo para crear interfaces de usuario… un paso de compilación que ocurre cuando creas tu aplicación. En lugar de usar técnicas como la diferenciación virtual de DOM, Svelte escribe un código que actualiza quirúrgicamente el DOM cuando cambia el estado de su aplicación”. - Iluminado
"Sobre la base de los estándares de los componentes web, Lit agrega solo... reactividad, plantillas declarativas y un puñado de funciones bien pensadas".
Para resumir lo que dicen los marcos sobre sus diferenciadores:
- React facilita la creación de interfaces de usuario con vistas declarativas.
- SolidJS sigue la filosofía de React pero usa una técnica diferente.
- Svelte utiliza un enfoque de tiempo de compilación para las interfaces de usuario.
- Lit usa los estándares existentes, con algunas características livianas adicionales.
Lo que resuelven los marcos
Los propios marcos mencionan las palabras declarativo, reactividad y DOM virtual. Vamos a sumergirnos en lo que significan.
Programación declarativa
La programación declarativa es un paradigma en el que la lógica se define sin especificar el flujo de control. Describimos cuál debe ser el resultado, en lugar de qué pasos nos llevarían allí.
En los primeros días de los marcos declarativos, alrededor de 2010, las API DOM eran mucho más sencillas y detalladas, y escribir aplicaciones web con JavaScript imperativo requería una gran cantidad de código repetitivo. Fue entonces cuando el concepto de "modelo-vista-modelo de vista" (MVVM) se hizo predominante, con los entonces innovadores marcos de trabajo Knockout y AngularJS, que proporcionaban una capa declarativa de JavaScript que manejaba esa complejidad dentro de la biblioteca.
MVVM no es un término muy utilizado en la actualidad, y es algo así como una variación del término anterior "enlace de datos".
El enlace de datos
El enlace de datos es una forma declarativa de expresar cómo se sincronizan los datos entre un modelo y una interfaz de usuario.
Todos los marcos de interfaz de usuario populares proporcionan alguna forma de vinculación de datos y sus tutoriales comienzan con un ejemplo de vinculación de datos.
Aquí está el enlace de datos en JSX (SolidJS y React):
function HelloWorld() { const name = "Solid or React"; return ( <div>Hello {name}!</div> ) }
Enlace de datos en Lit:
class HelloWorld extends LitElement { @property() name = 'lit'; render() { return html`<p>Hello ${this.name}!</p>`; } }
Enlace de datos en Svelte:
<script> let name = 'world'; </script> <h1>Hello {name}!</h1>
Reactividad
La reactividad es una forma declarativa de expresar la propagación del cambio.
Cuando tenemos una forma de expresar de forma declarativa el enlace de datos, necesitamos una forma eficiente para que el marco propague los cambios.
El motor React compara el resultado del renderizado con el resultado anterior y aplica la diferencia al propio DOM. Esta forma de manejar la propagación de cambios se denomina DOM virtual.
En SolidJS, esto se hace de manera más explícita, con su tienda y elementos integrados. Por ejemplo, el elemento Show
realizaría un seguimiento de lo que ha cambiado internamente, en lugar del DOM virtual.
En Svelte se genera el código “reactivo”. Svelte sabe qué eventos pueden causar un cambio y genera un código sencillo que traza la línea entre el evento y el cambio de DOM.
En Lit, la reactividad se logra mediante las propiedades de los elementos, basándose esencialmente en la reactividad integrada de los elementos personalizados de HTML.
Lógica
Cuando un marco proporciona una interfaz declarativa para el enlace de datos, con su implementación de reactividad, también debe proporcionar alguna forma de expresar parte de la lógica que tradicionalmente se escribe de forma imperativa. Los bloques de construcción básicos de la lógica son "si" y "para", y todos los marcos principales proporcionan alguna expresión de estos bloques de construcción.
Condicionales
Además de vincular datos básicos como números y cadenas, cada marco proporciona una primitiva "condicional". En React, se ve así:
const [hasError, setHasError] = useState(false); return hasError ? <label>Message</label> : null; … setHasError(true);
SolidJS proporciona un componente condicional incorporado, Show
:
<Show when={state.error}> <label>Message</label> </Show>
Svelte proporciona la directiva #if
:
{#if state.error} <label>Message</label> {/if}
En Lit, usarías una operación ternaria explícita en la función de render
:
render() { return this.error ? html`<label>Message</label>`: null; }
Liza
La otra primitiva de marco común es el manejo de listas. Las listas son una parte clave de las UI (lista de contactos, notificaciones, etc.) y para funcionar de manera eficiente, deben ser reactivas, no actualizar toda la lista cuando cambia un elemento de datos.
En React, el manejo de listas se ve así:
contacts.map((contact, index) => <li key={index}> {contact.name} </li>)
React usa el atributo de key
especial para diferenciar entre los elementos de la lista y se asegura de que la lista completa no se reemplace con cada renderizado.
En SolidJS, se utilizan los elementos integrados for
e index
:
<For each={state.contacts}> {contact => <DIV>{contact.name}</DIV> } </For>
Internamente, SolidJS usa su propia tienda junto con for
e index
para decidir qué elementos actualizar cuando los elementos cambian. Es más explícito que React, lo que nos permite evitar la complejidad del DOM virtual.
Svelte usa each
directiva, que se transpila en función de sus actualizadores:
{#each contacts as contact} <div>{contact.name}</div> {/each}
Lit proporciona una función de repeat
, que funciona de manera similar al mapeo de lista basado en key
de React:
repeat(contacts, contact => contact.id, (contact, index) => html`<div>${contact.name}</div>`
Modelo de componente
Una cosa que está fuera del alcance de este artículo es el modelo de componentes en los diferentes marcos y cómo se puede tratar con elementos HTML personalizados.
Nota : este es un tema amplio y espero cubrirlo en un artículo futuro porque este sería demasiado largo. :)
El costo
Los marcos proporcionan enlace de datos declarativo, primitivas de flujo de control (condicionales y listas) y un mecanismo reactivo para propagar cambios.
También proporcionan otras cosas importantes, como una forma de reutilizar componentes, pero ese es un tema para un artículo aparte.
¿Son útiles los marcos? Si. Nos dan todas estas características convenientes. Pero, ¿es esa la pregunta correcta? El uso de un marco tiene un costo. Veamos cuáles son esos costos.
Tamaño del paquete
Cuando miro el tamaño del paquete, me gusta mirar el tamaño minificado sin Gzip. Ese es el tamaño más relevante para el costo de CPU de la ejecución de JavaScript.
- ReactDOM tiene aproximadamente 120 KB.
- SolidJS tiene unos 18 KB.
- Lit tiene unos 16 KB.
- Svelte tiene unos 2 KB, pero el tamaño del código generado varía.
Parece que los marcos de trabajo actuales están haciendo un mejor trabajo que React al mantener el tamaño del paquete pequeño. El DOM virtual requiere mucho JavaScript.
Construye
De alguna manera nos acostumbramos a “construir” nuestras aplicaciones web. Es imposible iniciar un proyecto front-end sin configurar Node.js y un paquete como Webpack, lidiar con algunos cambios de configuración recientes en el paquete de inicio de Babel-TypeScript y todo ese jazz.
Cuanto más expresivo y más pequeño sea el tamaño del paquete del marco, mayor será la carga de las herramientas de compilación y el tiempo de transpilación.
Svelte afirma que el DOM virtual es pura sobrecarga. Estoy de acuerdo, pero tal vez la "construcción" (como con Svelte y SolidJS) y los motores de plantillas personalizadas del lado del cliente (como con Lit) también son pura sobrecarga, ¿de un tipo diferente?
depuración
Con la construcción y la transpilación viene un tipo diferente de costo.
El código que vemos cuando usamos o depuramos la aplicación web es totalmente diferente de lo que escribimos. Ahora confiamos en herramientas especiales de depuración de calidad variable para aplicar ingeniería inversa a lo que sucede en el sitio web y conectarlo con errores en nuestro propio código.
En React, la pila de llamadas nunca es "tuya": React maneja la programación por ti. Esto funciona muy bien cuando no hay errores. Pero trate de identificar la causa de las re-renderizaciones de bucle infinito y se encontrará con un mundo de dolor.
En Svelte, el tamaño del paquete de la biblioteca en sí es pequeño, pero va a enviar y depurar un montón de código generado críptico que es la implementación de reactividad de Svelte, personalizado para las necesidades de su aplicación.
Con Lit, se trata menos de construir, pero para depurarlo de manera efectiva, debe comprender su motor de plantillas. Esta podría ser la razón principal por la que mi sentimiento hacia los marcos es escéptico.
Cuando busca soluciones declarativas personalizadas, termina con una depuración imperativa más dolorosa. Los ejemplos de este documento usan Typescript para la especificación de API, pero el código en sí no requiere transpilación.
Actualizaciones
En este documento, analicé cuatro marcos, pero hay más marcos de los que puedo contar (AngularJS, Ember.js y Vue.js, por nombrar algunos). ¿Puede contar con el marco, sus desarrolladores, su mentalidad compartida y su ecosistema para trabajar para usted a medida que evoluciona?
Una cosa que es más frustrante que corregir sus propios errores es tener que encontrar soluciones para los errores del marco. Y una cosa que es más frustrante que los errores del marco son los errores que ocurren cuando actualiza un marco a una nueva versión sin modificar su código.
Es cierto que este problema también existe en los navegadores, pero cuando ocurre, le sucede a todos y, en la mayoría de los casos, es inminente una corrección o una solución alternativa publicada. Además, la mayoría de los patrones de este documento se basan en API de plataformas web maduras; no siempre es necesario ir con la vanguardia.
Resumen
Nos sumergimos un poco más en la comprensión de los problemas centrales que los marcos intentan resolver y cómo los resuelven, centrándonos en el enlace de datos, la reactividad, los condicionales y las listas. También miramos el costo.
En la Parte 2, veremos cómo se pueden abordar estos problemas sin usar un marco y qué podemos aprender de él. ¡Manténganse al tanto!
Un agradecimiento especial a las siguientes personas por las revisiones técnicas: Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal y Louis Lazaris.