Estamos trabajando en un modelo de almacenamiento en caché simple y potente para Next.js. En una publicación anterior, hablamos sobre nuestra experiencia con el caché y cómo llegamos a la directiva 'use cache'
.
Esta publicación discutirá el diseño de la API y los beneficios de 'use cache'
.
¿Qué es 'use cache'
?
'use cache'
hace que tu aplicación sea más rápida almacenando en caché datos o componentes según sea necesario.
Es una "directiva" de JavaScript —un literal de cadena que agregas en tu código— que le indica al compilador de Next.js que entre en un "límite" diferente. Por ejemplo, pasar del servidor al cliente.
Esta es una idea similar a las directivas de React como 'use client'
y 'use server'
. Las directivas son instrucciones del compilador que definen dónde debe ejecutarse el código, permitiendo que el framework optimice y orqueste piezas individuales por ti.
¿Cómo funciona?
Comencemos con un ejemplo simple:
Detrás de escenas, Next.js transforma este código en una función del servidor debido a la directiva 'use cache'
. Durante la compilación, se encuentran las "dependencias" de esta entrada de caché y se usan como parte de la clave de caché.
Por ejemplo, id
se convierte en parte de la clave de caché. Si llamamos a getUser(1)
múltiples veces, devolvemos la salida memorizada de la función del servidor en caché. Cambiar este valor creará una nueva entrada en el caché.
Veamos un ejemplo usando la función en caché en un componente del servidor con un cierre.
Este ejemplo es más difícil. ¿Puedes identificar todas las dependencias que deben ser parte de la clave de caché?
Los argumentos index
y limit
tienen sentido —si estos valores cambian, seleccionamos un segmento diferente de las notificaciones. Sin embargo, ¿qué pasa con el id
del usuario? Su valor proviene del componente padre.
El compilador puede entender que getNotifications
también depende de id
, y su valor se incluye automáticamente en la clave de caché. Esto evita toda una categoría de problemas de caché por dependencias incorrectas o faltantes en la clave de caché.
¿Por qué no usar una función de caché?
Revisemos el último ejemplo. ¿Podríamos usar una función cache()
en lugar de una directiva?
Una función cache()
no podría mirar dentro del cierre y ver que el valor de id
debería ser parte de la clave de caché. Tendrías que especificar manualmente que id
es parte de tu clave. Si olvidas hacerlo o lo haces incorrectamente, arriesgas colisiones de caché o datos obsoletos.
Los cierres pueden capturar todo tipo de variables locales. Un enfoque ingenuo podría accidentalmente incluir (u omitir) variables que no pretendías. Eso puede llevar a almacenar en caché los datos incorrectos, o podría arriesgar la contaminación del caché si información sensible se filtra en la clave de caché.
'use cache'
le da al compilador suficiente contexto para manejar cierres de manera segura y producir claves de caché correctamente. Una solución solo en tiempo de ejecución, como cache()
, requeriría que hicieras todo manualmente —y es fácil cometer errores. En contraste, una directiva puede analizarse estáticamente para manejar de manera confiable todas tus dependencias bajo el capó.
¿Cómo se manejan los valores de entrada no serializables?
Tenemos dos tipos diferentes de valores de entrada para almacenar en caché:
- Serializables: Aquí, "serializable" significa que una entrada puede convertirse en un formato estable basado en cadenas sin perder significado. Mientras que muchas personas piensan primero en
JSON.stringify
, en realidad usamos la serialización de React (por ejemplo, a través de Componentes del Servidor) para manejar un rango más amplio de entradas —incluyendo promesas, estructuras de datos circulares y otros objetos complejos. Esto va más allá de lo que JSON simple puede hacer. - No serializables: Estas entradas no son parte de la clave de caché. Cuando intentamos almacenar estos valores en caché, devolvemos una "referencia" del servidor. Esta referencia luego es usada por Next.js para restaurar el valor original en tiempo de ejecución.
Digamos que recordamos incluir id
en la clave de caché:
Esto funciona si los valores de entrada pueden serializarse. Pero si id
fuera un elemento React o un valor más complejo, tendríamos que serializar manualmente las claves de entrada. Considera un componente del servidor que obtiene el usuario actual basado en una propiedad id
:
Recorramos cómo funciona esto:
- Durante la compilación, Next.js ve la directiva
'use cache'
y transforma el código para crear una función especial del servidor que soporta caché. No ocurre almacenamiento en caché durante la compilación, sino que Next.js está configurando el mecanismo necesario para el caché en tiempo de ejecución. - Cuando tu código llama a la "función de caché", Next.js serializa los argumentos de la función. Cualquier cosa que no sea directamente serializable, como JSX, se reemplaza con un marcador de "referencia".
- Next.js verifica si existe un resultado en caché para los argumentos serializados dados. Si no se encuentra ningún resultado, la función calcula el nuevo valor para almacenar en caché.
- Después de que la función termina, el valor de retorno se serializa. Las partes no serializables del valor de retorno se convierten nuevamente en referencias.
- El código que llamó a la función de caché deserializa la salida y evalúa las referencias. Esto permite a Next.js intercambiar las referencias con sus objetos o valores reales, lo que significa que entradas no serializables como
children
pueden mantener sus valores originales, no almacenados en caché.
Esto significa que podemos almacenar en caché de manera segura solo el componente <Profile>
y no los hijos. En renderizados posteriores, getUser()
no se llama nuevamente. El valor de children
podría ser dinámico o un elemento almacenado en caché por separado con una vida útil de caché diferente. Esto es almacenamiento en caché componible.
Esto parece familiar...
Si estás pensando "eso se siente como el mismo modelo de composición de servidor y cliente"— tienes toda la razón. A veces esto se llama el patrón "donut":
- La parte externa de la dona es un componente del servidor que maneja la obtención de datos o lógica pesada.
- El hueco en el centro es un componente hijo que podría tener algo de interactividad
'use cache'
es lo mismo. La dona es el valor en caché del componente externo y el hueco son las referencias que se llenan en tiempo de ejecución. Es por eso que cambiar children
no invalida toda la salida en caché. Los hijos son solo algunas referencias que se llenan más tarde.
¿Qué pasa con el etiquetado y la invalidación?
Puedes definir la vida del caché con diferentes perfiles. Incluimos un conjunto de perfiles predeterminados, pero puedes definir tus propios valores personalizados si lo deseas.
Para invalidar una entrada de caché específica, puedes etiquetar el caché y luego llamar a revalidateTag()
. Un patrón poderoso es que puedes etiquetar el caché después de haber obtenido tus datos (por ejemplo, desde un CMS):
Simple y poderoso
Nuestro objetivo con 'use cache'
es hacer que la lógica de almacenamiento en caché sea simple y poderosa.
- Simple: Puedes crear entradas de caché con razonamiento local. No necesitas preocuparte por efectos secundarios globales, como entradas olvidadas en la clave de caché o cambios no intencionados en otras partes de tu código base.
- Poderoso: Puedes almacenar en caché más que solo código estáticamente analizable. Por ejemplo, valores que podrían cambiar en tiempo de ejecución, pero aún así deseas almacenar en caché el resultado después de que haya sido evaluado.
'use cache'
sigue siendo experimental dentro de Next.js. Nos encantaría recibir tus comentarios iniciales mientras lo pruebas.