Seit einigen Jahren empfiehlt fast jeder Artikel über fortgeschrittene Caching-Ansätze die Verwendung der folgenden Techniken in der Produktion:
- Hinzufügen von Informationen zu der Version der darin enthaltenen Daten zu den Dateinamen (normalerweise in Form eines Hash der Daten in den Dateien).
- Festlegen von HTTP-Headern
Cache-Control: max-age
und Expires
, die die Caching-Zeit von Materialien steuern (wodurch die erneute Validierung der relevanten Materialien für Besucher, die zur Ressource zurückkehren, entfällt).

Alle mir bekannten Tools zum Erstellen von Projekten unterstützen das Hinzufügen von Inhalten zur Hash-Datei. Dies erfolgt mithilfe einer einfachen Konfigurationsregel (wie unten gezeigt):
filename: '[name]-[contenthash].js'
Diese weit verbreitete Unterstützung für diese Technologie hat dazu geführt, dass diese Praxis äußerst verbreitet geworden ist.
Experten für die Leistung von Webprojekten empfehlen außerdem die Verwendung von
Codetrennungstechniken . Diese Techniken ermöglichen das Aufteilen von JavaScript-Code in separate Bundles. Solche Bundles können vom Browser auf Anfrage des Browsers parallel oder sogar nur dann heruntergeladen werden, wenn sie erforderlich werden.
Einer der vielen Vorteile der Codetrennung, insbesondere im Zusammenhang mit erweiterten Caching-Techniken, besteht darin, dass Änderungen an einer separaten Quelldatei nicht den gesamten Cache des Bundles ungültig machen. Mit anderen Worten, wenn ein Sicherheitsupdate für das vom Entwickler "X" erstellte npm-Paket veröffentlicht wurde und der Inhalt von
node_modules
von Entwicklern fragmentiert wird, muss nur das Fragment
node_modules
, das die von "X" erstellten Pakete enthält.
Das Problem hierbei ist, dass wenn all dies kombiniert wird, dies selten zu einer Steigerung der Effizienz des langfristigen Daten-Caching führt.
In der Praxis führen Änderungen an einer der Quellcodedateien fast immer zur Ungültigmachung von mehr als einer Ausgabedatei des Paketassemblierungssystems. Dies liegt genau an der Tatsache, dass den Dateinamen Hashes hinzugefügt wurden, die die Versionen des Inhalts dieser Dateien widerspiegeln.
Problem bei der Versionierung des Dateinamens
Stellen Sie sich vor, Sie haben eine Website erstellt und bereitgestellt. Sie haben die Codeaufteilung verwendet. Daher wird der größte Teil des JavaScript-Codes Ihrer Website auf Anfrage geladen.
Im nächsten Abhängigkeitsdiagramm sehen Sie den Codebasis-Einstiegspunkt - das
dep1
von
main
sowie drei asynchron geladene Abhängigkeitsfragmente -
dep1
,
dep2
und
dep3
. Es gibt auch ein
vendor
, das alle Site-Abhängigkeiten von
node_modules
. Alle Dateinamen enthalten gemäß den Caching-Richtlinien Hashes des Inhalts dieser Dateien.
Typischer Abhängigkeitsbaum für JavaScript-ModuleDa
dep2
und
dep3
Module aus dem
vendor
Snippet importieren, werden wir im oberen Teil ihres vom Project
dep3
generierten Codes höchstwahrscheinlich Importbefehle finden, die ungefähr so aussehen:
import {...} from '/vendor-5e6f.mjs';
Lassen Sie uns nun darüber nachdenken, was passieren wird, wenn sich der Inhalt des
vendor
ändert.
In diesem Fall ändert sich auch der Hash im Namen der entsprechenden Datei. Und da sich der Link zum Namen dieser Datei in den Importbefehlen für
dep2
und
dep3
, müssen sich diese Importbefehle ändern:
-import {...} from '/vendor-5e6f.mjs'; +import {...} from '/vendor-d4a1.mjs';
Da diese Importbefehle jedoch Teil des Inhalts der
dep2
und
dep3
, bedeutet eine Änderung, dass sich auch der Hash des Inhalts der
dep2
und
dep3
Dateien
dep3
. Und das bedeutet, dass sich auch die Namen dieser Dateien ändern.
Aber das endet nicht dort. Da das
main
die
dep2
und
dep3
Fragmente importiert und sich ihre Dateinamen geändert haben, ändern sich auch die Importbefehle in
main
:
-import {...} from '/dep2-3c4d.mjs'; +import {...} from '/dep2-2be5.mjs'; -import {...} from '/dep3-d4e5.mjs'; +import {...} from '/dep3-3c6f.mjs';
Und schließlich muss sich auch der Name dieser Datei ändern, da sich der Inhalt der
main
geändert hat.
So sieht nun das Abhängigkeitsdiagramm aus.
Module im Abhängigkeitsbaum, die von einer einzelnen Änderung des Codes eines der Blattknoten des Baums betroffen sindDieses Beispiel zeigt, wie eine kleine Codeänderung in nur einer Datei zur Ungültigmachung des Caches von 80% der Bundle-Fragmente führte.
Zwar führen nicht alle Änderungen zu solch traurigen Konsequenzen (zum Beispiel führt die Ungültigmachung des Blattknotencaches zur Ungültigmachung des Cache aller Knoten bis zur Wurzel, aber die Ungültigmachung des Wurzelcaches führt in einer idealen Welt nicht zu einer kaskadierenden Ungültigkeit, die den Blattfang erreicht) Wir müssten uns nicht mit unnötigen Cache-Ungültigmachungen befassen.
Dies führt uns zu der folgenden Frage: "Ist es möglich, die Vorteile unveränderlicher Ressourcen und langfristiger Zwischenspeicherung zu nutzen, ohne unter kaskadierenden Cache-Invalidierungen zu leiden?"
Lösungsansätze
Das Problem mit den Hashes des Inhalts von Dateien in Dateinamen besteht aus technischer Sicht nicht darin, dass die Hashes in Namen sind. Es liegt in der Tatsache, dass diese Hashes in anderen Dateien erscheinen. Infolgedessen wird der Cache dieser Dateien deaktiviert, wenn die Hashes in den Namen der Dateien geändert werden, von denen sie abhängen.
Die Lösung für dieses Problem besteht darin, die Sprache des obigen Beispiels zu verwenden, um das Importieren des
vendor
durch die
dep2
und
dep3
ohne die Versionsinformationen der
vendor
anzugeben. Dabei müssen Sie sicherstellen, dass die heruntergeladene
vendor
korrekt ist, wobei die aktuellen Versionen von
dep2
und
dep3
.
Wie sich herausstellte, gibt es mehrere Möglichkeiten, um dieses Ziel zu erreichen:
- Karten importieren.
- Servicemitarbeiter.
- Native Skripte zum Laden von Ressourcen.
Betrachten Sie diese Mechanismen.
Ansatz 1: Karten importieren
Importzuordnungen sind die einfachste Lösung für die Kaskadierung der Cache-Ungültigmachung. Darüber hinaus ist dieser Mechanismus am einfachsten zu implementieren. Leider wird es nur in Chrome unterstützt (diese Funktion muss außerdem explizit
aktiviert sein ).
Trotzdem möchte ich mit der Geschichte über Importkarten beginnen, da ich sicher bin, dass diese Entscheidung in Zukunft die häufigste sein wird. Darüber hinaus wird die Beschreibung der Arbeit mit Importkarten dazu beitragen, die Merkmale anderer Ansätze zur Lösung unseres Problems zu erläutern.
Die Verwendung von Importzuordnungen zur Verhinderung einer kaskadierenden Cache-Ungültigmachung besteht aus drei Schritten.
▍Schritt 1
Sie müssen den Bundler so konfigurieren, dass beim Erstellen des Projekts keine Hashes des Inhalts von Dateien in ihren Namen enthalten sind.
Wenn Sie ein Projekt zusammenstellen, dessen Module im Diagramm aus dem vorherigen Beispiel dargestellt sind, ohne Hashes ihres Inhalts in die Dateinamen aufzunehmen, sehen die Dateien im Projektausgabeverzeichnis folgendermaßen aus:
dep1.mjs dep2.mjs dep3.mjs main.mjs vendor.mjs
Importbefehle in den entsprechenden Modulen enthalten auch keine Hashes:
import {...} from '/vendor.mjs';
▍Schritt 2
Sie müssen ein Tool wie
rev-hash verwenden und damit eine Kopie jeder Datei erstellen, deren Name mit einem Hash versehen ist, der die Version des Inhalts angibt.
Nachdem dieser Teil der Arbeit erledigt ist, sollte der Inhalt des Ausgabeverzeichnisses ungefähr so aussehen wie der unten gezeigte (beachten Sie, dass es jetzt zwei Optionen für jede Datei gibt):
dep1-b2c3.mjs", dep1.mjs dep2-3c4d.mjs", dep2.mjs dep3-d4e5.mjs", dep3.mjs main-1a2b.mjs", main.mjs vendor-5e6f.mjs", vendor.mjs
▍Schritt 3
Sie müssen ein JSON-Objekt erstellen, das Informationen über die Entsprechung jeder Datei speichert, in deren Namen kein Hash vorhanden ist, zu jeder Datei, in deren Namen ein Hash vorhanden ist. Dieses Objekt muss zu HTML-Vorlagen hinzugefügt werden.
Dieses JSON-Objekt ist eine Importzuordnung. So könnte es aussehen:
<script type="importmap"> { "imports": { "/main.mjs": "/main-1a2b.mjs", "/dep1.mjs": "/dep1-b2c3.mjs", "/dep2.mjs": "/dep2-3c4d.mjs", "/dep3.mjs": "/dep3-d4e5.mjs", "/vendor.mjs": "/vendor-5e6f.mjs", } } </script>
Wenn der Browser danach den Importbefehl der Datei an der Adresse sieht, die einem der Schlüssel der Importzuordnung entspricht, importiert der Browser die Datei, die dem Schlüsselwert entspricht.
Wenn Sie diese
/vendor.mjs
als Beispiel verwenden, können Sie feststellen, dass der Importbefehl, der auf die Datei
/vendor.mjs
verweist, die Datei
/vendor.mjs
tatsächlich
/vendor-5e6f.mjs
und
/vendor-5e6f.mjs
:
Dies bedeutet, dass der Quellcode der Module leicht auf die Dateinamen von Modulen verweisen kann, die keine Hashes enthalten, und der Browser immer Dateien herunterlädt, deren Namen Informationen über Versionen ihres Inhalts enthalten. Und da der Quellcode der Module keine Hashes enthält (sie sind nur in der Importzuordnung vorhanden), führen Änderungen an diesen Hashes nicht zur Ungültigmachung anderer Module als derjenigen, deren Inhalt sich tatsächlich geändert hat.
Vielleicht fragen Sie sich jetzt, warum ich eine Kopie jeder Datei erstellt habe, anstatt nur die Dateien umzubenennen. Dies ist erforderlich, um Browser zu unterstützen, die nicht mit Importkarten arbeiten können. Im vorherigen Beispiel sehen solche Browser nur die Datei
/vendor.mjs
und laden diese Datei einfach herunter, wobei sie wie
/vendor.mjs
und auf ähnliche Konstrukte stoßen. Infolgedessen stellt sich heraus, dass beide Dateien auf dem Server vorhanden sein müssen.
Wenn Sie Importzuordnungen in Aktion sehen möchten, finden Sie hier eine
Reihe von Beispielen , die alle Möglichkeiten zur Lösung des in diesem Artikel gezeigten Problems der Kaskadierung der Cache-Ungültigkeit veranschaulichen. Sehen Sie sich auch die
Konfiguration der Projektassembly an , falls Sie erfahren möchten, wie ich die Importzuordnung und die Versions-Hashes für jede Datei generiert habe.
Fortsetzung folgt…
Liebe Leser! Ist Ihnen eine kaskadierende Cache-Ungültigmachung bekannt?
