Cloudflare hat das Modulregister in der Open-Source-Komponente workerd, der Kernkomponente der Workers-Laufzeitumgebung, neu geschrieben, um Geschwindigkeit und Standardkonformität zu verbessern und die Art und Weise, wie Module geladen und aufgelöst werden, stärker an das Verhalten von Node.js anzunähern. Dies geschieht gleichzeitig mit der standardmäßigen Aktivierung stabiler Node.js-APIs in Workers und der Möglichkeit, Anwendungen mit einer Größe von bis zu 64 MiB auf allen Tarifen bereitzustellen, nachdem das Limit für die Größe komprimierter Pakete entfernt wurde.
Die wichtigste Änderung betrifft jedoch nicht nur die Anzahl der verfügbaren APIs. Node.js-Anwendungen hängen auch davon ab, wie die Laufzeit Module identifiziert, lädt und zwischenspeichert. Dazu gehören ESM-, CommonJS- und WebAssembly-Module – Aufgaben, die das Modulregister innerhalb von workerd übernimmt.
Was hat sich im neuen Register geändert?
Entwickler können die neue Implementierung testen, indem sie das Flag new_module_registry zu den Worker-Einstellungen hinzufügen. Bei Aktivierung unterstützt Workers sowohl import.meta.url als auch import.meta.main und import.meta.resolve(). Außerdem behandelt es Modulspezifierer als echte URLs, einschließlich Query-Strings und URL-Fragmente.
In der Praxis bedeutet das, dass relative Importe denselben Regeln wie new URL() folgen und vollständige URLs als Modulspezifierer verwendet werden können. Module, die sich in ihrem Query-String oder URL-Fragment unterscheiden, werden ebenfalls zu eigenständigen Modulen, selbst wenn sie auf dieselbe Quelle verweisen. Dadurch können zwei Versionen derselben Datei einen getrennten Top-Level-Zustand besitzen, während die erneute Verwendung desselben Spezifierers dieselbe Modulinstanz zurückgibt.
Das Register unterstützt außerdem die korrekte Validierung von Importattributen. Der Typ json ist derzeit verfügbar, während Typen wie text und bytes zwar definiert, aber mit einer eindeutigen Fehlermeldung abgelehnt werden, weil sie noch nicht aktiviert wurden. Unbekannte Attribute werden ebenfalls nicht mehr stillschweigend ignoriert, sondern führen zu einem ausdrücklichen Fehler.
Größere Kompatibilität mit Node.js
Beim Laden eines ESM-Moduls folgt require() den Node.js-Regeln für require(esm). Wenn das Modul einen Wert mit dem Namen module.exports exportiert, wird dieser Wert zurückgegeben; andernfalls gibt der Aufruf das Namespace-Objekt des Moduls zurück. Eine Ausnahme bilden die in workerd integrierten node:-Module: Sie geben die erwartete CommonJS-Schnittstelle zurück, statt den Entwickler zum Zugriff auf die Eigenschaft default zu zwingen.
Es gibt eine wichtige Einschränkung: require() kann kein Modul laden, das ein top-level await enthält oder von einem solchen Modul abhängt, da require das Ergebnis synchron zurückgeben muss. In diesem Fall löst Workers einen Fehler aus; der asynchrone Aufruf import() ist dann der geeignete Weg. Diese Regel gilt auch dann, wenn das Modul selbst zuvor über import() geladen wurde.
Die neue Version vereinheitlicht außerdem Fehlerklassen und die Formulierung ihrer Meldungen unabhängig von der Lademethode – sei es über einen statischen Import, einen dynamischen import() oder require(). Wenn ein Modul nicht gefunden wird, wird ein gewöhnlicher Fehler zurückgegeben, während ein Spezifierer, der sich nicht als URL analysieren lässt, einen TypeError verursacht. Diese Konsistenz ist für Entwickler hilfreich, die eigene Loader oder Wiederholungslogik erstellen.
Was ändert sich praktisch für Entwickler?
Tools für Workers, etwa Wrangler, bündelten bisher die meisten Anwendungsdateien und Abhängigkeiten mithilfe von esbuild in ein einziges Modul, wodurch die Größe des von der Laufzeit verarbeiteten Graphen reduziert wurde. Bei Verwendung des Cloudflare-Vite-Plugins setzt Vite 8 dagegen auf Rolldown, um ein Eingangsmodul und beim Aufteilen des Codes zusätzliche Module zu erzeugen, etwa dynamisch geladene Module.
Das neue Register eröffnet Bundling-Tools die Möglichkeit, weniger Transformationen vorzunehmen und sich bei der Modulauflösung stärker auf die Laufzeit zu verlassen. Das ist besonders wichtig, wenn Anwendungen als mehrere Module bereitgestellt werden, die Option --no-bundle verwendet wird oder Wasm-Dateien, Textdateien und Binärdateien als eigenständige Dateien behandelt werden sollen, statt sie in ein einziges Paket einzubetten.
Die neue Implementierung verschiebt außerdem die Kompilierung, bis das Modul erstmals importiert wird – unabhängig davon, ob der Import statisch oder dynamisch erfolgt. Zudem ermöglicht sie die gemeinsame Nutzung von Code-Caches zwischen mehreren V8-Isolates, die denselben Worker ausführen. Laut Cloudflare behebt dies einen Teil der wiederholten Kompilierung und der mehrfachen Speicherung der Quelle im Speicher, die bei der vorherigen Implementierung auftraten.
Einschränkungen und wichtige Hinweise
Trotz dieser Änderungen wird das neue Register für keinen Worker automatisch aktiviert, unabhängig davon, ob er alt oder neu ist und welches verwendete Kompatibilitätsdatum gilt. Das Flag muss ausdrücklich hinzugefügt werden. Cloudflare lässt die bisherige Implementierung ebenfalls weiter in Betrieb und erklärt, dass bereitgestellte Workers weiterhin wie bisher funktionieren werden.
Die neue Implementierung unterstützt außerdem den Import von WebAssembly-Modulen auf Quellenebene und gibt dabei direkt ein WebAssembly.Module-Objekt zurück. Diese Funktion arbeitet derzeit jedoch nur mit WebAssembly; jeder andere Typ führt zu einem Syntaxfehler. Die Aktualisierung stellt daher keinen uneingeschränkten Übergang für alle Modul-Ladepfade dar, sondern bietet eine kompatiblere Grundlage, die Entwickler schrittweise testen können.
Redaktionelle Einordnung: Der tatsächliche Wert der Änderung liegt darin, Workers von einer teilweisen Nachbildung des Modulverhaltens zu einem Modell zu führen, das stärker den JavaScript- und Node.js-Standards entspricht. Dadurch könnten die von Build-Tools auferlegten Transformationen reduziert und Anwendungen mit mehreren Modulen portabler werden. Die endgültigen Auswirkungen hängen jedoch davon ab, wie Entwickler das neue Flag testen, insbesondere bei Abhängigkeiten, die require(), top-level await oder Importattribute verwenden. Da es nicht standardmäßig aktiviert wird, ist die verbesserte Kompatibilität noch nicht das allgemeine Verhalten aller Anwendungen.