From 77734b6da20ac2ece4f6f0b6f150463981eb57a6 Mon Sep 17 00:00:00 2001 From: DeeCee03 Date: Sat, 29 Aug 2026 16:31:25 +0200 Subject: [PATCH] Server: Assets werden nachgefragt statt jedes Mal neu geladen Auf allem stand no-store -- im Server und noch einmal in preloadAssets(). Der Browser durfte also nichts behalten, und jeder Reload lud die vollen Assets neu: 3 MB Screenshots, 3,3 MB Mockups, Schriften, Bibliotheken. Am schlimmsten in uebersicht.html. Die laedt die sechs Projekte als iframes, und weil keiner etwas behalten durfte, holte sich jeder iframe dieselben Dateien noch einmal: 38,1 MB in 435 Anfragen. uebersicht.html erster Aufruf 38,1 MB -> 2,2 MB uebersicht.html Reload 32,8 MB -> 1,9 MB post6.html Reload 5,4 MB -> 0,3 MB Die Absicht hinter no-store war richtig: ein Bild, das unter demselben Namen ausgetauscht wird, muss sofort ankommen. no-cache leistet das billiger -- der Browser behaelt seine Kopie, fragt aber vor jeder Benutzung nach. Nachgeprueft: unveraendert antwortet der Server mit 304 und 0 Bytes, nach einem touch mit 200 und der vollen Datei. Seite, Stand und Auskuenfte (/bilder, /state) bleiben bei no-store. Die sind klein und duerfen nie aus einer Kopie kommen. Die eine Zeile stand in allen sieben Projektdateien. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv --- README.md | 27 +++++++++++++++++++++++++++ post1.html | 10 ++++++++-- post2-alt.html | 10 ++++++++-- post2.html | 10 ++++++++-- post3.html | 10 ++++++++-- post4.html | 10 ++++++++-- post5.html | 10 ++++++++-- post6.html | 13 +++++++++++-- serve.py | 31 +++++++++++++++++++++++++------ 9 files changed, 111 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index fbd1471..adbbd31 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,33 @@ serve.py Statischer Server + /save, /state, /upload, /bilder, /anim rules.md Gestaltungsregeln, Farbwerte, Export-Wissen ``` +### Assets werden nachgefragt, nicht neu geladen + +Ein Bild, das unter demselben Namen ausgetauscht wird, muss sofort ankommen — +sonst arbeitet man gegen eine alte Kopie. Dafür stand auf allem `no-store`: +im Server und noch einmal in `preloadAssets()`. Der Browser durfte nichts +behalten. + +Das Ziel war richtig, der Preis zu hoch. Jeder Reload lud alles neu, und weil +`uebersicht.html` die sechs Projekte als iframes lädt und jeder iframe für +sich vorlud, kam die Seite auf **38 MB in 435 Anfragen**. + +`no-cache` leistet dasselbe billiger: der Browser behält seine Kopie, fragt +aber vor jeder Benutzung nach. Eine ausgetauschte Datei kommt genauso sofort +an, eine unveränderte kostet ein 304 ohne Rumpf — und die sechs iframes teilen +sich eine Kopie statt sechs zu ziehen. + +| | vorher | jetzt | +|---|---|---| +| `uebersicht.html`, erster Aufruf | 38,1 MB | 2,2 MB | +| `uebersicht.html`, Reload | 32,8 MB | 1,9 MB | +| `post6.html`, Reload | 5,4 MB | 0,3 MB | + +Nachgeprüft ist beides: unverändert antwortet der Server mit `304` und +0 Bytes, nach einem `touch` mit `200` und der vollen Datei. **Seite, Stand und +Auskünfte (`/bilder`, `/state`) bleiben bei `no-store`** — die sind klein und +dürfen nie aus einer Kopie kommen. + ## Regeln **`rules.md` ist die Arbeitsgrundlage.** Farbwerte, Geometrie, Typografie, diff --git a/post1.html b/post1.html index 6d3ed83..b3fe51a 100644 --- a/post1.html +++ b/post1.html @@ -1945,7 +1945,13 @@ if (location.search.indexOf("nur=streifen") >= 0) const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + /* no-cache statt no-store: der Browser behaelt seine Kopie und fragt + nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso + sofort an, ein unveraendertes kostet ein 304 statt seiner vollen + Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber + uebersicht.html, die sechs Projekte als iframes laedt, waren das + 38 MB und 21 Sekunden. */ + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -2511,7 +2517,7 @@ if (location.search.indexOf("nur=streifen") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/post2-alt.html b/post2-alt.html index 4e2b451..e29432b 100644 --- a/post2-alt.html +++ b/post2-alt.html @@ -3891,7 +3891,13 @@ if (location.search.indexOf("nur=anim") >= 0) const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + /* no-cache statt no-store: der Browser behaelt seine Kopie und fragt + nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso + sofort an, ein unveraendertes kostet ein 304 statt seiner vollen + Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber + uebersicht.html, die sechs Projekte als iframes laedt, waren das + 38 MB und 21 Sekunden. */ + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -4469,7 +4475,7 @@ if (location.search.indexOf("nur=anim") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/post2.html b/post2.html index 7f58e04..90d1f5d 100644 --- a/post2.html +++ b/post2.html @@ -3519,7 +3519,13 @@ if (location.search.indexOf("nur=anim") >= 0) const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + /* no-cache statt no-store: der Browser behaelt seine Kopie und fragt + nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso + sofort an, ein unveraendertes kostet ein 304 statt seiner vollen + Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber + uebersicht.html, die sechs Projekte als iframes laedt, waren das + 38 MB und 21 Sekunden. */ + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -4090,7 +4096,7 @@ if (location.search.indexOf("nur=anim") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/post3.html b/post3.html index b49c5ee..fd13949 100644 --- a/post3.html +++ b/post3.html @@ -3191,7 +3191,13 @@ if (location.search.indexOf("nur=anim") >= 0) const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + /* no-cache statt no-store: der Browser behaelt seine Kopie und fragt + nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso + sofort an, ein unveraendertes kostet ein 304 statt seiner vollen + Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber + uebersicht.html, die sechs Projekte als iframes laedt, waren das + 38 MB und 21 Sekunden. */ + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -3816,7 +3822,7 @@ if (location.search.indexOf("nur=anim") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/post4.html b/post4.html index 1480d48..5a62710 100644 --- a/post4.html +++ b/post4.html @@ -4025,7 +4025,13 @@ if (location.search.indexOf("nur=anim") >= 0) const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + /* no-cache statt no-store: der Browser behaelt seine Kopie und fragt + nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso + sofort an, ein unveraendertes kostet ein 304 statt seiner vollen + Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber + uebersicht.html, die sechs Projekte als iframes laedt, waren das + 38 MB und 21 Sekunden. */ + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -4603,7 +4609,7 @@ if (location.search.indexOf("nur=anim") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/post5.html b/post5.html index a30345d..1669db0 100644 --- a/post5.html +++ b/post5.html @@ -4267,7 +4267,13 @@ if (location.search.indexOf("nur=anim") >= 0) const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + /* no-cache statt no-store: der Browser behaelt seine Kopie und fragt + nur nach, ob sie noch stimmt. Ein ausgetauschtes Bild kommt genauso + sofort an, ein unveraendertes kostet ein 304 statt seiner vollen + Groesse. Mit no-store lud jeder Reload alle Assets neu -- ueber + uebersicht.html, die sechs Projekte als iframes laedt, waren das + 38 MB und 21 Sekunden. */ + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -4845,7 +4851,7 @@ if (location.search.indexOf("nur=anim") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/post6.html b/post6.html index afe393f..c2ae8d8 100644 --- a/post6.html +++ b/post6.html @@ -5001,8 +5001,17 @@ if (location.search.indexOf("nur=anim") >= 0) // Optional: the layout falls back to a hint if the file is not there yet. const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); + /* no-cache und nicht no-store: der Browser darf seine Kopie behalten, + muss aber vor jeder Benutzung nachfragen. Eine ausgetauschte Datei + kommt damit genauso sofort an wie vorher, eine unveraenderte kostet + ein 304 ohne Rumpf statt ihrer vollen Groesse. + + Mit no-store lud jeder Reload rund 6 MB neu -- 3 MB Screenshots, + 3,3 MB Mockups -- obwohl sich nichts davon geaendert hatte. Gemessen + an 61 Anfragen je Aufruf. Die Auskuenfte (/bilder) und der Stand + bleiben bei no-store: die sind klein und duerfen nie alt sein. */ for (const path of [...new Set(paths)]) { - const res = await fetch(path, { cache: "no-store" }).catch(() => null); + const res = await fetch(path, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) { if (optional.has(path)) continue; throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff")); @@ -5580,7 +5589,7 @@ if (location.search.indexOf("nur=anim") >= 0) // Eine Datei einlesen, damit sie sofort im Canvas und im Export vorliegt. async function bildBereitstellen(pfad) { if (assets[pfad]) return true; - const res = await fetch(pfad, { cache: "no-store" }).catch(() => null); + const res = await fetch(pfad, { cache: "no-cache" }).catch(() => null); if (!res || !res.ok) return false; const blob = await res.blob(); assets[pfad] = await new Promise((ok, fail) => { diff --git a/serve.py b/serve.py index 88f1bb8..500249e 100755 --- a/serve.py +++ b/serve.py @@ -267,12 +267,31 @@ class Handler(http.server.SimpleHTTPRequestHandler): self.wfile.write(payload) def end_headers(self): - # Werkzeug, kein Auslieferungsserver: Assets werden hier laufend - # ausgetauscht, und ein gecachter Screenshot unter gleichem Dateinamen - # kostet mehr Zeit als jeder erneute Ladevorgang. - self.send_header("Cache-Control", "no-store, must-revalidate") - self.send_header("Pragma", "no-cache") - self.send_header("Expires", "0") + # Werkzeug, kein Auslieferungsserver: ein Asset, das unter demselben + # Namen ausgetauscht wird, muss sofort ankommen. + # + # Dafuer stand hier no-store auf allem. Das Ziel war richtig, der + # Preis zu hoch: no-store verbietet dem Browser die Kopie ueberhaupt, + # und damit lud jeder Reload die vollen rund 6 MB neu -- 3 MB + # Screenshots, 3,3 MB Mockups, die Schriften, die Bibliotheken. Am + # Ende wartete man beim Neuladen auf Dateien, die sich seit Wochen + # nicht geaendert hatten. + # + # no-cache leistet dasselbe billiger: der Browser darf die Kopie + # behalten, muss aber vor jeder Benutzung nachfragen. Eine + # ausgetauschte Datei kommt damit genauso sofort an, eine + # unveraenderte kostet ein 304 ohne Rumpf. Geprueft: der Handler + # beantwortet If-Modified-Since bereits mit 304 und 0 Bytes. + # + # Seite, Stand und Auskuenfte bleiben bei no-store. Die sind klein, + # und sie duerfen nie aus einer Kopie kommen. + pfad = urllib.parse.urlparse(self.path).path + if pfad.startswith(("/assets/", "/vendor/", "/animation/")): + self.send_header("Cache-Control", "no-cache") + else: + self.send_header("Cache-Control", "no-store, must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") super().end_headers() def log_message(self, fmt, *args):