Directivas y el límite de la plataforma - Una tendencia silenciosa en el ecosistema de JavaScript

  • Tanner Linsley

Una Tendencia Silenciosa en el Ecosistema de JavaScript

Durante años, JavaScript ha tenido exactamente una directiva significativa: "use strict". Es estándar, aplicada por los entornos de ejecución y se comporta igual en todos los entornos. Representa un contrato claro entre el lenguaje, los motores y los desarrolladores.

Pero ahora estamos presenciando el surgimiento de una nueva tendencia. Los marcos de trabajo están inventando sus propias directivas de nivel superior: use client, use server, use cache, use workflow, y más están apareciendo en todo el ecosistema. Parecen características del lenguaje. Se sitúan donde se ubican las características reales del lenguaje. Afectan cómo se interpreta, empaqueta y ejecuta el código.

Hay una distinción importante: estas no son características estándar de JavaScript. Los entornos de ejecución no las entienden, no existe una especificación reguladora, y cada marco tiene libertad para definir su propio significado, reglas y casos límite.

Esto puede parecer ergonómico hoy, pero también aumenta la confusión, complica la depuración y supone costos para las herramientas y la portabilidad, patrones que ya hemos visto antes.


Cuando las directivas parecen la plataforma, los desarrolladores las tratan como tal

Una directiva al comienzo de un archivo parece autoritaria. Da la impresión de ser una verdad a nivel de lenguaje, no una sugerencia del marco. Esto crea un problema de percepción:

  • Los desarrolladores asumen que las directivas son oficiales
  • Los ecosistemas comienzan a tratarlas como una superficie de API compartida
  • Los nuevos aprendices tienen dificultades para distinguir entre JavaScript y magia del marco
  • El límite entre plataforma y proveedor se desdibuja
  • La depuración sufre y las herramientas deben especializarse en comportamientos

Ya hemos visto confusión. Muchos desarrolladores ahora creen que use client y use server son simplemente cómo funciona el JavaScript moderno, sin darse cuenta de que solo existen dentro de pipelines de compilación específicos y semánticas de componentes de servidor. Esa equivocación señala un problema más profundo.


Reconocimiento donde corresponde: use server y use client

Algunas directivas existen porque varias herramientas necesitaban un punto de coordinación simple. En la práctica, use server y use client son soluciones pragmáticas que indican a los empaquetadores y entornos de ejecución dónde se permite ejecutar el código en un mundo de RSC. Han recibido un apoyo relativamente amplio entre empaquetadores precisamente porque el alcance es estrecho: la ubicación de ejecución.

Eso dicho, incluso estas muestran los límites de las directivas cuando aparecen necesidades del mundo real. A gran escala, a menudo se necesitan parámetros y políticas que afectan profundamente a la corrección y la seguridad: método HTTP, encabezados, middleware, contexto de autenticación, rastreo, comportamientos de caché, y más. Las directivas no tienen un lugar natural para llevar esas opciones, lo que significa que frecuentemente se ignoran, se añaden en otro lugar o se reencodifican como nuevas variantes de directivas.


Donde las directivas comienzan a tensarse: opciones y APIs adyacentes a directivas

Cuando una directiva necesita opciones inmediatamente, o poco después de su creación, y genera hermanos (por ejemplo, 'use cache:remote') y llamadas auxiliares como cacheLife(...), eso suele ser una señal de que la característica quiere ser una API, no una cadena en la parte superior de un archivo. Si ya sabes que necesitas una función, simplemente úsala para todo.

Ejemplos:

'use cache:remote'
const fn = () => 'value'
// API explícita con procedencia y opciones
import { cache } from 'next/cache'
export const fn = cache(() => 'value', {
  strategy: 'remote',
  ttl: 60,
})

Y para comportamientos de servidor donde los detalles importan:

import { server } from '@acme/runtime'

export const action = server(
  async (req) => {
    return new Response('ok')
  },
  {
    method: 'POST',
    headers: { 'x-foo': 'bar' },
    middleware: [requireAuth()],
  }
)

Las APIs llevan procedencia (imports), versionado (paquetes), composición (funciones) y testabilidad. Las directivas típicamente no lo hacen, y tratar de codificar opciones en ellas puede convertirse rápidamente en un mal diseño.


Sintaxis compartida sin una especificación compartida puede ser una base frágil

Una vez que varios marcos comienzan a adoptar directivas, terminamos en el peor estado posible:

Categoría Sintaxis Compartida Contrato Compartido Resultado
ECMAScript :white_check_mark: :white_check_mark: Estable y universal
APIs de Framework :cross_mark: :cross_mark: Aislado y aceptable
Directivas de Framework :white_check_mark: :cross_mark: Confuso e inestable

Una superficie compartida sin una definición compartida crea:

  • Desviación en la interpretación, cada marco define sus propias semánticas
  • Problemas de portabilidad, código que parece universal pero no lo es
  • Carga de herramientas, empaquetadores, analizadores y IDEs deben adivinar o perseguir el comportamiento
  • Fricción con la plataforma, los organismos de estándares quedan atrapados por las expectativas del ecosistema

Un ejemplo de donde hemos visto estas luchas antes es con los decoradores. TypeScript normalizó una semántica no estándar, la comunidad construyó sobre ella, luego TC39 tomó una dirección diferente. Esto fue y sigue siendo una migración dolorosa para muchos.


“¿No es esto simplemente un plugin de Babel/macros con sintaxis diferente?”

Funcionalmente, sí. Tanto las directivas como las transformaciones personalizadas pueden cambiar el comportamiento en tiempo de compilación. El problema no es la capacidad; es la superficie y la apariencia.

  • Las directivas parecen la plataforma. Sin importación, sin propietario, sin fuente explícita. Señalan “esto es JavaScript”.
  • Las APIs/macros apuntan a un propietario. Las importaciones proporcionan procedencia, versionado y descubribilidad.

En el mejor de los casos, una directiva es equivalente a llamar a una función global sin importar, como window.useCache() al comienzo de tu archivo. Es exactamente por eso que es arriesgado: oculta al proveedor y mueve las semánticas del marco a lo que parece ser el lenguaje.

Ejemplos:

'use cache'
const fn = () => 'value'
// API explícita (importada, propia, descubrible)
import { createServerFn } from '@acme/runtime'
export const fn = createServerFn(() => 'value')
// Magia global (sin importar, proveedor oculto)
window.useCache()
const fn = () => 'value'

Por qué esto importa:

  • Propiedad y procedencia: las importaciones te dicen quién proporciona el comportamiento; las directivas no lo hacen.
  • Ergonomía de herramientas: las APIs viven en el espacio de paquetes; las directivas requieren especialización ecosistémica.
  • Portabilidad y migración: reemplazar una API importada es sencillo; deshacer las semánticas de directivas en varios archivos es costoso y ambiguo.
  • Educación y expectativas: las directivas difuminan el límite de la plataforma; las APIs hacen el límite explícito.

Así que aunque un plugin personalizado de Babel o una macro pueda implementar la misma característica subyacente, la API basada en importaciones la mantiene claramente en el espacio del marco. Las directivas trasladan ese mismo comportamiento al espacio que parece ser el lenguaje, que es la preocupación principal de este artículo.


“¿No lo arregla el nombrado?” (por ejemplo, “use next.js cache”)

El nombrado ayuda a la descubribilidad humana, pero no aborda los problemas fundamentales:

  • Aún parece la plataforma. Una literal de cadena en nivel superior implica lenguaje, no biblioteca.
  • Aún carece de procedencia y versionado a nivel de módulo. Las importaciones codifican ambos; las cadenas no lo hacen.
  • Aún requiere especialización en toda la cadena de herramientas (empaquetadores, analizadores, IDEs), en lugar de aprovechar la resolución normal de importaciones.
  • Aún fomenta una pseudo-estandarización de sintaxis sin una especificación, solo con prefijos de proveedor.
  • Aún aumenta el costo de migración en comparación con el intercambio de una API importada.

Ejemplos:

'use next.js cache'
const fn = () => 'value'
// API explícita, propia, con procedencia y versionado
import { cache } from 'next/cache'
export const fn = cache(() => 'value')
```Si el objetivo es la trazabilidad, las importaciones ya lo resuelven de forma limpia y funcionan con el ecosistema actual. Si el objetivo es una primitiva compartida entre marcos, se necesita una especificación real, no cadenas de proveedores que parezcan sintaxis.

---

### Las directivas pueden impulsar dinámicas competitivas

Una vez que las directivas se convierten en una superficie competitiva, los incentivos cambian:

1. Un proveedor lanza una nueva directiva  
2. Se convierte en una característica visible  
3. Los desarrolladores la esperan en todas partes  
4. Otros marcos sienten presión por adoptarla  
5. La sintaxis se propaga sin una especificación  

Así es como obtenemos:

```tsx
'use server'
'use client'
'use cache'
'use cache:remote'
'use workflow'
'use streaming'
'use edge'

Incluso tareas duraderas, estrategias de caché y ubicaciones de ejecución ahora se codifican como directivas. Estas son semánticas de tiempo de ejecución, no semánticas de sintaxis. Codificarlas como directivas establece una dirección fuera del proceso de estándares y merece precaución.


Considerar APIs en lugar de directivas para características con muchas opciones

La ejecución duradera es un buen ejemplo (por ejemplo, 'use workflow', 'use step'), pero el punto es general: las directivas pueden reducir el comportamiento a un valor booleano, mientras que muchas características se benefician de opciones y espacio para evolucionar. Los compiladores y transformadores pueden soportar cualquier superficie; se trata de elegir la adecuada para la longevidad y la claridad.

'use workflow'
'use step'

Una opción: una API explícita con trazabilidad y opciones:

import { workflow, step } from '@workflows/workflow'

export const sendEmail = workflow(
  async (input) => {
    /* ... */
  },
  { retries: 3, timeout: '1m' }
)

export const handle = step(
  'fetchUser',
  async () => {
    /* ... */
  },
  { cache: 60 }
)

Las formas de función pueden ser igual de compatibles con AST/transformaciones que las directivas, y llevan trazabilidad (imports) y seguridad de tipos.

Otra opción es inyectar una global una vez y tiparla:

// inicialización única
globalThis.workflow = createWorkflow()
// tipos globales (por ejemplo, global.d.ts)
declare global {
  var workflow: typeof import('@workflows/workflow').workflow
}

El uso permanece en formato de API, sin directivas:

export const task = workflow(
  async () => {
    /* ... */
  },
  { retries: 5 }
)

Los compiladores que amplían la ergonomía son excelentes. Solo hay que mirar JSX como precedente útil. Solo necesitamos hacerlo con cuidado y responsabilidad: ampliar mediante APIs con trazabilidad y tipos claros, no mediante cadenas en nivel superior que parezcan al lenguaje. Estas son opciones, no prescripciones.


Pueden surgir formas sutiles de bloqueo

Incluso sin mala intención, las directivas crean bloqueo por diseño:

  • Bloqueo mental: los desarrolladores forman memoria muscular alrededor de las semánticas de un proveedor
  • Bloqueo de herramientas: IDEs, empaquetadores y compiladores deben apuntar a un tiempo de ejecución específico
  • Bloqueo de código: las directivas están en el nivel de sintaxis, lo que las hace costosas de eliminar o migrar

Las directivas pueden no parecer propietarias, pero pueden comportarse más como características propietarias que una API, porque reconfiguran la gramática del ecosistema.


Si queremos primitivas compartidas, debemos colaborar en especificaciones y APIs

Absolutamente hay problemas reales que resolver:

  • Límites de ejecución en servidor
  • Flujos de trabajo asíncronos y streaming
  • Primitivas de tiempo de ejecución distribuidas
  • Tareas duraderas
  • Semánticas de caché

Pero esos son problemas para APIs, capacidades y futuros estándares, no para pseudo-sintaxis sin gobernanza impulsadas a través de empaquetadores.

Si múltiples marcos realmente quieren primitivas compartidas, un camino responsable es:

  • Colaborar en una especificación multi-marco
  • Proponer primitivas a TC39 cuando sea apropiado
  • Mantener las características no estándar claramente delimitadas al espacio de API, no al espacio del lenguaje

Las directivas deberían ser raras, estables y estandarizadas, y especialmente usadas con cautela en lugar de proliferar entre proveedores.


Por qué esto difiere del momento de JSX/Virtual DOM

Es tentador comparar la crítica a las directivas con la escepticismo inicial hacia JSX o el Virtual DOM de React. Los modos de falla son diferentes. JSX y el VDOM no se disfrazaron como funciones del lenguaje; venían con imports explícitos, trazabilidad y límites de herramientas. Por el contrario, las directivas viven en el nivel superior de los archivos y parecen parte de la plataforma, lo que crea expectativas de ecosistema y cargas de herramientas sin una especificación compartida.


La conclusión

Las directivas de marcos podrían parecer magia de experiencia de usuario hoy, pero la tendencia actual arriesga un futuro más fragmentado compuesto por dialectos definidos no por estándares, sino por herramientas.

Podemos aspirar a límites más claros.

Si los marcos quieren innovar, deben hacerlo, pero también deben distinguir claramente comportamiento de marco de semánticas de plataforma, en lugar de difuminar esa línea para una adopción a corto plazo. Límites más claros ayudan al ecosistema.

2 Me gusta