Bilder, Anhänge und Seitenleistung gestalten
Originale und Anzeigedateien trennen und Bilder passend zur Anzeigegröße ausliefern. Datenmenge, Antwortzeit, Darstellung und Interaktionen getrennt messen, um langsame Abschnitte zu finden.
Bildspeicher, Komprimierung und Browseranfragen einrichten
Stehen Öffnungszeiten nur in einem Plakatbild, finden textlesende Werkzeuge Wochentage und Ausnahmen kaum im Seitentext. Läuft auch die Plakatadresse ab, lässt sich das Bild selbst nicht mehr abrufen. Öffnungszeiten und Schließtage in HTML sowie Fotos mit erreichbaren Adressen und Beschreibungen schaffen Zugänge zu Text und Bildern. Googles Hinweise zu Bildern
Object Storage speichert Bilder und Anhänge als Schlüssel und Dateiinhalte. Ein Bucket ist eine Speichereinheit für eine Gruppe von Dateien. Die Datenbank kann Dateipfad, Beschreibung und Veröffentlichungsstatus enthalten, der Speicher Originale und Anzeigevarianten. Eine signierte URL erlaubt Dateizugriff über Signatur und Ablaufbedingungen. Wird eine kurz gültige Adresse dauerhaft in einer öffentlichen Seite gespeichert, kann das Bild bei späteren Abrufen fehlen.
WebP und AVIF sind Bilddateiformate. Encoding wandelt Pixel in ein Dateiformat um, Decoding zurück in anzeigbare Pixel. EXIF enthält Zusatzinformationen wie Ausrichtung und Aufnahmedaten. Ein Hash ist ein aus dem Inhalt berechneter Kennwert, mit dem sich Änderungen am Original und die Wiederverwendung von Varianten unterscheiden lassen.
src nennt die Standardbildadresse, srcset die Kandidaten nach Größe und sizes die erwartete Anzeigebreite im Layout. DPR (Device Pixel Ratio) ist das Verhältnis von Gerätepixeln zu einem CSS-Pixel. Bei gleicher Anzeigebreite kann je nach DPR eine größere Variante gewählt werden. Vorab reservierter Platz durch width und height oder aspect-ratio reduziert Verschiebungen des umgebenden Texts nach dem Laden.
Brotli und gzip komprimieren übertragene Inhalte wie HTML, CSS und JavaScript. Server oder CDN liefern sie mit ausgehandeltem Content-Encoding. Die Größe bereits komprimierter Bilder wird bei der Bildumwandlung reduziert. Lazy Loading eines wichtigen Bilds im ersten sichtbaren Bereich kann dessen Anzeige verzögern. Prüfen Sie deshalb den Anfragezeitpunkt für anfänglich sichtbare und weiter unten liegende Bilder getrennt.
CORS (Cross-Origin Resource Sharing) steuert, ob JavaScript im Browser Antworten eines anderen Origins lesen darf. Das unterscheidet sich von öffentlichem Zugriff bei Serveranfragen oder gewöhnlichen Dokumentaufrufen. Muss eine öffentliche API aus einer anderen Domain im Browser aufgerufen werden, konfigurieren Sie erlaubte Origins. Authentifizierungspflichtige APIs benötigen zusätzlich eine Berechtigungsprüfung.
Antwortzeit, Seitenleistung und Fehlerquote verstehen und messen
TTFB (Time To First Byte) ist die Zeit vom Beginn einer Anfrage bis zum ersten Antwortbyte. DNS-Abfrage, Verbindungsaufbau, Netzwerklaufzeiten und Serververarbeitung beeinflussen sie. Lesen Sie die Serververarbeitungszeit gesondert aus Anwendungsprotokollen, um die Verzögerung zu lokalisieren.
LCP (Largest Contentful Paint) bezeichnet den Zeitpunkt, zu dem große Hauptinhalte erscheinen, CLS (Cumulative Layout Shift) unerwartete Layoutverschiebungen. INP (Interaction to Next Paint) erfasst während eines gesamten Seitenbesuchs die Reaktionsverzögerung von Klicks, Tippen und Tastatureingaben bis zur nächsten Darstellung. Meist wird die längste Verzögerung verwendet; pro 50 Interaktionen wird ein höchster Wert zur Behandlung von Ausreißern ausgeschlossen. Unterscheiden Sie die Verzögerung eines einzelnen Klicks vom INP des gesamten Besuchs. Das p75 der INP-Werte mehrerer Besuche ist eine weitere Aggregationsstufe. Definition von INP · Web Vitals
Labordaten stammen aus Tests unter festgelegten Geräte- und Netzwerkbedingungen. Felddaten werden in tatsächlichen Nutzerumgebungen erhoben. p75 und p95 sind die Werte an der 75%- beziehungsweise 95%-Position der aufsteigend sortierten Beobachtungen. Die Verteilung neben dem Mittelwert zeigt Fälle, in denen nur einzelne Anfragen stark verzögert sind. Speichern Sie Gerät, Zeitraum und Stichprobengröße mit.
Die Fehlerquote ist der Anteil fehlgeschlagener Anfragen an den betrachteten Anfragen. Zählen Sie 5xx, Zeitüberschreitungen und WAF-Sperren getrennt. Die Cache-Trefferquote ist der Anteil der Cache-Abfragen, die mit gespeicherten Antworten bedient werden. Auch eine hohe Trefferquote kann alte Artikel liefern; prüfen Sie deshalb die revision der Antwort mit.
Wer über die Suche kommt und auf ein großes Bild warten muss oder eine beim Laden verrutschende Anmeldeschaltfläche vorfindet, wird beim Lesen und Handeln behindert. LCP, INP und CLS messen unterschiedliche Aspekte dieser Erfahrung. Google erklärt, Core Web Vitals in seinen Rankingsystemen zu verwenden. Für Zitate in generierten Antworten sind die tatsächlichen Zitierergebnisse gesondert zu lesen. Unterscheiden Sie Änderungen an Bildzugriff, Textbeschreibung und Nutzererfahrung. Seitenerfahrung und Suche
CMS-Bildauslieferung und Cache an die Veröffentlichungsregeln anpassen
Auch ein klein dargestelltes Foto kann das gesamte Original herunterladen. Vergleichen Sie in Network der Entwicklerwerkzeuge tatsächliche URL, Antwortbytes, MIME-Typ und Pixelmaße mit der Anzeigebreite. Prüfen Sie, ob nur CMS-Fotos Originale liefern, während statische Fotos optimiert sind, ob srcset und sizes ausgegeben werden und welchen Kandidaten der Browser auswählt.
Originale als Quellenmaterial erhalten und separate Anzeigevarianten erzeugen. Begrenzen Sie Eingabegröße, Pixelzahl, zulässige Formate und Verarbeitungszeit des Konverters. Wenden Sie die Ausrichtung an, verkleinern Sie das Bild und kodieren Sie es in ein unterstütztes Format wie WebP oder AVIF. Nicht über das Original hinaus vergrößern; Transparenz und Animationen ebenfalls prüfen. Erforderliche EXIF-Entfernung auf öffentliche Varianten anwenden und Originale nicht überschreiben.
Varianten-Schlüssel enthalten Originalhash, Ausgabebreite und Version der Encodereinstellungen. So werden Umwandlungen desselben Fotos wiederverwendet, ohne nach Originalersatz oder geänderter Qualität alte Dateien fälschlich zu nutzen. Legen Sie Breiten und Qualität anhand tatsächlicher Anzeigegröße, DPR und Bildqualität fest. Karten, Inhaltsbilder und Vergrößerungen können unterschiedliche Variantenbreiten benötigen.
Verwenden Sie img mit srcset und sizes oder eine konfigurierte Bildkomponente. sizes muss zur tatsächlichen Bildbreite im Raster passen. Eine kleine Breite für alle Karten erzeugt auf großen Bildschirmen Unschärfe; stets die volle Bildschirmbreite anzugeben lädt unnötig große Bilder. Reservieren Sie Platz mit width und height oder aspect-ratio. Googles Hinweise zu Bildern erläutert die Bereitstellung einer src-Ersatzadresse, von Alternativtext und Kontext im Seiteninhalt.
unoptimized in next/image überspringt die eingebaute Bildumwandlung. Ein loader erzeugt eine Bildadresse für vorgegebene Breite und Qualität. Prüfen Sie, ob eigene Loader Varianten liefern und ob Komponenten auf authentifizierungspflichtige oder direkt auf Originaldateien verweisen. Vergleichen Sie Breite, Größe und Format der gewählten Variante in Network.
Trennen Sie bei Caches Umwandlungskosten und Zugriffskontrolle. Müssen Materialien nach Rücknahme sofort gesperrt sein, können private Varianten wiederverwendet und zugleich bei jeder Anfrage aktueller Veröffentlichungsstatus und Berechtigung geprüft werden. Langlebige immutable-Antworten auf einem öffentlichen CDN können Dateien nach Rücknahme weiter ausliefern. Wenden Sie dieselben Rücknahmebedingungen auf Original-, Varianten- und allgemeine Bildoptimierungsadressen an und suchen Sie nach Wegen, die Zugriffskontrollen umgehen.
Der standardmäßige Next.js-Bildoptimierer leitet keine Authentifizierungsheader an die Originalanfrage weiter. Prüfen Sie bei geschützten Originalen die Anfragestruktur; möglich sind ein separater öffentlicher Bildpfad oder Varianten eines vorhandenen Bilddienstes. Bei unoptimized hängt die Datenmenge davon ab, ob src auf eine kleine Variante oder ein großes Original zeigt. Auch wenn ein loader /media/photo?w=640 erzeugt, muss der Server tatsächlich eine 640px-Datei erstellen, damit die Größe sinkt. Next.js-Image-Konfiguration
Bilddatenmenge und tatsächliche Ladezeit getrennt messen
Vergleichen Sie Änderungen bei gleicher Seite, Fensterbreite, DPR, Scrollstrecke und Cache-Bedingung. Erfassen Sie Bildanzahl, ausgewählte srcset-Kandidaten, Antwortbytes, Decoding und sichtbare Unterschiede zum Original. Gemischte Erst- und Wiederholungsbesuche erschweren die Trennung von Komprimierungs- und Cache-Effekten.
Weniger übertragene Daten betreffen die Größe heruntergeladener Dateien. LCP für Hauptinhalte, INP für Reaktionsfähigkeit und CLS für Layoutverschiebungen werden gesondert gemessen. Ein kleineres Hauptbild verbessert LCP möglicherweise weniger als erwartet, wenn Serverantworten oder andere Ressourcen den Engpass bilden. Lighthouse misst unter festgelegten Testbedingungen; Nutzerdaten entstehen auf unterschiedlichen Geräten und Netzen. Speichern Sie beide Ergebnisse getrennt, um langsame Abschnitte zu finden.
Halten Sie bei Felddaten Zeitraum und Aggregation auf Seiten- oder Websiteebene fest. Fehlt wegen unzureichender Daten ein Wert, vermerken Sie ihn als unbestätigt. Vergleichen Sie unmittelbar nach Bereitstellung geprüfte Datenmengen getrennt von später aggregierter Nutzererfahrung sowie Suchimpressionen und Klicks. Vorher-nachher-Werte für Impressionen, Klicks und KI-Zitate gehören in ihre jeweiligen Beobachtungstabellen.
Die vom Browser gewählte Bildadresse direkt prüfen
make-image-demo.mjs erzeugt zwei geometrische Bilder und eine HTML-Datei. Die WebP-Bilder sind 640px und 1280px breit. Führen Sie im Next.js-Ordner npm install --save-dev sharp@0.35.5 und danach node make-image-demo.mjs aus. Starten Sie den Server nach der Dateierzeugung. Läuft er bereits, starten Sie ihn neu, damit die public-Dateien erkannt werden.
// Installation: npm install --save-dev sharp@0.35.5
// Ausführen: node make-image-demo.mjs (Stammordner des Next.js-Übungsprojekts)
import fs from 'node:fs/promises';
import sharp from 'sharp';
await fs.mkdir('public/photos', { recursive: true });
const source = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720">
<rect width="1280" height="720" fill="#e8eff5"/>
<path d="M160 540V180H1120V540Z" fill="#31516b"/>
<path d="M240 260H1040M240 360H1040M240 460H1040" stroke="#e8eff5" stroke-width="32"/>
</svg>`);
for (const width of [640, 1280]) {
await sharp(source).resize({ width }).webp({ quality: 80 }).toFile(`public/photos/shelves-${width}.webp`);
}
await fs.writeFile('public/image-demo.html', `<!doctype html>
<html lang="de"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Übung zur Bildgrößenauswahl</title>
<style>body{margin:16px}main{max-width:960px;margin:auto}img{display:block;width:100%;height:auto}</style>
<main><h1>Übung zur Bildgrößenauswahl</h1>
<img src="/photos/shelves-640.webp"
srcset="/photos/shelves-640.webp 640w, /photos/shelves-1280.webp 1280w"
sizes="(max-width: 992px) calc(100vw - 32px), 960px"
width="1280" height="720" alt="Übungsgrafik mit drei Regalreihen"
fetchpriority="high">
<p>Ein geometrisches Übungsbild zur Prüfung der Dateiauswahl nach Größe.</p>
</main></html>`);
console.log('Created public/image-demo.html and two WebP fixtures');
Buffer wandelt die SVG-Zeichenfolge in Bytes für die Dateiverarbeitung um; resize, webp und toFile von sharp speichern Varianten jeder Breite. src ist die Standardadresse, srcset nennt Kandidaten und tatsächliche Pixelbreiten, sizes die Anzeigebreite im CSS-Layout. Die beidseitigen 16px-Ränder des body und die maximale main-Breite von 960px stimmen mit sizes überein. width und height reservieren einen Bereich im Verhältnis 16:9.
Öffnen Sie /image-demo.html auf dem Übungsserver bei 390px und 1440px Fensterbreite. Prüfen Sie in der Console document.querySelector("img").currentSrc, naturalWidth und getBoundingClientRect().width. Lesen Sie angeforderte Datei und Datenmenge in Network. DPR und Cache beeinflussen die Auswahl; auch schmale Ansichten können eine große Datei wiederverwenden. Vergleichen Sie neue Kontexte mit gleichen DPR- und Cache-Bedingungen.
Das wichtige Bild im ersten sichtbaren Bereich hat kein loading="lazy". Bei unteren Bildern lässt sich nach Lazy Loading der Anfragezeitpunkt vor und nach dem Scrollen prüfen. alt beschreibt die Bildbedeutung; reine Dekoration erhält einen leeren alt-Text. Bei 404 eines Kandidaten public-Pfad und srcset prüfen; bei dauerhaft zu großen Dateien sizes mit dem tatsächlichen Layout vergleichen.
srcset und sizes lassen den Browser einen geeigneten Kandidaten wählen. Eine passende Variante statt eines großen Originals kann bei 390px die Datenmenge reduzieren; width und height reservieren Platz vor der Bildankunft. Prüfen Sie die Änderungen an currentSrc, Antwortbytes und Layoutverschiebungen. Zeitplaninformationen, die nur im Bild stehen, benötigen zusätzlich erklärenden Seitentext.
Original- und Variantenanfragen desselben CMS-Bilds vergleichen
Die vorige Übung wählte vorbereitete statische Dateien. Diese Übung prüft bei Anfragen der CMS-Bild-ID photo den Veröffentlichungsstatus und erzeugt oder verwendet Varianten. Das HTML behält dasselbe Layout: Die Originalansicht gibt die Originaladresse aus, die Variantenansicht srcset-Kandidaten. Der Vergleich zeigt, ob ein per CSS klein dargestelltes Foto tatsächlich eine kleine Datei ist.
Speichern Sie in einem leeren Ordner Öffentliche Daten: media-store.mjs, Dokumente und Zeit: identity-time.mjs, Implementierung: article-jsonld.mjs, URLs und Crawling: feed.mjs. Speichern Sie den folgenden Code als media-demo.mjs. Verwenden Sie Node.js 22 und sharp 0.35.5 in einem separaten Ordner außerhalb der bisherigen Next.js-Übung.
import http from 'node:http';
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import sharp from 'sharp';
import { createMediaStore } from './media-store.mjs';
import { article, renderArticle, escapeHtml } from './article-jsonld.mjs';
import { renderFeed } from './feed.mjs';
export async function sampleImage() {
// Reproduzierbares geometrisches Bild. Ein echtes Foto als Argument übergeben.
const rectangles = Array.from({ length: 1600 }, (_, i) =>
`<rect x="${(i * 67) % 1600}" y="${(i * 43) % 1000}" width="55" height="55"
fill="hsl(${i % 360},70%,50%)"/>`).join('');
return sharp(Buffer.from(`<svg width="1600" height="1000">${rectangles}</svg>`))
.jpeg({ quality: 95 }).toBuffer();
}
export function createDemoServer(store, origin = 'http://127.0.0.1:3031') {
return http.createServer(async (req, res) => {
const send = (status, type, body, extra = {}) => {
res.writeHead(status, { 'Content-Type': type, 'Cache-Control': 'private, no-store, max-age=0', ...extra });
res.end(body);
};
try {
const url = new URL(req.url, origin);
if (req.method !== 'GET') return send(405, 'text/plain', 'Method not allowed');
if (url.pathname === '/media/photo') {
const width = url.searchParams.has('w') ? Number(url.searchParams.get('w')) : null;
const r = await store.read(width);
return send(r.status, 'text/plain', r.body, r.headers);
}
if (url.pathname === '/rss.xml') {
// Diese Übung veröffentlicht und entfernt Artikel und ein Foto gemeinsam.
const visible = (await store.read()).status === 200;
return send(200, 'application/rss+xml; charset=utf-8', renderFeed(visible ? [article] : [], origin,
new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')));
}
if (url.pathname === '/video.html' || url.pathname === '/talk.mp4') {
const body = await readFile(new URL('.' + url.pathname, import.meta.url));
if (url.pathname.endsWith('.html')) return send(200, 'text/html; charset=utf-8', body);
const headers = { 'Accept-Ranges': 'bytes', 'Content-Length': body.length };
if (!req.headers.range) return send(200, 'video/mp4', body, headers);
// Einzelner Byte-Bereich für ein kleines lokales Video. Im Betrieb Dateistreams oder Medienserver verwenden.
const match = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range);
let start = match?.[1] ? Number(match[1]) : Math.max(0, body.length - Number(match?.[2]));
let end = match?.[1] && match[2] ? Math.min(Number(match[2]), body.length - 1) : body.length - 1;
if (!match || (!match[1] && !match[2]) || !Number.isSafeInteger(start)
|| !Number.isSafeInteger(end) || start > end || start >= body.length)
return send(416, 'text/plain', '', { 'Content-Range': `bytes */${body.length}` });
return send(206, 'video/mp4', body.subarray(start, end + 1), {
...headers, 'Content-Length': end - start + 1,
'Content-Range': `bytes ${start}-${end}/${body.length}`,
});
}
if (url.pathname !== '/' && url.pathname !== article.path) return send(404, 'text/plain', 'Not found');
const candidates = await Promise.all([320, 640, 1280].map(async width => {
const r = await store.read(width);
return { ...r, path: `/media/photo?w=${width}` };
}));
if (candidates.some(c => c.status !== 200)) return send(404, 'text/plain', 'Not found');
const largest = candidates.at(-1);
if (url.pathname === article.path)
return send(200, 'text/html; charset=utf-8', renderArticle(origin, largest).html);
// Kleine Originale nicht vergrößern. Pro resultierender Breite nur einen Kandidaten behalten.
const unique = [...new Map(candidates.map(c => [c.width, c])).values()];
const srcset = unique.map(c => `${c.path} ${c.width}w`).join(', ');
const original = url.searchParams.get('mode') === 'original';
return send(200, 'text/html; charset=utf-8', `<!doctype html><html lang="de"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Übung zur CMS-Bildauslieferung</title><link rel="alternate" type="application/rss+xml" href="/rss.xml">
<style>body{margin:16px;font-family:sans-serif}img{display:block;width:min(100%,640px);height:auto}</style>
</head><body><h1>Auslieferung: ${original ? 'Original' : 'Variante'}</h1>
<p><a href="/?mode=original">Original anzeigen</a> · <a href="/">Varianten anzeigen</a>
· <a href="${article.path}">Artikel</a> · <a href="/video.html?t=4">Video ab Sekunde 4</a></p>
<img src="${original ? '/media/photo' : largest.path}"
${original ? '' : `srcset="${escapeHtml(srcset)}" sizes="(max-width:672px) calc(100vw - 32px), 640px"`}
width="${largest.width}" height="${largest.height}" alt="Geometrisches Bild zum Vergleich der Bildauslieferung">
</body></html>`);
} catch (error) {
send(error.code === 'ENOENT' ? 404 : 500, 'text/plain', 'Resource unavailable');
}
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const source = process.argv[2] ? await readFile(process.argv[2]) : await sampleImage();
const store = await createMediaStore(source);
createDemoServer(store).listen(3031, '127.0.0.1', () => console.log('http://127.0.0.1:3031'));
}
npm init -y
npm install sharp@0.35.5
node media-demo.mjs
# Antworten in einem anderen Terminal vergleichen
curl -sS -D original-headers.txt http://127.0.0.1:3031/media/photo -o original.jpg
curl -sS -D variant-headers.txt "http://127.0.0.1:3031/media/photo?w=640" -o variant.webp
# Nach dem Beenden des Servers ist auch ein eigenes Foto verwendbar.
# node media-demo.mjs ./photo.jpg
sampleImage erzeugt ein geometrisches JPEG mit 1600×1000 Pixeln. Ein eigener Fotopfad als Aufrufargument durchläuft dieselbe Verarbeitung. Der Server bindet nur an 127.0.0.1 und bietet keinen öffentlichen Upload. /media/photo liefert das Original, ?w=640 eine Variante, /news/new-hours den Artikel und /rss.xml den Feed. Der Videolink funktioniert nach Vorbereitung der beiden Dateien und des MP4 aus dem folgenden Inhaltskapitel.
Die Kandidatenbreiten sind 320, 640 und 1280. sizes berücksichtigt insgesamt 32px seitlichen Abstand und maximal 640px Anzeigebreite. Bei 390px Fensterbreite beträgt die Anzeige 358px; DPR 2 benötigt etwa 716 Pixel. Daher kann auch eine schmale Ansicht den 1280-Kandidaten wählen. Browserentscheidung, Cache und Verbindung wirken ebenfalls mit; allein die Fensterbreite rechtfertigt keine Erwartung einer 320-Datei.
Bei kleinen Originalen können mehrere Kandidaten dieselbe tatsächliche Breite erhalten. unique fasst solche Kandidaten zusammen. srcset verwendet die resultierende Breite, width und height von img das tatsächliche Verhältnis. Vergleichen Sie nach Größenänderung nicht nur currentSrc, sondern auch Antwortbytes und Breite aus den Dateimetadaten.
Dieser Server bereitet vor dem HTML alle drei Varianten vor, um tatsächliche Abmessungen zu erhalten. Ein produktives CMS kann Varianten und Größen beim Upload speichern, sodass die erste Seitenanfrage nicht auf drei Umwandlungen wartet. Verknüpfen Sie Uploadabschluss und Veröffentlichungsbereitschaft und bestimmen Sie ein Ersatzbild bei Fehlern. Bei Fotoersatz auch Alternativtext und Beschreibung ändern.
Die Übung behandelt Artikel und ein Foto als gemeinsam veröffentlichtes Material. Nach Rücknahme des Fotos antworten Artikel und Startseite mit 404; auch RSS enthält den Artikel nicht mehr. Kann ein reales CMS Fotos unabhängig wechseln, speichern Sie Text- und Bildstatus getrennt und entscheiden Sie über die Auslieferung des Texts ohne privates Bild.
Dateigröße, Download- und Anzeigezeitpunkt im selben Durchlauf erfassen
inspect-image.mjs führt acht Kombinationen aus 390/1440px, DPR 1/2 und Original/Variante in neuen Browserkontexten aus. Es zählt Antwortbytes und liest mit sharp die tatsächliche Dateibreite. Im Browser werden Anzeigebreite, currentSrc, Resource Timing und LCP-Kandidaten erfasst. Nach img.decode() werden Frames abgewartet, um die Darstellung zu prüfen.
Rufen Sie vor der Messung einmal die Startseite auf, um serverseitige Varianten vorzubereiten. Neue Browserkontexte verringern Einflüsse früherer Bildauswahl und des Browsercaches. Für die Kosten der ersten Umwandlung starten Sie den Server neu und erfassen die Verarbeitungszeit der ersten Anfrage gesondert.
import { chromium } from 'playwright';
import sharp from 'sharp';
const browser = await chromium.launch();
const rows = [];
try {
// Servervarianten zuerst vorbereiten. Jedes Mal einen neuen Browserkontext erstellen.
const warm = await fetch('http://127.0.0.1:3031/');
if (!warm.ok) throw new Error('Image server unavailable');
await warm.text();
for (const width of [390, 1440]) for (const dpr of [1, 2]) for (const mode of ['original', 'variants']) {
const context = await browser.newContext({ viewport: { width, height: 900 }, deviceScaleFactor: dpr });
try {
const page = await context.newPage();
await page.addInitScript(() => {
window.imageLcp = [];
new PerformanceObserver(list => {
window.imageLcp.push(...list.getEntries().map(e => ({ time: e.startTime, url: e.url, tag: e.element?.tagName })));
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
const imageResponse = page.waitForResponse(r => new URL(r.url()).pathname === '/media/photo');
await page.goto('http://127.0.0.1:3031/' + (mode === 'original' ? '?mode=original' : ''));
const response = await imageResponse;
const bytes = await response.body();
const metadata = await sharp(bytes).metadata();
await page.locator('img').evaluate(img => img.decode());
// Zwei Frames abwarten, um Darstellung und LCP-Einträge nach dem Decoding zu lesen.
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
const observed = await page.locator('img').evaluate(img => {
const resource = performance.getEntriesByName(img.currentSrc).at(-1);
return { currentSrc: img.currentSrc, cssWidth: img.getBoundingClientRect().width,
naturalWidth: img.naturalWidth, dpr: devicePixelRatio,
resource: resource && { start: resource.startTime, responseStart: resource.responseStart,
responseEnd: resource.responseEnd, encodedBodySize: resource.encodedBodySize, transferSize: resource.transferSize },
lcp: window.imageLcp.at(-1) ?? null };
});
rows.push({ width, mode, ...observed, bodyBytes: bytes.length,
fileWidth: metadata.width, fileHeight: metadata.height, format: metadata.format,
cacheControl: response.headers()['cache-control'] });
} finally { await context.close(); }
}
} finally { await browser.close(); }
console.log(JSON.stringify(rows, null, 2));
# Bei laufendem Bildserver in einem anderen Terminal desselben Übungsordners ausführen
npm install --save-dev playwright@1.59.1
npx playwright install chromium
node inspect-image.mjs > image-observations.json
bodyBytes ist die Größe des empfangenen Bildinhalts, fileWidth die Pixelbreite der Datei und cssWidth die Anzeigebreite. responseStart bis responseEnd umfasst den Empfang; start bis responseStart unter anderem Anfragevorbereitung und Serverwartezeit. transferSize meldet die übertragene Größe einschließlich Headern. Bei entfernten Adressen können Werte etwa aufgrund von Timing-Allow-Origin null sein.
Lesen Sie tag und url des LCP gemeinsam, um Foto und Überschrift als größtes Element zu unterscheiden. Trotz kleinerem Bilddownload bleibt die Anzeige verzögert, wenn HTML spät ankommt oder die Bildanfrage spät entdeckt wird. Der Code speichert acht lokale Durchläufe. Vergleichen Sie Produktionsgeschwindigkeit mit mehreren Läufen bei gleichem Browser, Gerät, Netzwerk und Cache und betrachten Sie die Verteilung. Decoding-Zeit und genaue Ursachen der Hauptthread-Blockierung zeigen Performance-Aufzeichnungen des Browsers.
TBT summiert im Beobachtungszeitraum nach FCP (First Contentful Paint) die Anteile von Hauptthread-Aufgaben über 50ms. Eine 120ms-Aufgabe erzeugt 70ms Blockierungszeit. INP bewertet die Verzögerung von Nutzerinteraktionen wie Klicks und Eingaben bis zur Bildschirmreaktion. Auch Seiten mit niedrigem TBT können langsam reagieren, wenn ein später geöffnetes Menü eine aufwendige Ereignisbehandlung hat. Untersuchen Sie Ladeaufzeichnungen und tatsächliche Interaktionen getrennt. Zusammenhang zwischen TBT und INP
Späte Schriftdateien können Text verzögern oder beim Wechsel von der Ersatzschrift Zeilenumbrüche verschieben. font-display: swap verwendet nach kurzer Wartezeit eine Ersatzschrift; preload startet die benötigte Schriftanfrage früher. Unterschiedliche Schriftbreiten können CLS bestehen lassen. Laden Sie nur Dateien des ersten sichtbaren Bereichs vor und prüfen Sie auf Mobilgeräten tatsächliche Schriftanfragen, Textanzeige und verschobene Zeilenumbrüche. Funktionsweise von font-display
In einem lokalen Durchlauf dieses geometrischen Bilds hatte das Original 238.236 Byte, das 640px-WebP 28.676 Byte und das 1280px-WebP 49.546 Byte. Bei 390px und DPR 1 wurde 640px geladen, bei gleicher Fensterbreite und DPR 2 die 1280px-Datei. Das zeigt unterschiedliche Datenmengen je nach physischem Pixelbedarf desselben Originals. Bei geändertem Foto, geänderter Qualität oder anderem Encoder sind Dateigrößen neu zu messen.