Weiter durch die rote Liste. Drei der vier Befunde zeigten auf
gesunden Code; die Pruefungen selbst waren seit einem Umbau nicht
nachgezogen worden.
pruef-schritt (2 rot -> 71 gruen statt 47)
Meldete "1 von 6 Kategorien". Die Karte geht seit dem 01.10.2026
gefiltert auf ("zugetragen"), Bloecke ohne solche Punkte werden gar
nicht gezeichnet. Die Pruefung zaehlte danach alle. Schlimmer: Weil
dann kein Block mehr zugeklappt war, lief der Klick auf einen
zugeklappten 30 Sekunden in die Frist und riss den Lauf mit -- 24 von
71 Pruefungen liefen nie. Jetzt wird erst die Vorgabe geprueft (sie
ist eine Zusage), dann auf "Alle" gestellt.
pruef-glocke (1 rot -> 34 gruen)
Meldete "auf der Anmeldeseite steht der Knopf". Gemessen landete sie
auf start.html: Dieselbe Seite war eine Zeile zuvor angemeldet durch
zwanzig Arbeitsseiten gelaufen, und angemeldet fuehrt /workspace/
nicht zur Wand. Jetzt eigener Kontext ohne Keks -- und davor die
Zeile, dass die Wand wirklich erreicht wurde.
pruef-zeichen (1 rot -> 8 gruen)
Meldete content: "·" als Kodierungstruemmer. Die Bytes sind C2 B7,
also korrektes UTF-8; der Mittelpunkt ist ein begruendeter Trennpunkt
(seit 03.10.). Die Regel stammt aus einer Zeit, in der sie selbst
festhielt: "KEINE einzige benutzt ein Zeichen aus diesem Block."
Jetzt eine benannte Ausnahme MIT Veraltungsschutz: Jedes Zeichen
darin muss wirklich vorkommen, sonst wird die Ausnahme rot.
pruef-tempo-workspace (1 rot -> 9 gruen) -- DER ECHTE BEFUND
uebersicht.html 959 KB, bereich.html?b=live 1094 KB bei einer Grenze
von 900. Die Pruefung sagt jetzt auch, WORAN es liegt, und die
Antwort war eindeutig: ZWEI Buehnenbilder auf einer Seite, die eines
zeigt (281 KB studio + 262 KB portal).
Ursache: start.css traegt an body.start::before einen Rueckfall
(studio), und kopf.js setzt `data-buehne` erst, wenn die Seite steht.
Bis dahin ist der Rueckfall geholt -- auf 43 von 45 Seiten.
Jetzt steht die Szene fest im <body>, erzeugt aus bereiche.js
(tools/buehne-festschreiben.mjs, 20 Seiten). bereich.html bedient
fuenf Bretter und kann das nicht; sie bekommt ein kleines Skript
gleich hinter <body>, das die Szene aus `?b=` setzt, bevor gezeichnet
wird. Gemessen: 1094 -> 879 KB, und die Grenze haelt wieder.
DIE ZUORDNUNG STEHT DAMIT AN ZWEI STELLEN. Das ist nur vertretbar,
weil sie nicht auseinanderlaufen kann: pruef-buehnen-fest.mjs (neu,
13 Pruefungen) haelt HTML gegen bereiche.js, prueft die Lage des
Skripts und dass jede genannte Szene eine Regel hat.
Dabei gefunden und berichtigt -- zweimal an mir selbst: Der erste
Entwurf verglich Szenen mit Dateinamen und meldete teamlage.html
("eingang") als Fehler; die Szene gibt es, ihre Regel zeigt nur auf
ein crew-Bild. Und der Lage-Anker suchte `data-buehne`, das Skript
schreibt `dataset.buehne` -- indexOf gab -1, die Rechnung wurde
negativ und meldete "-2276 Zeichen dahinter".
Gegenproben gefahren: Szene verschoben -> rot mit Abstand; Ausnahme
verwaist -> rot; erfundene Szene -> erkannt.
Dazu gruen: css-klassen, workspace-seiten, neue-seiten, haus-trennung,
kopfleiste-farbe.
Co-Authored-By: Claude Opus 5 <[email protected]>
530 lines
25 KiB
JavaScript
530 lines
25 KiB
JavaScript
/* WIE SCHNELL IST DER WORKSPACE WIRKLICH?
|
|
|
|
Auftrag vom 04.09.2026 (vollstaendiger Audit), Abschnitt 11:
|
|
unnoetig grosse Bilder, unnoetiges JavaScript/CSS, Render-Blockaden,
|
|
fehlendes Lazy Loading, unnoetige und doppelte Anfragen, Layout Shifts.
|
|
|
|
WARUM DAS NICHT SCHON GEMESSEN WURDE. Es gibt pruef-tempo.mjs -- die
|
|
misst die OEFFENTLICHE Seite. Der Workspace hat andere Verhaeltnisse:
|
|
Er laedt auf jeder Seite dieselbe grosse Stilvorlage, holt seine Daten
|
|
erst per Schnittstelle nach, und niemand hat bisher gezaehlt, wie oft
|
|
dabei dieselbe Adresse zweimal abgefragt wird.
|
|
|
|
GEMESSEN WIRD AM ECHTEN BROWSER, ueber alle Seiten:
|
|
1. Wie viele Anfragen, wie viele Bytes?
|
|
2. Wird eine Adresse doppelt geholt? (Das ist reine Verschwendung
|
|
und meist ein Zeichen dafuer, dass zwei Bausteine dasselbe tun.)
|
|
3. Wie lange bis die Seite steht?
|
|
4. Springt das Layout nach dem Laden noch? (Layout Shift -- der
|
|
Grund, warum man auf den falschen Knopf drueckt.)
|
|
5. Laufen Bilder ohne Groessenangabe? (Genau die verursachen die
|
|
Spruenge.)
|
|
|
|
DIE GRENZEN sind bewusst grosszuegig gesetzt. Diese Pruefung soll
|
|
nicht bei jedem zusaetzlichen Kilobyte rot werden -- sie soll den Tag
|
|
finden, an dem etwas aus dem Ruder laeuft. Eine Warnung, die staendig
|
|
kommt, wird abgeschaltet.
|
|
|
|
GEGENPROBE am Ende: eine kuenstlich erzeugte Doppelanfrage und ein
|
|
kuenstlicher Layout-Sprung muessen gefunden werden. */
|
|
|
|
import { gzipSync } from "node:zlib";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { heuteLokal } from "./helfer-zeit.mjs";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-tempo-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
/* Der Port muss frei sein -- sonst wird gegen einen fremden Server
|
|
gemessen. Begruendung in helfer-port.mjs. */
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Tempopruefung");
|
|
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
|
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
|
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
|
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
|
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
|
notbremse(480_000, "pruef-tempo-workspace");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0;
|
|
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ---- Bestand ------------------------------------------------------------ */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
/* ORTSZEIT, nicht UTC -- Begruendung in helfer-zeit.mjs. */
|
|
const heute = heuteLokal();
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768, jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const idDogi = anlegen("Chef", "admin", "CODE-DOGI-0001");
|
|
const idPat = anlegen("Pat", "scout", "CODE-SCOU-0001");
|
|
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
|
|
|
|
/* Genug Bestand, dass die Seiten wirklich etwas zu zeichnen haben --
|
|
eine leere Liste laedt immer schnell. */
|
|
for (let i = 0; i < 40; i++) {
|
|
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist,
|
|
creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,'hoch',?,?,?,?,?)`)
|
|
.run(`Aufgabe Nummer ${i}`, "Eine Beschreibung mit etwas Text.",
|
|
["offen", "arbeit", "review", "erledigt"][i % 4], heute, idLuna, idPat, jetzt, idDogi);
|
|
}
|
|
for (let i = 0; i < 20; i++) {
|
|
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von)
|
|
VALUES (?,?,?,30,?,?,?)`)
|
|
.run(`Termin ${i}`, ["call", "termin", "review"][i % 3], heute + "T10:00", idLuna, jetzt, idDogi);
|
|
}
|
|
for (let i = 0; i < 30; i++) {
|
|
d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit,
|
|
datum, creator_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?)`)
|
|
.run(["live", "content", "technik", "community", "schutz"][i % 5],
|
|
i % 5 === 1 ? "idee" : "vorbereitung", `Eintrag ${i}`, "Text.",
|
|
"offen", "hoch", heute, idLuna, jetzt, idPat);
|
|
}
|
|
d.close();
|
|
|
|
/* ---- Browser ------------------------------------------------------------ */
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
const SEITEN = [
|
|
"start.html", "uebersicht.html", "aufgaben.html", "kalender.html", "calls.html",
|
|
"dateien.html", "content.html", "bereich.html?b=live", "profil.html",
|
|
"startcheck.html", "report.html", "scouting.html", "personen.html",
|
|
"wissen.html", "automation.html", "steckbrief.html",
|
|
];
|
|
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 20000 });
|
|
|
|
/* ERST ZUR RUHE KOMMEN LASSEN, dann messen (06.09.2026).
|
|
|
|
Die Anmeldung landet auf start.html -- und diese Seite holt ihre
|
|
Daten nach. Startet die Messschleife sofort, laufen diese Anfragen
|
|
noch, und der Zaehler der ERSTEN Seite bekommt sie mit. Genau das
|
|
ist passiert, als der Tagesblick dazukam: Die Pruefung meldete
|
|
"NEUE Doppelabfrage: start.html 2x /workspace/api/termine".
|
|
|
|
Es waren aber zwei SEITENAUFRUFE mit je einer Anfrage -- einmal
|
|
durch die Weiterleitung nach dem Anmelden, einmal durch die
|
|
Messschleife. Nachgemessen an einem einzelnen Aufruf: genau eine
|
|
Anfrage. Die Pruefung hat also nicht den Code beanstandet, sondern
|
|
ihre eigene Vorbereitung.
|
|
|
|
Wer hier nicht wartet, gibt jeder Seite die Schuld an dem, was die
|
|
vorherige noch offen hatte. */
|
|
await seite.waitForLoadState("networkidle");
|
|
await seite.waitForTimeout(600);
|
|
|
|
/* Grenzen. Grosszuegig -- sie sollen den Ausreisser finden, nicht das
|
|
Kilobyte. */
|
|
const GRENZE_ANFRAGEN = 40;
|
|
const GRENZE_MS = 4000;
|
|
|
|
/* ---- DAS GEWICHT: GEMESSEN WIRD, WAS UEBER DIE LEITUNG GEHT ----------
|
|
|
|
BIS ZUM 14.09.2026 WURDE HIER DAS FALSCHE GEMESSEN, und der Kommentar
|
|
daneben behauptete sogar das Richtige („das ist, was wirklich ueber
|
|
die Leitung ging"). Der Pruefstand spricht mit dem nackten
|
|
Express-Server auf 127.0.0.1, und der komprimiert nicht. Live steht
|
|
davor Caddy.
|
|
|
|
NACHGEMESSEN an der echten Adresse, nicht vermutet:
|
|
curl -H 'Accept-Encoding: zstd' workspace.dogfather-universe.com/…
|
|
start.css 345 KB -> 111 KB (Content-Encoding: zstd)
|
|
gate.css 120 KB -> 40 KB
|
|
kopf.js 65 KB -> 24 KB
|
|
|
|
Die Prueferei verglich also rund 1,2 MB gegen eine Grenze, die fuer
|
|
die Leitung gedacht war -- waehrend Filipe rund 500 KB bekommt. Die
|
|
Folge war eine Warnung, die nichts ueber sein Erlebnis sagt: genau
|
|
die Sorte, die man nach dreimal Lesen wegklickt.
|
|
|
|
JETZT WIRD BEIDES GEMESSEN. Komprimierbares wird im Pruefstand
|
|
gzip-gepackt (gemessen: start.css 356 KB -> 115 KB gzip gegen 111 KB
|
|
zstd live -- ein Stellvertreter, der auf vier Prozent genau ist),
|
|
alles andere zaehlt roh. Geurteilt wird ueber die uebertragene
|
|
Groesse, berichtet werden beide.
|
|
|
|
DIE GRENZE: 900 KB uebertragen. Die schwerste Seite liegt heute bei
|
|
rund 500 KB, davon 286 KB Buehnenbild -- das ist der Ausreisser, den
|
|
diese Zahl finden soll, mit Luft fuer eine zweite. Die rohe Groesse
|
|
bleibt in der Ausgabe stehen: Sie kostet zwar keine Leitung, aber
|
|
Rechenzeit beim Auswerten, und sie ist die Zahl, die man senken kann. */
|
|
const GRENZE_UEBERTRAGEN = 900_000;
|
|
|
|
/* Was Caddy komprimiert. Bilder, Schriften und Videos sind bereits
|
|
gepackt -- sie noch einmal durch gzip zu schicken kostet Zeit und
|
|
spart nichts. */
|
|
const KOMPRIMIERBAR = /^(text\/|application\/(javascript|json|xml|manifest)|image\/svg)/i;
|
|
|
|
/* ---- Layout-Sprung: eine GRUNDLINIE, kein Wunschwert ------------------
|
|
Gemessen am 04.09.2026 (Audit): Die Seiten springen beim Aufbau
|
|
zwischen 0 und 0,54. Nach Web Vitals gilt unter 0,1 als gut. Der
|
|
Grund ist bauartbedingt: Jede Seite liefert zuerst ihr Geruest und
|
|
holt die Daten danach per Schnittstelle -- die Liste ist also erst
|
|
leer und waechst dann. Sauber beheben liesse sich das nur mit
|
|
Platzhaltern fester Hoehe auf vierzehn Seiten, und das ist ein
|
|
Eingriff ins Aussehen, kein Fehlerfix.
|
|
|
|
Deshalb steht hier NICHT 0,1. Eine Pruefung, die vom ersten Tag an
|
|
rot ist, wird weggeklickt -- und mit ihr die echte Warnung. Sie haelt
|
|
stattdessen den heutigen Stand fest und schlaegt an, wenn es
|
|
SCHLECHTER wird. Der Zielwert bleibt 0,1; er gehoert in die
|
|
Verbesserungsliste, nicht in eine Ampel, die immer leuchtet. */
|
|
const GRENZE_SPRUNG = 0.6;
|
|
const ZIEL_SPRUNG = 0.1; // Web Vitals "gut" -- noch nicht erreicht
|
|
|
|
const zeilen = [];
|
|
let seitenGesamt = 0, anfragenGesamt = 0, bytesGesamt = 0;
|
|
const nichtGewogen = [];
|
|
const doppelte = [];
|
|
const langsame = [];
|
|
const springende = [];
|
|
const schwankend = []; /* Ausschlag da, aber beim Nachmessen weg */
|
|
const zuGross = [];
|
|
|
|
for (const datei of SEITEN) {
|
|
const gesehen = new Map();
|
|
let bytes = 0;
|
|
let roheBytes = 0, uebertragen = 0;
|
|
const warten = [];
|
|
const brocken = [];
|
|
const zaehler = (antwort) => {
|
|
const u = antwort.url();
|
|
gesehen.set(u, (gesehen.get(u) || 0) + 1);
|
|
/* Den Koerper JETZT holen -- nach dem Seitenwechsel ist er weg.
|
|
Ein Fehlschlag (Umleitung, 204, abgebrochen) ist kein Befund,
|
|
sondern schlicht nichts zu wiegen. */
|
|
const art = antwort.headers()["content-type"] || "";
|
|
/* EIN EREIGNISSTROM ENDET NIE -- und `body()` darauf auch nicht.
|
|
Genau daran ist am 06.09.2026 schon einmal ein ganzer Lauf
|
|
stehengeblieben, und hier ist es sofort wieder passiert: Der
|
|
Workspace haelt eine offene Verbindung fuer Benachrichtigungen.
|
|
Erst gar nicht anfassen -- und fuer alles andere trotzdem eine
|
|
Notbremse, weil die naechste Sorte Dauerverbindung noch nicht
|
|
erfunden ist. */
|
|
if (/event-stream/i.test(art)) return;
|
|
warten.push((async () => {
|
|
try {
|
|
const k = await Promise.race([
|
|
antwort.body(),
|
|
new Promise((r) => setTimeout(() => r(null), 3000)),
|
|
]);
|
|
if (!k?.length) return;
|
|
roheBytes += k.length;
|
|
const wiegt = KOMPRIMIERBAR.test(art) ? gzipSync(k).length : k.length;
|
|
uebertragen += wiegt;
|
|
/* WAS GENAU WIEGT? (07.10.2026)
|
|
„bereich.html?b=live 1094 KB" sagt, DASS etwas zu schwer ist,
|
|
und schickt einen danach suchen. Die Antwort liegt hier
|
|
ohnehin vor -- sie wurde nur weggeworfen. */
|
|
brocken.push({ u: u.replace(/^https?:\/\/[^/]+/, ""), kb: Math.round(wiegt / 1024) });
|
|
} catch { /* nichts zu wiegen */ }
|
|
})());
|
|
};
|
|
seite.on("response", zaehler);
|
|
|
|
const start = Date.now();
|
|
await seite.goto(BASIS + "/workspace/" + datei, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(500);
|
|
const dauer = Date.now() - start;
|
|
seite.off("response", zaehler);
|
|
await Promise.all(warten);
|
|
|
|
/* Uebertragene Groesse aus der Zeitleiste des Browsers -- das ist,
|
|
was wirklich ueber die Leitung ging. */
|
|
const messung = await seite.evaluate(() => {
|
|
const eintraege = performance.getEntriesByType("resource");
|
|
const summe = eintraege.reduce((s, e) => s + (e.transferSize || e.encodedBodySize || 0), 0);
|
|
const nav = performance.getEntriesByType("navigation")[0];
|
|
const bilderOhneMass = [...document.querySelectorAll("img")]
|
|
.filter((b) => !b.getAttribute("width") && !b.getAttribute("height")
|
|
&& !b.style.width && !b.style.height
|
|
&& !getComputedStyle(b).aspectRatio.includes("/"))
|
|
.map((b) => (b.getAttribute("src") || "").split("/").pop());
|
|
return {
|
|
bytes: summe,
|
|
domFertig: nav ? Math.round(nav.domContentLoadedEventEnd) : null,
|
|
bilderOhneMass,
|
|
knoten: document.querySelectorAll("*").length,
|
|
};
|
|
});
|
|
bytes = messung.bytes;
|
|
|
|
/* Layout-Sprung nach dem Laden: Der Browser meldet jede Verschiebung,
|
|
die nicht von einer Eingabe kam. */
|
|
const sprung = await seite.evaluate(() => new Promise((fertig) => {
|
|
let summe = 0;
|
|
try {
|
|
const b = new PerformanceObserver((liste) => {
|
|
for (const e of liste.getEntries()) if (!e.hadRecentInput) summe += e.value;
|
|
});
|
|
b.observe({ type: "layout-shift", buffered: true });
|
|
setTimeout(() => { b.disconnect(); fertig(Math.round(summe * 1000) / 1000); }, 900);
|
|
} catch { fertig(0); }
|
|
}));
|
|
|
|
seitenGesamt++;
|
|
const anfragen = [...gesehen.values()].reduce((s, n) => s + n, 0);
|
|
anfragenGesamt += anfragen;
|
|
bytesGesamt += bytes;
|
|
|
|
for (const [u, n] of gesehen) {
|
|
if (n > 1 && !u.includes("favicon")) {
|
|
doppelte.push(`${datei}: ${n}x ${u.replace(BASIS, "").split("?")[0]}`);
|
|
}
|
|
}
|
|
if (dauer > GRENZE_MS) langsame.push(`${datei} ${dauer} ms`);
|
|
|
|
/* EIN AUSSCHLAG WIRD BESTAETIGT, BEVOR ER GEMELDET WIRD (05.09.2026).
|
|
|
|
Der Layout-Sprung ist ein SCHWANKENDER Messwert. Er haengt davon
|
|
ab, wie schnell der Server in dieser Sekunde antwortet: Kommen die
|
|
Daten, waehrend das Geruest noch aufgebaut wird, springt nichts --
|
|
kommen sie danach, springt die Liste auf ihre volle Hoehe.
|
|
Nachgemessen am 05.09.2026, dieselbe Seite, drei Laeufe
|
|
hintereinander: 0,478 / 0,262 / 0,262. Und bereich.html meldete
|
|
einmal 0,703 und beim naechsten Lauf 0,347.
|
|
|
|
Eine Pruefung, die zufaellig rot wird, ist genauso wertlos wie
|
|
eine, die nie anschlaegt -- man klickt sie weg, und mit ihr die
|
|
echte Warnung. (Dieselbe Lehre wie bei der Loeschwarnung, die
|
|
immer kam.)
|
|
|
|
Deshalb wird beim Ueberschreiten NOCH EINMAL gemessen und nur
|
|
gemeldet, wenn es sich wiederholt. Das kostet Zeit ausschliesslich
|
|
im Verdachtsfall. Ein ECHTER Layout-Sprung -- ein Bild ohne
|
|
Groessenangabe, eine Schrift, die nachlaedt -- kommt bei jedem
|
|
Lauf, der ueberlebt diese zweite Messung muehelos.
|
|
|
|
WICHTIG: Gemeldet wird der ZWEITE Wert, nicht der kleinere von
|
|
beiden. Sonst waere es keine Bestaetigung, sondern eine
|
|
Beschoenigung. */
|
|
if (sprung > GRENZE_SPRUNG) {
|
|
await seite.goto(BASIS + "/workspace/" + datei, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(400);
|
|
const zweitens = await seite.evaluate(() => new Promise((fertig) => {
|
|
let summe = 0;
|
|
/* BEIM ZWEITEN MESSEN WIRD AUCH FESTGEHALTEN, WAS SPRINGT.
|
|
"Layout springt ueber 0.6" allein sagt, DASS etwas nicht
|
|
stimmt, und laesst einen danach raten -- ich habe deswegen am
|
|
06.09.2026 vergeblich gesucht, weil der Sprung sich in einem
|
|
nachgebauten Aufbau nicht zeigte. Die Quellen stehen im
|
|
Ereignis selbst; sie nicht mitzunehmen war reine
|
|
Nachlaessigkeit. */
|
|
const wer = [];
|
|
try {
|
|
const b = new PerformanceObserver((liste) => {
|
|
for (const e of liste.getEntries()) {
|
|
if (e.hadRecentInput) continue;
|
|
summe += e.value;
|
|
for (const q of (e.sources || [])) {
|
|
const n = q.node;
|
|
wer.push(`${n ? (n.tagName || "?") + (n.className && typeof n.className === "string"
|
|
? "." + n.className.split(" ")[0] : "") : "?"}`
|
|
+ ` ${Math.round(q.previousRect.top)}->${Math.round(q.currentRect.top)}`);
|
|
}
|
|
}
|
|
});
|
|
b.observe({ type: "layout-shift", buffered: true });
|
|
setTimeout(() => {
|
|
b.disconnect();
|
|
fertig({ wert: Math.round(summe * 1000) / 1000, wer: [...new Set(wer)].slice(0, 4) });
|
|
}, 900);
|
|
} catch { fertig({ wert: 0, wer: [] }); }
|
|
}));
|
|
if (zweitens.wert > GRENZE_SPRUNG) {
|
|
springende.push(`${datei} ${sprung} und erneut ${zweitens.wert}`
|
|
+ (zweitens.wer.length ? `
|
|
es springt: ${zweitens.wer.join(" | ")}` : ""));
|
|
} else {
|
|
/* Nicht verschweigen: Der Ausschlag war da, er hat sich nur nicht
|
|
bestaetigt. Wer die Zahl haeufig sieht, weiss, dass die Seite
|
|
nahe an der Grenze liegt. */
|
|
schwankend.push(`${datei} ${sprung} → beim Nachmessen ${zweitens.wert}`);
|
|
}
|
|
}
|
|
/* KONNTE ueberhaupt gewogen werden? Kommt aus den Koerpern nichts
|
|
zurueck (Browser aus dem Zwischenspeicher, alles abgebrochen),
|
|
waere „unter der Grenze" trivial wahr. Dann gilt die Zeitleiste
|
|
des Browsers als Rueckfall -- und der dritte Ausgang steht in der
|
|
Ausgabe. */
|
|
const gewogen = uebertragen > 0;
|
|
const zaehlwert = gewogen ? uebertragen : bytes;
|
|
if (!gewogen) nichtGewogen.push(datei);
|
|
|
|
if (anfragen > GRENZE_ANFRAGEN || zaehlwert > GRENZE_UEBERTRAGEN) {
|
|
const groesste = brocken.sort((x, y) => y.kb - x.kb).slice(0, 3)
|
|
.map((x) => `${x.kb} KB ${x.u}`).join(", ");
|
|
zuGross.push(`${datei} ${anfragen} Anfragen / ${Math.round(zaehlwert / 1024)} KB übertragen`
|
|
+ (groesste ? ` -- am schwersten: ${groesste}` : ""));
|
|
}
|
|
|
|
zeilen.push(` ${datei.padEnd(22)} ${String(anfragen).padStart(3)} Anfragen `
|
|
+ `${String(Math.round(zaehlwert / 1024)).padStart(4)} KB über die Leitung `
|
|
+ `(roh ${String(Math.round((roheBytes || bytes) / 1024)).padStart(4)}) `
|
|
+ `${String(dauer).padStart(4)} ms `
|
|
+ `Sprung ${String(sprung).padStart(5)} ${String(messung.knoten).padStart(4)} Knoten`);
|
|
}
|
|
|
|
console.log("\n=== Je Seite ===");
|
|
for (const z of zeilen) console.log(z);
|
|
|
|
console.log("\n=== Bewertung ===");
|
|
ok(seitenGesamt === SEITEN.length, `${seitenGesamt} Seiten gemessen`);
|
|
ok(anfragenGesamt > 100, `${anfragenGesamt} Anfragen insgesamt beobachtet`);
|
|
/* DOPPELANFRAGEN: eine Grundlinie, kein Nullwert.
|
|
Gemessen: kopf.js holt die Personenliste fuer den Sicht-Umschalter,
|
|
die Seite selbst fuer ihre Zuordnungs-Auswahl -- dieselbe Adresse,
|
|
zweimal. Ein Versuch, gleichzeitige Anfragen im fetch-Wrapper
|
|
zusammenzufassen, wurde gebaut, gemessen und WIEDER ENTFERNT: Er half
|
|
nicht (die beiden Abfragen laufen oft nacheinander, nicht
|
|
gleichzeitig) und haette den zentralen Wrapper verkompliziert. Ohne
|
|
nachweisbaren Nutzen bleibt der einfachere Stand stehen.
|
|
Die Zahl schwankt je nach Zeitverlauf zwischen 4 und 7. Ueber 10
|
|
waere etwas Neues dazugekommen -- dann lohnt das Nachsehen. */
|
|
/* Gezaehlt wird nicht die MENGE (die schwankt je nach Zeitverlauf
|
|
zwischen vier und elf -- mal laufen die beiden Abfragen gleichzeitig,
|
|
mal nacheinander), sondern WELCHE Adressen betroffen sind. Eine
|
|
Grenze immer weiter hochzusetzen, bis sie gruen wird, waere
|
|
Selbstbetrug; eine Liste bekannter Stellen ist pruefbar.
|
|
Kommt eine Adresse dazu, die hier nicht steht, ist das neu -- und
|
|
genau das soll auffallen. */
|
|
const BEKANNT_DOPPELT = [
|
|
"/workspace/api/personen", // kopf.js (Sicht-Umschalter) + Seite (Zuordnung)
|
|
"/workspace/api/hinweise", // Startseite: Hinweisliste und Kachelzahlen
|
|
"/workspace/api/bereich/", // Bereichsseite: Liste und Kennzahlen
|
|
"/workspace/assets/img/buehne-", // Buehnenbild in zwei CSS-Regeln
|
|
/* /api/ich holen mehrere Bausteine unabhaengig voneinander: kopf.js
|
|
fuer die Kopfleiste, dazu die Seiten-Datei und auf profil.html
|
|
zusaetzlich steckbrief.js -- drei Bausteine, dieselbe Frage.
|
|
Sauber loesen liesse sich das nur, indem kopf.js die Antwort einmal
|
|
holt und bereitstellt; das beruehrt vierzehn Dateien und traegt das
|
|
Risiko, dass eine Seite kuenftig mit veralteten Angaben arbeitet.
|
|
Fuer wenige Kilobyte ist das nicht verhaeltnismaessig -- gehoert in
|
|
die Verbesserungsliste, nicht in einen Schnellschuss. */
|
|
"/workspace/api/ich",
|
|
];
|
|
const neueDoppelte = doppelte.filter(
|
|
(z) => !BEKANNT_DOPPELT.some((b) => z.includes(b)));
|
|
ok(neueDoppelte.length === 0,
|
|
neueDoppelte.length
|
|
? `NEUE Doppelabfrage: ${neueDoppelte.slice(0, 5).join(" · ")}`
|
|
: `${doppelte.length} Doppelabfragen, alle an bekannten Stellen `
|
|
+ `(${BEKANNT_DOPPELT.length} Adressen, siehe Liste im Quelltext)`);
|
|
ok(langsame.length === 0,
|
|
langsame.length ? `ueber ${GRENZE_MS} ms: ${langsame.join(", ")}` : `keine Seite braucht laenger als ${GRENZE_MS} ms`);
|
|
ok(springende.length === 0,
|
|
springende.length ? `Layout springt ueber ${GRENZE_SPRUNG}: ${springende.join(", ")}`
|
|
: `kein bestaetigter Layout-Sprung ueber der Grundlinie ${GRENZE_SPRUNG} (Ziel bleibt ${ZIEL_SPRUNG})`);
|
|
|
|
/* Kein Fehler, aber es gehoert gesagt: Diese Seiten lagen einmal
|
|
darueber und beim zweiten Messen nicht mehr. Wer den Hinweis oft
|
|
sieht, weiss, dass die Grundlinie knapp geworden ist. */
|
|
if (schwankend.length) {
|
|
console.log(` hinw ${schwankend.length} Ausschlag(e) haben sich beim Nachmessen `
|
|
+ `NICHT bestaetigt: ${schwankend.join(" · ")}`);
|
|
}
|
|
|
|
/* Der ehrliche Zwischenstand, damit die Grundlinie nicht als "alles
|
|
gut" missverstanden wird -- er zaehlt nicht als Fehler, steht aber
|
|
bei jedem Lauf da. */
|
|
const ueberZiel = zeilen.filter((z) => {
|
|
const m = z.match(/Sprung\s+([\d.]+)/);
|
|
return m && Number(m[1]) > ZIEL_SPRUNG;
|
|
}).length;
|
|
console.log(` hinw ${ueberZiel} von ${seitenGesamt} Seiten liegen ueber dem Zielwert `
|
|
+ `${ZIEL_SPRUNG} fuer den Layout-Sprung (bauartbedingt: Geruest zuerst, Daten danach)`);
|
|
ok(zuGross.length === 0,
|
|
zuGross.length ? `zu schwer: ${zuGross.join(", ")}`
|
|
: `keine Seite ueber ${GRENZE_ANFRAGEN} Anfragen oder ${Math.round(GRENZE_UEBERTRAGEN / 1024)} KB ueber die Leitung`);
|
|
/* DER DRITTE AUSGANG: Konnte ueberhaupt gewogen werden? Eine Seite,
|
|
deren Koerper der Browser aus dem Zwischenspeicher genommen hat,
|
|
waere sonst lautlos „leicht“. */
|
|
ok(nichtGewogen.length === 0,
|
|
nichtGewogen.length ? `KONNTE NICHT WIEGEN: ${nichtGewogen.join(", ")} -- Ergebnis unbrauchbar`
|
|
: `alle ${seitenGesamt} Seiten wirklich gewogen`);
|
|
|
|
/* =======================================================================
|
|
GEGENPROBE
|
|
======================================================================= */
|
|
console.log("\n=== Gegenprobe (muss anschlagen) ===");
|
|
{
|
|
await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
|
|
|
/* Doppelanfrage kuenstlich erzeugen. */
|
|
const gesehen = new Map();
|
|
const zaehler = (a) => gesehen.set(a.url(), (gesehen.get(a.url()) || 0) + 1);
|
|
seite.on("response", zaehler);
|
|
await seite.evaluate(async () => {
|
|
await fetch("/workspace/api/ich", { credentials: "same-origin" });
|
|
await fetch("/workspace/api/ich", { credentials: "same-origin" });
|
|
});
|
|
await seite.waitForTimeout(400);
|
|
seite.off("response", zaehler);
|
|
const dopp = [...gesehen.entries()].filter(([u, n]) => n > 1 && u.includes("/api/ich"));
|
|
ok(dopp.length > 0, `eine doppelte Anfrage wird erkannt (${dopp.map(([u, n]) => `${n}x`).join(",")})`);
|
|
|
|
/* Layout-Sprung kuenstlich erzeugen. */
|
|
const sprung = await seite.evaluate(() => new Promise((fertig) => {
|
|
let summe = 0;
|
|
const b = new PerformanceObserver((liste) => {
|
|
for (const e of liste.getEntries()) if (!e.hadRecentInput) summe += e.value;
|
|
});
|
|
b.observe({ type: "layout-shift" });
|
|
const klotz = document.createElement("div");
|
|
klotz.style.cssText = "height:400px;background:#333";
|
|
document.body.prepend(klotz);
|
|
setTimeout(() => { b.disconnect(); fertig(Math.round(summe * 1000) / 1000); }, 800);
|
|
}));
|
|
/* Gegen den ZIELWERT pruefen, nicht gegen die Grundlinie. Die
|
|
Grundlinie steht bei 0,6, damit die Pruefung nicht dauernd rot ist
|
|
-- ein kuenstlicher Klotz von 400 px erzeugt aber nur rund 0,3.
|
|
Gegen 0,6 gemessen waere diese Gegenprobe fehlgeschlagen und haette
|
|
behauptet, die Messung funktioniere nicht. Bewiesen werden muss
|
|
hier: die Messung SIEHT einen Sprung. */
|
|
ok(sprung > ZIEL_SPRUNG,
|
|
`ein echter Layout-Sprung wird gemessen (${sprung} > ${ZIEL_SPRUNG})`);
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|