Back

Singletons in JavaScript: Nützliches Werkzeug oder versteckte Falle?

Singletons in JavaScript: Nützliches Werkzeug oder versteckte Falle?

Sie haben ein Modul geschrieben, das einen konfigurierten Logger oder eine API-Client-Instanz exportiert. Jede Datei importiert es, und Sie gehen davon aus, dass genau eine Instanz in Ihrer gesamten Anwendung läuft. Dann beginnen Ihre Tests, Zustand zwischen Durchläufen zu übertragen. Oder Ihre Microfrontend-Architektur hat plötzlich zwei Kopien Ihres „Singletons”, die miteinander konkurrieren. Was ist passiert?

Das JavaScript-Singleton-Pattern funktioniert nicht so, wie es die klassische Design-Pattern-Theorie nahelegt. Zu verstehen, wo ES-Modul-Singletons tatsächlich existieren – und wo sie versagen – erspart Ihnen Debugging-Sessions, die sich anfühlen, als würden Sie Geister jagen.

Wichtigste Erkenntnisse

  • ES-Modul-Singletons werden pro Modulgraph und Runtime gecacht, nicht global über Ihr gesamtes System hinweg
  • Singletons funktionieren gut für unveränderliche Daten und zustandslose Operationen wie Logging-Utilities und schreibgeschützte Konfiguration
  • Veränderbarer Zustand in Singletons wird gefährlich bei Server-Side Rendering, Testumgebungen und Microfrontend-Architekturen
  • Bevor Sie ein Singleton verwenden, überlegen Sie, ob Ihr Code in mehreren Bundles, Workern oder Server-Requests läuft und ob Tests die Instanz zurücksetzen müssen

Was „Singleton” in modernem JavaScript tatsächlich bedeutet

Vergessen Sie für einen Moment die Gang-of-Four-Definition. In modernem JavaScript ist ein Singleton typischerweise einfach ein Modul, das ein bereits instanziiertes Objekt exportiert:

// logger.js
class Logger {
  log(message) {
    console.log(`[${Date.now()}] ${message}`)
  }
}

export const logger = new Logger()

Wenn Sie logger aus mehreren Dateien importieren, erhalten Sie dieselbe Instanz – nicht wegen cleverer Konstruktor-Tricks, sondern weil ES-Module pro Modulgraph und Runtime gecacht werden. Das Modul wird einmal ausgeführt, die Instanz wird einmal erstellt, und jeder Import erhält eine Referenz auf dasselbe Objekt.

Das ist die Grundlage von ES-Modul-Singletons. Es ist einfach und oft genau das, was Sie brauchen.

Die Annahme, die bricht

Hier zeigen sich Singleton-Fallstricke in JavaScript: Diese „einzelne Instanz” ist auf einen bestimmten Modulgraphen innerhalb einer bestimmten JavaScript-Runtime beschränkt. Sie ist nicht magisch global über Ihr gesamtes System hinweg.

Die Annahme bricht in mehreren realen Szenarien:

Mehrere Bundles oder doppelte Packages. Wenn Ihr Monorepo zwei Packages hat, die jeweils ihre eigene Kopie einer Abhängigkeit bündeln, erhalten Sie zwei separate Modulgraphen. Zwei „Singletons”. Ihr gemeinsamer Zustand ist nicht mehr gemeinsam.

Test-Runner. Jest, Vitest und ähnliche Tools setzen oft Modul-Caches zwischen Testdateien zurück oder verwenden Worker-Prozesse. Ihr Singleton aus einer Testdatei ist möglicherweise nicht dieselbe Instanz in einer anderen.

Microfrontends. Jedes unabhängig deployete Frontend hat typischerweise seine eigene JavaScript-Runtime. Ein Singleton in einem Microfrontend ist für ein anderes unsichtbar, es sei denn, Instanzen werden explizit über den Build oder die Runtime geteilt.

Web Workers und Service Workers. Diese laufen in separaten JavaScript-Kontexten. Ein in einem Worker importiertes Modul ist eine völlig andere Instanz als dasselbe Modul in Ihrem Main-Thread.

Server-Runtimes mit Request-Isolation. In Frameworks wie Next.js oder Nuxt, die auf dem Server laufen, kann ein Singleton je nach Runtime und Deployment-Modell über mehrere Benutzeranfragen hinweg bestehen bleiben, was das Risiko von Datenlecks birgt, wenn es veränderbaren, anfragespezifischen Zustand hält.

Wo Singletons gut funktionieren

Trotz dieser Fallstricke bleiben ES-Modul-Singletons für bestimmte Frontend-Utility- und Koordinationsmuster wirklich nützlich:

  • Logging-Utilities. Ein gemeinsamer Logger mit konsistenter Formatierung schadet nicht, wenn er dupliziert wird, und hält keinen sensiblen anfragespezifischen Zustand.
  • Konfigurations-Snapshots. Schreibgeschützte Konfiguration, die beim Start geladen wird, funktioniert als Singleton einwandfrei. Das Schlüsselwort ist schreibgeschützt.
  • Zustandslose Utilities. Hilfsfunktionen oder Klassen, die keinen veränderbaren Zustand zwischen Aufrufen halten, sind sichere Kandidaten.

Der gemeinsame Nenner: Diese Singletons halten entweder unveränderliche Daten oder führen zustandslose Operationen aus.

Wo Singletons zur Last werden

Veränderbarer Zustand ist der Punkt, an dem es gefährlich wird. Betrachten Sie:

  • Benutzersitzungsdaten. Bei Server-Side Rendering kann ein Singleton mit Benutzerinformationen zwischen Anfragen lecken.
  • Anfragespezifische Caches. Daten, die pro Anfrage zurückgesetzt werden sollten, bleiben fälschlicherweise bestehen.
  • Gemeinsame veränderbare Konfiguration. Ein Teil Ihrer App modifiziert Einstellungen und beeinflusst unerwartet einen anderen.

Modernes Tooling verstärkt diese Probleme. In React 18+ ruft der Strict Mode absichtlich Renders und bestimmte Effects in der Entwicklung doppelt auf, wodurch Singleton-Zustand aufgedeckt wird, der nicht richtig isoliert ist. Hot Module Replacement in Vite oder webpack kann Singleton-Zustand über Code-Änderungen hinweg bewahren und subtile Bugs erzeugen, bei denen Ihr „frischer” Code mit veralteten Daten arbeitet.

Eine praktische Haltung

Das JavaScript-Singleton-Pattern ist nicht grundsätzlich schlecht – es ist nur eingeschränkter, als viele Entwickler annehmen. Bevor Sie zu einer Instanz auf Modulebene greifen, fragen Sie:

  1. Ist dieser Zustand wirklich unveränderlich oder zustandslos?
  2. Könnte dieser Code in mehreren Bundles, Workern oder Server-Requests laufen?
  3. Müssen meine Tests diese Instanz zurücksetzen oder mocken?

Wenn Sie bei Fragen 2 oder 3 mit veränderbarem Zustand „Ja” antworten, ziehen Sie Alternativen in Betracht: Factory-Funktionen, Dependency Injection oder Framework-spezifische Patterns wie React Context oder anfragespezifische Services.

Fazit

Singletons sind ein nützliches Werkzeug, wenn Sie ihren tatsächlichen Geltungsbereich verstehen. Sie werden zur versteckten Falle, wenn Sie annehmen, dass „einzelne Instanz” etwas bedeutet, das die Runtime nie versprochen hat. Bleiben Sie bei unveränderlichen oder zustandslosen Daten für Ihre Instanzen auf Modulebene und greifen Sie zu Dependency Injection oder Factory-Patterns, wenn Sie eine ordnungsgemäße Isolation über Tests, Anfragen oder Runtime-Grenzen hinweg benötigen.

FAQs

Jest setzt oft Modul-Caches zwischen Testdateien zurück oder führt Tests in isolierten Worker-Prozessen aus, wodurch neue Modulgraphen erstellt und Ihr Singleton neu instanziiert wird. Verwenden Sie jest.resetModules() oder jest.isolateModules() wo angemessen, oder vermeiden Sie veränderbaren Zustand auf Modulebene, indem Sie Abhängigkeiten injizieren.

Nein. Web Workers laufen in separaten JavaScript-Kontexten mit ihren eigenen Modulgraphen. Ein in einem Worker importiertes Modul ist eine völlig andere Instanz als dasselbe Modul in Ihrem Main-Thread. Um Zustand zu teilen, müssen Sie explizit Daten zwischen Kontexten über postMessage oder SharedArrayBuffer übergeben.

Nur für unveränderliche oder zustandslose Daten. Singletons auf dem Server können je nach Runtime und Deployment-Modell über mehrere Benutzeranfragen hinweg bestehen bleiben und potenziell sensible Daten zwischen Benutzern leaken. Für anfragespezifischen Zustand wie Benutzersitzungen oder Caches verwenden Sie anfragespezifische Patterns, die von Ihrem Framework bereitgestellt werden, anstelle von Instanzen auf Modulebene.

Factory-Funktionen ermöglichen es Ihnen, bei Bedarf neue Instanzen zu erstellen. Dependency Injection übergibt Instanzen explizit und erleichtert das Testen. Framework-spezifische Lösungen wie React Context bieten scoped State Management. Für Server-Anwendungen sorgen anfragespezifische Services für ordnungsgemäße Isolation zwischen Anfragen bei gleichzeitiger Beibehaltung der Bequemlichkeit gemeinsamer Utilities.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay