Cloudflare ha reescrito el registro de módulos del componente de código abierto workerd, el componente principal del entorno de ejecución de Workers, con el objetivo de mejorar la velocidad, el cumplimiento de los estándares y acercar la forma de cargar y resolver módulos al comportamiento de Node.js. Esto coincide con la activación predeterminada de las API estables de Node.js en Workers y con la posibilidad de desplegar aplicaciones de hasta 64 MiB en todos los planes, después de eliminar el límite de tamaño del paquete comprimido.
Sin embargo, el cambio más importante no está relacionado únicamente con el número de API disponibles. Las aplicaciones de Node.js también dependen de la forma en que el entorno de ejecución identifica, carga y almacena en caché los módulos. Esto incluye los módulos ESM, CommonJS y WebAssembly, responsabilidades que gestiona el registro de módulos dentro de workerd.
¿Qué ha cambiado en el nuevo registro?
Los desarrolladores pueden probar la nueva implementación añadiendo la opción new_module_registry a la configuración del Worker. Al activarla, Workers admite import.meta.url, import.meta.main e import.meta.resolve(), y trata los especificadores de módulos como URL reales, incluidas las cadenas de consulta y los fragmentos de URL.
En la práctica, esto significa que las importaciones relativas siguen las mismas reglas que new URL() y que las URL completas pueden utilizarse como especificadores de módulos. Además, los módulos que difieren en la cadena de consulta o en el fragmento posterior de la URL se convierten en módulos independientes, aunque apunten a la misma fuente. Por ello, dos versiones del mismo archivo pueden tener estados de nivel superior separados, mientras que reutilizar el mismo especificador devuelve la misma instancia del módulo.
El registro también admite la validación correcta de los atributos de importación. El tipo json está disponible actualmente, mientras que tipos como text y bytes están definidos, pero se rechazan con un mensaje claro porque todavía no se han activado. Asimismo, los atributos desconocidos ya no se ignoran silenciosamente, sino que provocan un error explícito.
Mayor compatibilidad con Node.js
require() sigue las reglas de Node.js para require(esm) al cargar un módulo ESM. Si el módulo exporta un valor con el nombre module.exports, se devuelve ese valor; de lo contrario, la llamada devuelve el objeto de espacio de nombres del módulo. Se exceptúan los módulos integrados node: de workerd, que devuelven la interfaz CommonJS esperada en lugar de obligar al desarrollador a acceder a una propiedad default.
Existe una limitación importante: require() no puede cargar un módulo que contenga, o dependa de un módulo que contenga, top-level await, porque require debe devolver el resultado de forma síncrona. En este caso, Workers genera un error y import(), que es asíncrono, constituye la vía adecuada. Esta regla sigue siendo válida incluso si el propio módulo se ha cargado previamente mediante import().
La nueva versión también unifica las categorías de errores y el formato de sus mensajes independientemente del método de carga, ya sea mediante importación estática, import() dinámico o require(). Si no se encuentra el módulo, se devuelve un error normal, mientras que un especificador que no pueda analizarse como URL provoca un TypeError. Esta coherencia resulta útil para los desarrolladores que crean cargadores personalizados o lógica de reintento.
¿Qué cambia en la práctica para los desarrolladores?
Las herramientas de Workers, como Wrangler, solían reunir la mayoría de los archivos de la aplicación y sus dependencias en un único módulo mediante esbuild, lo que reducía el tamaño del grafo que debía procesar el entorno de ejecución. En cambio, el uso del plugin de Cloudflare para Vite se basa en Rolldown en Vite 8 para producir un módulo de entrada y módulos adicionales al dividir el código, como los módulos cargados dinámicamente.
El nuevo registro permite que las herramientas de compilación realicen menos transformaciones y dependan más del entorno de ejecución para resolver los módulos. Esto es especialmente importante al desplegar aplicaciones como varios módulos, utilizar la opción --no-bundle o trabajar con archivos Wasm, de texto y binarios como archivos independientes en lugar de incluirlos en un único paquete.
La nueva implementación también pospone la compilación hasta que el módulo se importa por primera vez, ya sea mediante una importación estática o dinámica, y permite compartir cachés de código entre varias instancias de V8 isolate que ejecutan el mismo Worker. Según Cloudflare, esto aborda parte de la compilación repetida y de la existencia de varias copias del código fuente en memoria en la implementación anterior.
Limitaciones y aspectos que deben tenerse en cuenta
A pesar de estos cambios, el nuevo registro no se activará automáticamente para ningún Worker, antiguo o nuevo, independientemente de la fecha de compatibilidad utilizada. La opción debe añadirse explícitamente. Cloudflare también ha mantenido en uso la implementación anterior y afirma que los Workers desplegados seguirán funcionando como hasta ahora.
La nueva implementación también admite la importación en fase de origen de módulos WebAssembly, lo que devuelve directamente un objeto WebAssembly.Module, pero esta función actualmente solo funciona con WebAssembly; cualquier otro tipo provoca un error de sintaxis. Por tanto, la actualización no representa una transición integral y sin restricciones para todas las rutas de carga de módulos, sino que ofrece una base más compatible que los desarrolladores pueden probar gradualmente.
Lectura editorial: el valor real del cambio reside en trasladar Workers de una simulación parcial del comportamiento de los módulos a un modelo más cercano a los estándares de JavaScript y Node.js, lo que podría reducir las transformaciones impuestas por las herramientas de compilación y hacer que las aplicaciones multimódulo sean más portables. Sin embargo, su efecto final dependerá de las pruebas que los desarrolladores realicen con la nueva opción, especialmente con dependencias que utilicen require(), top-level await o atributos de importación. Además, el hecho de que no se active de forma predeterminada significa que la compatibilidad mejorada todavía no se convierte en el comportamiento general de todas las aplicaciones.