Posponer los efectos secundarios en Next.js con la API after()
Aprenda cómo la API after() de Next.js ejecuta análisis, registro y tareas en segundo plano después de la respuesta, además de sus garantías, problemas comunes y los compromisos en el manejo de errores.
La mayoría de los problemas de rendimiento en las aplicaciones Next.js se deben a tratar cada tarea como si tuviera la misma urgencia. Los equipos hacen que los usuarios esperen mientras se escriben datos en las analíticas, se generan registros de auditoría, se limpia la caché o se envían notificaciones antes de devolver una respuesta. Un visitante debe soportar un retraso de 300 milisegundos para que se inserte un dato en la base de datos, cuyo resultado nadie realmente necesita ver. La respuesta en sí llega tarde porque se obligó a ejecutar una serie de efectos secundarios no relacionados uno tras otro.
El patrón típico vincula la ejecución de tareas críticas con las labores rutinarias del sistema. Las acciones del servidor permanecen inactivas esperando la respuesta de un proveedor de registro. Los controladores de rutas se detienen para que un servicio de métricas pueda registrar un evento. Cada una de estas tareas en segundo plano reduce gradualmente el tiempo de carga percibido, y el efecto acumulativo es una aplicación lenta que aleja a los usuarios.
La API after() en Next.js separa los efectos secundarios del ciclo de vida de la respuesta. Puedes encerrar las tareas no esenciales en after(), y el framework programa su ejecución una vez que la respuesta ya ha sido entregada. El cliente recibe su carga de datos de inmediato, mientras que la generación de registros, el análisis y otras tareas en segundo plano se ejecutan posteriormente, completamente fuera de la ruta crítica.
El resto de este artículo explica los mecanismos de after(), los escenarios en los que resulta útil y las consideraciones operativas que debes evaluar antes de utilizarlo en entornos de producción.
Puntos clave
after()pospone los efectos secundarios hasta que la respuesta haya finalizado, evitando que las tareas no esenciales afecten la ruta de solicitud dirigida al usuario.
waitUntil(), que está vinculado al Edge Runtime, after() funciona de manera consistente tanto si se ejecuta en Node.js como en Edge, independientemente del proveedor de hosting.after() ocurren después de que el cliente ya ha recibido una respuesta, es necesario utilizar un manejo explícito con try-catch para capturarlos y registrarlos.after().¿Qué es la API after() y cómo funciona?
after() acepta una función de callback que se ejecuta una vez que el flujo de respuesta se ha cerrado. La secuencia es la siguiente: el entorno de ejecución pone en cola la función de callback, se envía la respuesta HTTP al cliente y solo entonces se ejecuta el trabajo diferido. El navegador nunca tiene que esperar a que finalice esa función de callback.
Dentro de una misma solicitud, Next.js mantiene el orden en que se registraron las llamadas a after(). Si su manejador llama a after() tres veces seguidas, esas tres funciones de callback se ejecutan una tras otra, en ese mismo orden, una vez que la respuesta haya finalizado. Esta garantía de orden es importante siempre que una tarea diferida dependa de otra; por ejemplo, al registrar una acción en un registro antes de invalidar una entrada de caché que refleje dicha acción.
Se trata de un modelo significativamente diferente al simplemente enviar una promesa y olvidarse de ella. Las promesas de tipo “envía y olvida” tienden a absorber en silencio las rechazos no manejados, lo que puede dejar de forma discreta los registros incompletos o el estado de la aplicación des sincronizado. after() hace que el entorno de ejecución se encargue explícitamente de programar esa tarea, lo que facilita enormemente la observación y el manejo adecuado de los errores en producción.
La diferencia entre la ejecución bloqueante y no bloqueante se vuelve evidente una vez que se mide. Un manejador que registra de forma síncrona antes de responder suele añadir entre 50 y 150 milisegundos por solicitud. Al mover esa misma llamada de registro a after(), el manejador puede devolver la respuesta en menos de 10 milisegundos, tiempo justo para realizar la escritura en la base de datos. Desde el punto de vista del usuario, la respuesta parece instantánea, mientras que todo el proceso ocurre de forma invisible en segundo plano.
Casos de uso en el mundo real: análisis de registros y tareas en segundo plano
El seguimiento analítico es el ejemplo clásico. Cuando un cliente finaliza la compra, tu aplicación registra la transacción y muestra una pantalla de confirmación. La plataforma analítica no necesita conocer esa compra en el instante en que ocurre, solo necesita saberlo eventualmente. Al posponer la llamada de seguimiento a after() se pueden ahorrar de 100 a 200 milisegundos en el flujo de compra sin perder ningún dato.
El registro de auditoría funciona de la misma manera. Las reglas de cumplimiento suelen exigir que cada cambio de estado se registre en algún lugar, pero no hay razón para que el usuario tenga que esperar a que ese sistema de auditoría confirme la escritura. La ruta crítica se encarga del cambio real en la base de datos; la función de callback after() envía la entrada de auditoría a un sistema separado, generalmente un almacén de registros optimizado para escrituras o una cola de mensajes.
La invalidación de caché es otra opción adecuada, especialmente cuando esta afecta a varios servicios remotos. Actualizar un contenido puede significar borrar los cachés de CDN, eliminar claves específicas de Redis y enviar señales a los clientes WebSocket conectados. Nada de esto afecta lo que el usuario ve en la respuesta. La actualización se guarda en la base de datos, la respuesta indica éxito y after() se encarga posteriormente de la cascada de invalidación.
El envío de correos electrónicos o notificaciones también encaja bien dentro de after(), siempre y cuando la aplicación no necesite mostrar de forma síncrona un fallo en la entrega. Considere el restablecimiento de contraseñas: la aplicación guarda el token de restablecimiento, devuelve un mensaje de éxito y envía el correo en segundo plano. Si el proveedor de correos falla, el usuario puede simplemente intentarlo de nuevo desde la interfaz en lugar de ver un error en la solicitud original.
Se evita un modo de fallo sutil pero costoso. Si el envío por correo electrónico se realiza de forma directa y el proveedor llega al tiempo límite, al usuario se le muestra un error 500 aunque el token de restablecimiento se haya creado con éxito. El usuario lo intenta nuevamente, generando un token duplicado, y ahora su sistema debe eliminar los tokens huérfanos o correr el riesgo de tener una brecha de seguridad. Al manejar el envío por correo electrónico dentro de after(), ese fallo se aísla completamente: la respuesta sigue siendo exitosa, y una capa de monitoreo separada puede detectar los problemas de entrega de forma independiente.
La revalidación de datos en segundo plano y el calentamiento del caché son casos similares. Supongamos que un producto popular se agota: la actualización del inventario debe provocar la revalidación de las páginas de la categoría correspondiente y del caché de la página principal. La actualización del stock en sí se realiza de inmediato, mientras que la función de callback after() recorre el grafo de dependencias y marca las entradas obsoletas para su regeneración. Los visitantes que naveguen durante ese período de revalidación podrían ver datos ligeramente desactualizados, pero la aplicación seguirá funcionando rápidamente.
Implementación de after() en acciones del servidor y controladores de ruta
Las acciones del servidor pueden llamar a after() directamente como parte de una mutación. La acción realiza su operación principal, programa los efectos secundarios necesarios y devuelve el control al cliente. Next.js gestiona el ciclo de vida en segundo plano, asegurándose de que la función de callback se ejecute antes de que la función sin servidor pueda cerrarse.
Los manejadores de rutas siguen la misma estructura: el manejador realiza el trabajo esencial, envía su respuesta y coloca cualquier tarea de mantenimiento en after(). Esto funciona tanto en los manejadores de rutas de App Router como en las rutas API de Pages Router configuradas para utilizar el entorno de ejecución de App Router.
Un detalle importante es que after() tiene acceso al contexto completo que existía cuando se registró. Todo lo capturado en el cierre —cabeceras de la solicitud, datos del cuerpo analizados, estado de autenticación— permanece disponible dentro de la función de callback sin necesidad de configuraciones adicionales.
El middleware también puede utilizar after() para registrar metadatos de la solicitud sin ralentizar aún más al manejador en la cadena. El middleware obtiene las cabeceras que necesita, programa la llamada de registro y pasa la solicitud adelante. La entrada del registro se escribe de forma asíncrona mientras la solicitud sigue su camino hacia su destino.
La fiabilidad real de esa ejecución depende en gran medida del lugar donde la alojes. Plataformas como Vercel amplían el tiempo de vida de la función específicamente para que los callbacks after() tengan tiempo de completarse. Si la alojas tú mismo en contenedores serverless, debes configurar los tiempos de espera con cuidado: si el contenedor se detiene antes de que finalice el callback, ese trabajo simplemente se pierde. Eso representa un problema real para cualquier tarea que no pueda soportar interrupciones, como los eventos de facturación o el registro relacionado con el cumplimiento normativo.
after() vs waitUntil() vs Enfoques tradicionales
waitUntil(), que proviene del Edge Runtime, resuelve un problema similar pero a un nivel más bajo: mantiene activa una función hasta que se resuelva una promesa determinada, evitando que el entorno de ejecución cierre las operaciones demasiado pronto. after() crea una abstracción sobre ese mecanismo y se comporta de la misma manera tanto en Node.js como en Edge.
Los proyectos que ya utilizan waitUntil() en un contexto de Edge Runtime pueden adoptar gradualmente after(). Los dos no son idénticos en su estructura: waitUntil() recibe directamente una promesa, mientras que after() envuelve una función de callback. Ambos logran el mismo objetivo de evitar la terminación prematura, pero after() es más fácil de usar cuando se necesitan enlazar varias tareas en segundo plano, ya que evita tener que orquestar manualmente múltiples promesas.
Las técnicas de tipo “dispara y olvida” basadas en promesas no esperadas o llamadas a setTimeout no ofrecen ninguna garantía real. El entorno de ejecución puede cerrarse antes de que la promesa se complete, descartando silenciosamente cualquier tarea en curso. Las bibliotecas de registro desarrolladas con process.nextTick() o setImmediate() se enfrentan al mismo problema una vez desplegadas en entornos serverless. En contraste, after() expresa claramente la intención y brinda a la plataforma una verdadera oportunidad de respetarla.
Las colas de mensajes siguen siendo la opción más sólida para el procesamiento en segundo plano, pero conllevan costos operativos reales. Se necesita infraestructura, trabajadores dedicados, mecanismos de intento repetido y paneles de monitoreo para mantener todo funcionando. Para efectos secundarios menores como escribir registros o invalidar una entrada de caché, ese nivel de complejidad es excesivo. after() se sitúa entre estos dos extremos: más robusto que una llamada sencilla sin seguimiento, pero mucho menos complejo que implementar un sistema basado en colas.
Ese compromiso se manifiesta de manera más clara en la forma en que se manejan los fallos. Una cola de mensajes vuelve a intentar automáticamente las tareas fallidas y puede dirigir los fallos persistentes a una cola de cartas muertas para su inspección posterior. Por otro lado, una función de callback after() solo se ejecuta una vez por solicitud. Si falla, ese trabajo se pierde a menos que haya creado su propio mecanismo de reintentos alrededor de ella. Ese es un riesgo aceptable para operaciones de bajo impacto, pero para cualquier tarea crítica, como procesar un pago o actualizar los conteos de inventario, una cola adecuada sigue siendo la herramienta correcta.
Consideraciones de producción: manejo de errores y garantías de ejecución
La gestión de errores dentro de after() recae exclusivamente en usted: envuelva la lógica en bloques explícitos de try-catch. Una excepción lanzada dentro del callback no llegará al cliente, ya que la respuesta ya se ha enviado para cuando se ejecuta el callback. La plataforma registrará el fallo, pero recuperarse de él —reintentos, alertas, lógica de respaldo— es responsabilidad de la aplicación.
La fiabilidad con la que realmente finaliza la función de callback depende en gran medida del lugar donde se aloje la aplicación. En Vercel, la ejecución de las funciones se prolonga para permitir que las callbacks after() se ejecuten, hasta el tiempo de espera configurado. Ejecutar Next.js en AWS Lambda requiere un ajuste cuidadoso del tiempo de espera para que la función no sea reciclada antes de que finalice la callback. GCP Cloud Run y Azure Container Instances presentan restricciones similares. En todos estos casos, el patrón de fallo recurrente es el mismo: la función se agota antes de que termine el trabajo diferido, y ese trabajo se pierde para siempre.
La observabilidad se vuelve esencial una vez que este patrón llega a producción. Los registros regulares de la aplicación cubren el ciclo de vida principal de la solicitud, pero las devoluciones de llamada after() se ejecutan después de que ese ciclo de vida haya terminado técnicamente, fuera del contexto habitual de registro. Las herramientas de rastreo distribuido como OpenTelemetry deben configurarse para capturar explícitamente la devolución de llamada como un span independiente. Si se omite ese paso, cualquier error que ocurra dentro de after() quedará efectivamente invisible para quien esté monitoreando el sistema.
Las pruebas de carga revelan otra dimensión del comportamiento del patrón. Considere un manejador de rutas que ejecuta una tarea de 500 ms dentro de after(). Probado de forma aislada, esa ruta parece rápida. Pero bajo una carga de 100 solicitudes simultáneas, la plataforma debe ejecutar 100 llamadas de retorno casi al mismo tiempo, y es posible que no pueda mantener el ritmo. La ruta principal de solicitudes sigue siendo ágil, pero las tareas diferidas comienzan a acumularse. La infraestructura debe dimensionarse no solo en función del volumen de solicitudes entrantes, sino también de la carga adicional que generan las tareas en segundo plano.
after(). Una vez que se estabiliza el flujo de respuestas, el proveedor de análisis recibe repentinamente aproximadamente 1,000 llamadas seguidas, lo que puede hacer que comience a limitar o rechazarlas. Agrupar la lógica de devolución de llamada ayuda en este caso: en lugar de enviar una solicitud por cada evento, se acumulan los eventos en memoria y se envían todos juntos en lotes más grandes y menos frecuentes.
No obstante, el agrupamiento conlleva sus propios problemas de ajuste. El búfer que almacena los eventos acumulados permanece en memoria hasta que se ejecuta el vaciado, consumiendo recursos todo ese tiempo. Un aumento repentino del tráfico puede agotar esa memoria antes de que se active el vaciado programado. La otra opción es que la función de callback after() envíe los eventos a una cola persistente en lugar de almacenarlos en memoria, pero en ese caso se vuelve a introducir gran parte de la complejidad que after() estaba destinado a ayudarle a evitar.
En última instancia, si after() es la opción adecuada depende de cuán grave sería perder el trabajo diferido. Elementos como eventos de análisis, registros de auditoría e invalidación de caché pueden soportar una ejecución ocasional fallida sin afectar nada esencial. En cambio, las confirmaciones de pago, los cambios en el inventario y los eventos relacionados con la seguridad no lo pueden. Para este tipo de tareas, una cola de mensajes o un procesamiento síncrono directo sigue siendo la opción más segura, incluso si implica una menor velocidad de respuesta.
Preguntas frecuentes
¿Pueden los callbacks de after() acceder a datos del escopo de la solicitud, como encabezados o cookies?
Sí. La función de callback hereda el ámbito que existía en el momento en que se llamó a after(), por lo que cualquier variable, valor de encabezado o datos de cookie disponibles en ese instante siguen siendo accesibles dentro de la función de callback. En la práctica, eso significa que se pueden referenciar elementos como el usuario autenticado, el cuerpo de la solicitud analizado o los metadatos extraídos sin necesidad de pasarlos por algún canal separado.
¿Qué sucede si una función de callback after() lanza un error no manejado?
El entorno de ejecución lo registra, pero el error nunca llega al cliente, ya que la respuesta ya se ha enviado para cuando se ejecuta la función de callback. Corresponde a la aplicación envolver las funciones de callback after() en bloques try-catch y reenviar los fallos a una herramienta de monitoreo o a un mecanismo de reintentos. Si se omite esto, los fallos simplemente desaparecen sin dejar rastro.
¿Funciona after() tanto en entornos Node.js como en Edge Runtime?
Sí, la API mitiga las diferencias entre los dos entornos de ejecución. Bajo Edge Runtime, se basa en las mismas semánticas que waitUntil(). Bajo el entorno de Node.js, utiliza cualquier mecanismo específico de la plataforma disponible para prolongar el tiempo de vida de la función. La interfaz permanece igual en ambos casos, pero el grado en que se garantiza la ejecución depende aún del diseño del proveedor de hosting.
¿En qué se diferencia after() del uso de una cola de mensajes para tareas en segundo plano?
Una cola de mensajes ofrece reintentos automáticos, manejo de mensajes no entregados y ejecución fiable, pero a costa de un mayor sobrecargo operativo. after() es una opción mucho más ligera, adecuada para efectos secundarios como el registro de logs o la invalidación de caché, donde una pérdida ocasional no supone un gran problema. Cuando el trabajo definitivamente no puede perderse, una cola sigue siendo la opción más adecuada.
¿Pueden ejecutarse simultáneamente múltiples callbacks after() o se ejecutan de forma secuencial?
Dentro de una sola solicitud, los callbacks se ejecutan uno tras otro, en el orden en que fueron registrados. Si se llama a after() tres veces por separado, el motor de ejecución termina completamente el primer callback antes de comenzar con el segundo y luego con el tercero. Ese orden es importante cuando una operación diferida depende de otra, como registrar una acción antes de invalidar una entrada de caché relacionada.
Conclusión: Cuándo utilizar after() en tu aplicación Next.js
after() permite separar las tareas no esenciales del camino de la solicitud que está esperando el usuario, sin obligar a implementar una infraestructura de colas. Tareas como el seguimiento analítico, el registro de auditoría, la invalidación de caché y el envío de notificaciones son ejemplos adecuados, ya que el usuario recibe su respuesta de inmediato mientras la aplicación se ocupa silenciosamente del resto en segundo plano.
El problema es que la ejecución no está garantizada. Las plataformas serverless desactivan rápidamente las funciones, y si el entorno interrumpe la llamada de retorno a mitad de ejecución, ese trabajo se pierde. Por esta razón, los tiempos de espera deben configurarse teniendo en cuenta las necesidades de la llamada de retorno, y cada llamada de retorno after() debe incluir su propio manejo de errores. Cuando perder ese trabajo ocasionalmente es aceptable, la simplicidad de after() lo hace valer la pena. Cuando no lo es, una cola de mensajes sigue siendo la opción más fiable.
Comprender estos patrones debería ser suficiente para comenzar a utilizar after() de manera efectiva en una aplicación Next.js. Si se aplica con cuidado, puede marcar una diferencia notable en la velocidad percibida por los usuarios, y esa diferencia es especialmente importante en rutas con alto tráfico, donde pequeñas demoras se acumulan y pueden llevar a los usuarios a abandonar la sesión por completo.
Lecturas relacionadas
- Benchmarking del compilador Go de TypeScript 7 en una app real de Next.js — Una comparación práctica de los tiempos de verificación con tsc entre TypeScript 6 y 7 en un códigobase real de Next.js, incluyendo un error de incompatibilidad en CI y orientaciones para la actualización.