El servidor y el cliente
Thiago Granata
Se formó mucho revuelo con la salida de Next 14 y las directivas use client
y use server
. Esto trajo muchas dudas cómo:
- ¿Qué es el servidor?
- ¿Qué son los Server Components? ¿Es lo mismo que SSR?
- ¿Qué hace exactamente
use server
? - ¿Y
use client
para qué sirve?
Y si bien no estoy tan integrado dentro del ecosistema de React y Next, toda esta confusión hizo que este tema comience a interesarme más.
Para responder todas estas preguntas, me parece interesante remontarnos a lo más fundamental de todo.
¿Por qué el revuelo?
Pero antes que nada, le quiero comentar un poco sobre mi experiencia (que a su vez, puede explicar la razón del revuelo): Para los desarrolladores que incursionamos en el desarrollo web en los últimos años es normal tener el modelo mental del Client-side Rendering como regla. Esto debido a que es la técnica que se utilizaba mayormente en los frameworks como React ó Angular.
💡
Hoy en día, la mayoría de frameworks/bibliotecas tienen una forma nativa de implementar Server-side Rendering. Los componentes de React, por ejemplo, primero se renderizan en el servidor.
En mi caso personal, mi primer experiencia en el desarrollo web fue con Angular, así que fue normal para mí suponer que así funcionaban las páginas normalmente.
No fue hasta que me encontré por mi cuenta con Next que empecé a descubrir e interesarme más por las distintas técnicas que existen para mostrar contenido en la web.
Ahora sí, empecemos por lo más fundamental de todo.
¿Cómo funciona la web?
Si lo pensamos a grandes rasgos parece algo simple:
- El usuario realiza una petición en su navegador (por ejemplo: busca tmgranata.com)
- El protocolo DNS se encarga de asociar el dominio con la IP del sitio
- Se envia una petición HTTP a dicha IP mediante TCP
- El servidor procesa la solicitud y responde con el contenido solicitado
Como habia dicho, esto es esencialmente simple, podriamos decir que son dos computadoras comunicandose mediante protocolos preestablecidos. Pero, ¿de qué nos sirve conocer esto?
Imaginemos que al realizar esa petición a tmgranata.com el navegador responde con un documento HTML estático, sin JavaScript ni frameworks en el medio. Básicamente nos estaría devolviendo el HTML para que nuestro navegador se encargue de renderizarlo.
Sin embargo esto es completamente estático, y el usuario no tiene ningún tipo de interacción con el sitio. ¿Qué pasa si queremos sumarle algo de interactividad a la página? Tendriamos que sumarle JavaScript. Lo que significa responder con un archivo más (script.js
)
Pero todavia Javascript no se involucra en el proceso de renderizado de nuestra web, es un archivo que “descargamos” al recibir la respuesta del servidor y vinculamos a nuestro HTML para que el usuario pueda realizar ciertas interacciones en nuestra página.
Técnicas de renderizado
Existen principalmente 3 técnicas para renderizar contenido en la web. Para entenderlas, consideremos el mismo ejemplo:
Vamos a suponer que el cliente (nosotros) realiza una petición a https://tmgranata.com/blog/ejemplo
y a partir de ahí analicemos estas 3 técnicas
Static Site Generation
Con SSG generamos todas las páginas de nuestra aplicación en build-time. Esto quiere decir que generamos todos los documentos HTML estáticos para renderizarlos según la página que se nos solicite.
Entonces lo que va a pasar cuando el usuario solicite la página (/blog/ejemplo
) es que el servidor va a ir a buscar
ese recurso y le va a servir al cliente el archivo HTML previamente generado.
Esto es algo ideal para el ejemplo que estamos dando, ya que el contenido de un artículo no cambia con frecuencia, y si lo hace basta con generar el build de la aplicación nuevamente para mostrar el contenido actualizado. Además, está técnica es muy amigable con el SEO y los tiempos de carga son bastante rápidos.
También tiene sus puntos negativos, por ejemplo: si tenemos millones de articulos y necesitamos actualizarlos, tendriamos que reconstruir la aplicación nuevamente. El build de una aplicación de ese tamaño no solo puede llevar mucho tiempo, sino que también puede tener repercusiones en el SEO al actualizar el sitemap. Esto podría hacer que Google utilice su Crawl Budget revisando contenido que ya indexó, pasando por alto nuestro nuevo contenido.
Server-side Rendering
En el SSR el HTML se genera del lado del servidor en cada request. Esto significa que al recibir la petición del cliente, el servidor va a solicitar la información (ya sea llamando a una API u obteniendo la información desde una base de datos) y generará el HTML correspondiente que luego servirá al cliente.
Esta técnica también es amigable con el SEO y mejora los tiempos de carga en comparación al CSR al mostrar contenido de manera inmediata. Como contra, nuestro servidor debe ocuparse de generar el contenido, lo que puede requerir más recursos. Y si nuestro servidor está sobrecargado, se pueden experimentar tiempos de carga más lentos.
Client-side Rendering
Con el CSR por su parte, el cliente es quien se encarga de generar el HTML, usualmente el servidor nos devuelve un archivo JavaScript y a partir de él se renderiza nuestra aplicación. Por eso es que es normal que lo primero que veamos al navegar un sitio web que utilice CSR sea una página en blanco.
Si observamos el HTML que recibimos en la sección ‘Network’ al cargar el sitio, usualmente nos encontramos con algo como esto:
<!doctype html>
<html lang="en">
<head>
<title>My Blog</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Como punto positivo, una vez que se carga el JavaScript, el sitio funciona muy rápido y el usuario puede interactúar instantaneamente con él. Por su parte, no es muy amigable con el SEO y pueden sufrir de tiempos de carga más lentos.
Combinación de técnicas
Es importante recalcar que todas las técnicas tienen pros y contras y elegir utilizar una u otra es una decisión que tomar en relación al problema que hay que resolver. De la misma forma podemos utilizar más de una técnica a la vez en nuestro sitio web. Esto es normal verlo en frameworks como Next o Astro.
Por ejemplo: Con el Pages Directory, Next.js nos brinda del ISR (Incremental Static Regeneration) qué significa que vamos a generar las páginas en build-time, como con SSG, pero cada un tiempo que nosotros establezcamos el contenido de esa página se va a invalidar y, cuando llegue una nueva petición, vamos a generarlo nuevamente. También podemos revalidar nuestro contenido on-demand, lo que es ideal cuando cambia nuestro contenido o metadata.
💡
Desde la salida del App Directory en Next, el ISR ya no se utiliza. Si les interesa conocer más acerca de revalidar data en nuevas versiones de Next, pueden visitar la documentación oficial
Astro utiliza una arquitectura llamada Islands para mezclar distintas técnicas. Donde las páginas HTML se renderizan en el servidor y luego las partes dinámicas de nuestro sitio web se hidratan en el cliente de manera individual (y pueden hacerlo en paralelo).
Esto permite que Astro funcione bastante rápido y obtenga lo mejor de los dos mundos, porque apenas ingresemos a una página vamos a cargar rápidamente el HTML estático y despues se van a hidratar todas las partes dinámicas de nuestro sitio pero el usuario no va a ver una pantalla en blanco durante la carga.
Tambien nos permite controlar como se van a hidratar los componentes en nuestro sitio mediante directivas, pero como esto no es un articulo dedicado a Astro, lo voy a dejar para otro momento.
Hydration
A esta altura del artículo hice mención varias veces a “hidratar” o “hydration”. ¿Pero a qué le llamamos ‘hidratar’?.
La hidratación es el proceso en el cual nuestro sitio web descarga el JavaScript y se dota de interactividad. En un paso a paso se vería algo así:
- El usuario realiza una petición en su navegador (por ejemplo: busca tmgranata.com)
- El protocolo DNS se encarga de asociar el dominio con la IP del sitio
- Se envia una petición HTTP a dicha IP mediante TCP
- El servidor procesa la solicitud y responde con un HTML
- Se renderiza nuestro HTML, pero aún no podemos interactuar con el contenido.
- Se descarga el JavaScript, que luego ‘hidrata’ el contenido estático previamente renderizado para agregarle interactividad.
A partir del punto número 6 es cuando el usuario puede interactuar con el sitio web. Esta técnica es comunmente utilizada en páginas que utilicen SSG o SSR.
¿Qué es el servidor?
Con todo el contexto previo, podemos responder la primer pregunta que planteé al principio del artículo ¿Qué es el servidor?
Nuestras aplicaciones pueden renderizarse en dos lugares: el cliente y el servidor.
- El cliente, entonces, es el navegador que está ejecutando la aplicación y que realiza la petición a nuestro server.
- El servidor, que es la computadora donde se aloja nuestra aplicación, quien recibe y responde las peticiones.
De esta forma, al escribir código para nuestras webs, podemos notar que no todo el código del cliente es válido en el server y viceversa.
Esto es lógico, porque cuando el cliente ejecuta el código lo hace en el contexto de un navegador, por lo tanto tiene acceso a ciertas
caracteristicas como: document
, window
entre otras APIs que pertenecen al navegador.
El server, por el contrario, no puede acceder a esas APIs del navegador porque se está ejecutando en un contexto de Node. A su vez, en Node tenemos acceso a APIs que en el navegador no, como a la API de fs
Por ejemplo: es por esta distinción, y por razones de seguridad, que podemos ver como se trabajan las variables de entorno en distintos
frameworks fullstack (process.env
no está disponible en el navegador).
En Remix, no podemos acceder a las variables de entorno desde el cliente, excepto que las “inyectemos” al cliente mediante el loader.
// El loader se ejecuta del lado del servidor, por eso tenemos acceso al process.env
export async function loader() {
return json({
ENV: {
PUBLIC_API_KEY: process.env.PUBLIC_API_KEY,
},
});
}
export function Root(){
// Recibimos las variables de entorno desde el loader
const data = useLoader<typeof loader>();
<html>
<head>
// (...)
</head>
<body>
<script
// Inyectamos las variables de entorno al objeto window
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(
data.ENV
)}`,
}}
/>
</body>
</html>
}
Por su parte, Next tambien tiene su propia forma de manipular variables de entorno en el servidor/cliente.
Las variables de entorno, por defecto, solo las podemos acceder en un contexto de Node. Para que esten disponibles dentro del navegador
tenemos que prefijarlas de NEXT_PUBLIC_
SUPER_SECRET_API_KEY=1234567 // disponible solo en el contexto de Node
NEXT_PUBLIC_API_KEY=1234567 // disponible en el navegador
Lo que va a hacer NEXT_PUBLIC_
es indicarle a Next que debe reemplazar esas referencias por el valor que especificamos en nuestro .env
al realizar el build.
Es por esto que las variables públicas van a “congelar” el valor que adquirieron al hacer el build y no van a responder a cambios.
💡
No es imposible tener variables públicas que se modifiquen en tiempo de ejecución en Next, para eso debemos utilizar las Runtime Environment Variables
Directivas ‘use server’ y ‘use client’
Entendiendo que es el servidor y que es el cliente, estamos listos para poder responder que función cumplen las directivas ‘use server’ y ‘use client’. Si bien mi objetivo con este artículo es que sea lo más agnóstico a frameworks posible, creo que todos nos podemos beneficiar de entender estos conceptos.
Una página en Next, por defecto, utiliza Server Components, es por esto que no tenemos acceso a funcionalidades como los hooks o el contexto desde ellos.
💡
Los Server Components NO son una caracteristica exclusiva de Next, sino de React (React Server Components).
Server Components
Antes de continuar con las directivas, me parece importante remarcar que Server Components (RSC) no es lo mismo que SSR. Desde una demo del equipo de React acerca de los Server Components podemos observar esta distinción:
Los Server Components son una tecnología diferente (pero complementaria) al Server-side Rendering (SSR). Los Server Components te permiten ejecutar algunos de tus componentes exclusivamente en el servidor. Por otro lado, SSR, te permite generar el HTML antes de cargar cualquier archivo JavaScript (…)
Entonces, como comentamos previamente, SSR es una estrategia de renderizado que nos ayuda en disminuir el tiempo que demora el contenido en ‘pintarse’ en la pantalla del usuario (FCP) al renderizar primeramente el HTML estático de nuestra aplicación permitiendo que el usuario vea algo rápidamente y luego obtener interactividad mediante el proceso de hydration.
Los Server Components son una manera de ejecutar un componente exclusivamente del lado del servidor para enviarlo al cliente, sin re-renders ni cambios de estado.
Estos componentes, por lo tanto, no pueden tener eventos que ocurran en el cliente (como onClick
). Los Server Components no se envían al cliente como HTML, se envian en
un formato streamable similar a JSON para que React lo pueda renderizar.
Sumado a eso, los Server Components tienen otros beneficios, quizá uno de los más destacables es que no son incluidos en el bundle de nuestra aplicación que se envia al cliente. (un bundle más ligero = menor tiempo de carga)
Los Clients Components, por su parte, son los componentes tradicionales de React, donde tenemos acceso a todas las funcionalidades usuales de la biblioteca (hooks, context, etc)
Estos componentes, SÍ son incluidos en el bundle de la aplicación y, por lo tanto, enviados y descargados por el cliente.
💡
Los Clients Components se renderizan tanto en el servidor como en el cliente.
Para quien esté interesado en esto, particularmente desde la perspectiva de React, hay un artículo muy interesante escrito por Dan Abramov: “The Two Reacts”
Ahora sí, podemos continuar con las directivas. Como decia, todos los componentes, por defecto son Server Components y para que se comporten como componentes
tradicionales tenemos que usar la directiva use client
'use client' // sin decirle a React que es un Client Component, no podriamos utilizar el useState
import React, { useState } from 'react'
export const Counter = () => {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log("Los efectos tambien están disponibles")
}, [counter])
return (
<p>Valor: {counter}</p>
<button onClick={() => setCounter(prev => prev + 1)}> +1 </button>
<button onClick={() => setCounter(prev => prev - 1)}> -1 </button>
)
}
Podriamos pensar entonces, que utilizar la directiva use server
especificaria que queremos definir un Server Component.
Pero esto no es así, la directiva use server
se utiliza para declarar Server Actions, que comentaremos más adelante.
Como los Server Components son la norma, basta con no utilizar ninguna directiva especifica para declararlos.
Server Actions
Las Server Actions son funciones que podemos llamar desde el cliente pero que se van a ejecutar en el servidor (en el contexto de Node). El caso de uso más común de una Server Action (pero no estan limitadas a él) es cuando estamos manejando formularios.
// create-product.js
'use server'
async function createProduct(formData) { // -> esta función se está ejecutando del lado del servidor
const name = await formData.get('productName');
const insertedProduct = fakeDb.products.insert({
name
});
return insertedProduct.id ? 'success' : 'failed';
}
// product.jsx
export default function ProductComponent({}){
return (
<form action={createProduct}>
<input type="text" placeholder="Nombre del producto..." name="productName"/>
<button type="submit">Crear</button>
</form>
)
}
Las Server Actions solo pueden ser declaradas en archivos server-side y utilizadas tanto del lado del cliente como del servidor. Hay un montón de utilidades más que acompañan las Server Actions.
💡
Más información respecto a las Server Actions en la documentación oficial
Conclusión
El tópico cliente-servidor tiene mucho de lo que hablar y lo comentado en este articulo no fue más que una pincelada de conceptos para tener un mejor punto de inicio a partir del cual comenzar a profundizar. Es por eso que algunos temas quedaron fuera de este post, pero me gustaría visitarlos más adelante.
Bibliografia
Categorias
- programacion