Miss auf der Ziel-URL den durch Webfonts verursachten Layout Shift, behebe ihn
über ein metrisch angeglichenes Fallback-Face und weise die Wirkung gegen zwei
Kontrollarme nach. Arbeite die Schritte 1 bis 5 strikt in dieser Reihenfolge ab.
Berichte nach jedem Schritt in zwei bis drei Sätzen: was du getan hast, welche
Zahl herauskam, welcher Entscheidungspfad daraus folgt.
ZIEL-URL:
Prüfe alle fünf Punkte vor dem ersten Abruf. Jeder Fehlschlag ist ein Abbruch
mit Klartext-Meldung, keine Ersatzhandlung, keine Teilausführung.
1. ZIEL-URL: Ist der Platzhalter nicht ersetzt, brich ab und frage nach der URL.
2. BERECHTIGUNG: Bestätige, dass die Seite dem Auftraggeber gehört oder in
seiner Betreuung liegt. Ist das unklar, frage nach, bevor du misst.
3. WERKZEUG: Playwright (Chromium) mit CDP-Zugriff. Prüfe die Installation.
Kein Fallback auf manuelle DevTools-Bedienung.
4. FONTTOOLS: `python -c "import fontTools"`. Fehlt es, lege ein venv an:
`python -m venv .venv && .venv/bin/pip install fonttools brotli`.
5. LOKALE FAMILIEN: Liste die auf dem Messsystem installierten Familien auf
(`fc-list : family` unter Linux, `system_profiler SPFontsDataType` unter
macOS, Registry unter Windows). Enthält das System keine der Familien aus
dem Fallback-Stack der Zielseite, brich ab: `src: local()` löst dann nicht
auf, und jede Kalibrierung wäre gegenstandslos. Berichte die Liste.
ARME. Drei Zustände, jeder mit fünf gültigen Kaltläufen:
- Arm A "Baseline": Seite unverändert.
- Arm B "Positivkontrolle": font-family der Treiberrolle auf ein generisches
Keyword gezwungen (Fließtext monospace, Überschrift serif). Nur generische
Keywords. Ein benannter Familienname, den das System nicht kennt, löst nicht
auf und misst still Arm A.
- Arm C "Zielzustand": Seite plus Fallback-Face aus Schritt 4.
Arm B ist das Messgerät, nicht der Vorher-Zustand. Vorher/Nachher ist A gegen C.
KALTLAUF. Frischer BrowserContext, `Network.setCacheDisabled(true)`, kein
Storage-State, kein Reload. Nachweis pro Lauf: die Font-Antworten haben
`fromDiskCache === false` und `transferSize > 0`. Läuft der Nachweis nicht auf,
ist der Lauf ungültig: verwerfen, wiederholen, im Bericht zählen.
PROBEN-ELEMENT. Der längste Textlauf im ersten Viewport je Schriftrolle,
ermittelt über die Zeichenzahl des Textknotens, nicht geschätzt. Alle
Versatzmessungen laufen an genau diesem Element und werden mit seinem Selektor
berichtet.
MESSGRÖSSEN. Vor dem Font-Load (direkt nach dem ersten Paint, Fallback aktiv)
und nach `document.fonts.ready` jeweils erheben:
- V-SHIFT (px): Differenz von `offsetHeight` des Proben-Elements plus Differenz
von `getBoundingClientRect().top`. Das ist die für CLS relevante Größe.
- H-SHIFT (%): Breitendifferenz des Textlaufs, gemessen über
`Range.getBoundingClientRect()` auf den Textknoten, nicht über die Layout-Box
des Blockelements. Die Box eines Block-Level-Elements ist schriftunabhängig
und misst konstant 0.
- ZEILENZAHL: `Math.round(offsetHeight / lineHeight)` vor und nach dem Load.
Unsichtbarer Text (font-display: block) belegt Layout und wird über diese Werte
mitgemessen, nicht über Sichtbarkeit.
CLS. Über `PerformanceObserver` auf `layout-shift` mit `hadRecentInput === false`,
Sessionfenster nach Web-Vitals-Definition. Die verschobenen Knoten liest du aus
`entry.sources[].node`. Ein Chrome-Trace ist optional zur Gegenprobe.
WÄCHTER. Jeder gepatchte Lauf besteht drei Prüfungen, jede Fehlprüfung
invalidiert den Lauf:
W1 `getComputedStyle(probe).fontFamily` entspricht dem Patch, ausgelesen beim
ersten Layout, nicht erst nach dem Paint.
W2 Kaltlauf-Nachweis wie oben definiert.
W3 `document.fonts.ready` abgewartet und alle beteiligten Faces in
`document.fonts` mit `status === "loaded"`.
Berichte je Arm: gültige Läufe, verworfene Läufe, Grund.
- Viewport 412 x 915, `isMobile: true`, `hasTouch: true`, DPR 2.
- Drosselung über CDP: `Network.emulateNetworkConditions` mit Slow-4G-Werten
(400 kbit/s down, 400 kbit/s up, 400 ms RTT), `Emulation.setCPUThrottlingRate: 4`.
Identisch in allen Armen, auch in der Abnahme.
- Fünf gültige Läufe je Arm. Berichte Median und Spannweite, nie einen Einzelwert.
- Patches greifen vor dem ersten Layout: `addInitScript` oder `route()` mit
Umschreiben der CSS-Antwort. Ein Patch nach dem ersten Paint ist für die
Messphase wirkungslos und wird von W1 abgefangen.
- Die Fallback-Schriften stammen vom Messsystem, nicht vom emulierten Gerät.
Das Ergebnis gilt für dieses Betriebssystem. Nenne es im Bericht und
vermerke, dass andere Systeme eine eigene Kalibrierung brauchen.
ABRUF-BUDGET: 35 Läufe. Reicht es nicht, kürze in dieser Reihenfolge:
Schritt 2 auf die Rolle mit dem größten Textanteil, dann Arm B auf drei Läufe.
Nie unter fünf Läufe in Arm A und Arm C.
Miss Arm A. Erhebe Median-CLS und Spannweite, die verschobenen Knoten aus
`entry.sources`, und ob der Shift vor oder nach dem ersten Paint liegt.
Notiere je Face: font-display (swap, block, fallback, optional, auto), bereits
vorhandene size-adjust-, ascent-override-, descent-override- und
line-gap-override-Werte, sowie den vollständigen font-family-Stack inklusive
Auflösung aller CSS-Custom-Properties.
ENTSCHEIDUNG:
- Enthält die Shift-Attribution keine Textelemente: Ende mit Befund und
CLS-Median. Die Schrift ist nicht der Treiber.
- Median-CLS unter 0,1 UND V-Shift unter 5 px: Ende mit Befund. Kein Fix nötig,
ein Eingriff wäre nicht messbar zu rechtfertigen.
- Sonst weiter mit Schritt 2.
Bestimme alle Schriftrollen mit Text im ersten Viewport. Rollen ohne solchen
Text dokumentierst du mit Begründung und schließt sie aus. Teilen sich Rollen
dieselbe Familie und dieselben Achsen, genügt ein Arm.
Jede verbleibende Rolle bekommt einen eigenen Arm nach dem Muster von Arm B,
fünf Kaltläufe. Die Rolle mit dem größten V-Shift ist der Treiber.
Berichte V-Shift, H-Shift und Zeilenzahldifferenz je Rolle.
Arm B auf die Treiberrolle, fünf Kaltläufe, Wächter aktiv.
Arm B muss Arm A bei V-Shift oder CLS mindestens verdoppeln. Tut er das nicht,
ist das Messgerät unbrauchbar: Abbruch mit Fehlermeldung, keine Auswertung,
keine Zahlen aus Schritt 4 oder 5.
1. Lade die Webfont-Datei der Treiberrolle. Ist sie nicht ladbar, brich ab mit
dieser Meldung. Bei mehreren Subsets kalibriere jedes, dessen Unicode-Range
den Haupt-Textlauf abdeckt.
2. Kandidaten sind nur Familien, die in Vorbedingung 5 als vorhanden bestätigt
wurden. Verworfene Kandidaten mit Grund berichten.
3. size-adjust, zwei Rechenwege, beide berichten:
a) Gewichtet: Summe der Advance-Widths (hmtx, normalisiert auf unitsPerEm
aus head) über einen echten Textlauf der Zielsprache aus dem
Proben-Element, Ziel geteilt durch Fallback.
b) Ungewichtet: Mittel über alle gemeinsamen Glyphen.
Entscheide mit Begründung. Bei Systemschriften mit großem Zeichensatz liegt
Weg b um Faktoren daneben und ist als alleinige Basis unzulässig.
4. Vertikale Overrides, zwingend zusätzlich zu size-adjust, sonst bleibt der
CLS-relevante Anteil unkorrigiert:
ascent-override = Ziel-Ascender / unitsPerEm / (size-adjust)
descent-override = Ziel-Descender / unitsPerEm / (size-adjust)
line-gap-override = Ziel-LineGap / unitsPerEm / (size-adjust)
Quelle der Ziel-Werte: hhea.ascender/descender/lineGap, gegengelesen gegen
OS/2 sTypoAscender/sTypoDescender und usWinAscent/usWinDescent. Nenne, welche
Tabelle du verwendest und warum, und ob USE_TYPO_METRICS (fsSelection Bit 7)
gesetzt ist.
5. Baue das Face:
@font-face mit `src: local("")`, den vier Overrides und
`font-weight` über die volle auf der Seite genutzte Achsenspanne. Ohne die
Spanne greift das Face nur bei einem Gewicht und fette Texte fallen
unkorrigiert durch. Ein Face je genutztem Style (normal/italic).
6. Setze das Face als ersten Fallback-Eintrag hinter der Zielschrift in den
Stack, direkt in der Regel oder in der Custom Property, die die Seite nutzt.
Bestehende Overrides aus Schritt 1 darf dein Face nicht konterkarieren.
Berichte die exakte Stelle.
Fünf gültige Kaltläufe Arm C, fünf Arm B, gleiche Drosselung wie im Befund.
Arm A liegt aus Schritt 1 vor.
BESTANDEN, wenn alle drei Bedingungen für Arm C gelten:
1. Median-CLS unter 0,1.
2. Größter V-Shift im ersten Viewport unter 5 px.
3. H-Shift des Proben-Textlaufs unter 3 %.
- Liegt Arm C unter den Schwellen, Arm B aber ebenfalls: das Messgerät hat
nichts gemessen. Metrikbruch in Arm B vergrößern (anderes Keyword, größere
Metrikdistanz) und Schritt 3 wiederholen. Ohne Trennschärfe in Arm B ist die
Abnahme wertlos.
- Reißt Arm C eine Schwelle bei grünen Wächtern: genau ein Korrekturlauf
(Overrides nachjustieren), dann Bericht mit den Zahlen. Keine unbegrenzte
Iteration.
Jede Zahl mit Arm, Anzahl gültiger Läufe und Messbedingung.
- Befund: Median-CLS und Spannweite Arm A, verschobene Knoten, font-display je
Face, bestehende Overrides, Kaltlauf-Nachweis.
- Ursache: V-Shift, H-Shift, Zeilenzahldifferenz je Rolle; ausgeschlossene
Rollen mit Grund.
- Aufgelöster Fallback-Stack, vorhandene und verworfene Kandidaten mit Grund.
- size-adjust je Kandidat, beide Rechenwege, Entscheidung mit Begründung.
- Ascent-, Descent-, Line-Gap-Override mit Tabellenquelle.
- Vorher/Nachher: Median und Spannweite je Arm A/B/C, Wächterergebnis und
verworfene Läufe je Arm.
- Fertiges CSS des Fallback-Faces und die Stelle im Stack.
- Betriebssystem und Browserversion des Messgeräts, Hinweis auf systemabhängige
Gültigkeit.
- Keine Reloads als Kaltläufe, keine Werte aus warmen oder invalidierten Läufen.
- Keine Zahl ohne Arm und Messbedingung.
- Keine Zusammenfassung aus einem Einzellauf.
- Keine Glyphenmittelung als alleinige size-adjust-Basis.
- Kein size-adjust ohne die vertikalen Overrides.
- Kein font-display: optional als Lösung: es unterdrückt den Swap, indem die
Zielschrift auf langsamer Leitung gar nicht erscheint, und behebt nichts.
- Keine benannten Familien in Arm B.
- Keine geschätzten Werte, wenn eine Messung abgebrochen ist. Abbruch melden.