Messen statt vermuten: 18 Abfragen auf 8, Ladebudget, Blaettern ueberall

Arbeitspaket M sagte "Abfragen je Seite zaehlen. Messen, dann zusammenfassen."
Messen braucht ein Messgeraet, also gibt es jetzt eines: ein Zaehler als
Doctrine-Middleware, dessen Zahl ausserhalb der Produktion im Kopf
X-Kant-Queries steht. Keine Profiler-Maschinerie, eine Zahl.

Die erste Messung: die Startseite brauchte 18 Abfragen. Nicht wegen einer
komplizierten Seite, sondern weil SiteSettings::load() und Theme::load() je
Aufrufer dieselbe Zeile erneut holten - Renderer, strukturierte Daten,
Einwilligungsschalter, Vorlagen. Beide merken sich ihren Stand jetzt fuer die
Dauer einer Anfrage: 18 auf 8.

Dabei ist mir die klassische Falle des Merkens selbst passiert, und ein
bestehender Test hat sie sofort gezeigt: ich leerte den Merker am Anfang des
Speicherns. Das Speichern liest den Stand aber noch einmal und fuellte ihn
damit erneut - der Rest der Anfrage bekam den alten Stand. Geleert wird jetzt
nach dem Schreiben.

Das Ladebudget wird gemessen statt behauptet: die Browserpruefung liest je
Seite HTML-Groesse, CSS-Groesse, Anzahl Anfragen, Anzahl Skripte und
Abfragezahl und prueft Obergrenzen. Startseite: 7,4 KB HTML, 22,7 KB CSS, 2
Anfragen, 8 Abfragen, 0 Skripte. Die Null ist die wichtigste Zahl - sie haelt
als Test fest, was bisher nur Absicht war.

Das erste Bild einer Seite ist nicht mehr lazy. Es ist meistens das groesste
Ding ueber der Falz, und es zu verzoegern verzoegert genau das Pixel, auf das
jemand wartet. Der Renderer bestimmt es in Renderreihenfolge, Kopfbereich
zuerst.

Eine feste Obergrenze ist kein Blaettern: der Fassungsverlauf hoerte bei
dreissig auf, die Einsendungen bei fuenfzig, die Weiterleitungen zeigten bis
zu zweitausend auf einmal. Das Erste sieht aus wie "es gibt dreissig" und ist
das Einzige, was eine Historie nicht tun darf. Alle drei blaettern jetzt, mit
derselben Rechnung und demselben Pager wie Beitragsarchiv und Mediathek.

347 PHP-Tests / 1422 Assertions unter PHP 8.2.12 und 8.4.22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 21:56:09 +02:00
1 parent 8c49f6a67a
commit 269fb223af
27 files changed
+480 -54

No files matched your search

+83 -5
View File
@@ -1303,16 +1303,19 @@ final class CmsTest extends TestCase
$second = DemoContent::starter('Zweite Fassung');
$this->pages->saveDraft('page-1', $first, 0);
$this->pages->saveDraft('page-1', $second, 1);
// The history is paged now, so it says how many there are instead of stopping silently.
$revisions = $this->pages->revisions('page-1');
self::assertSame(['Zweite Fassung', 'Erste Fassung'], array_column($revisions, 'title'), 'Newest first.');
self::assertTrue($revisions[0]['draft']);
self::assertSame(['Zweite Fassung', 'Erste Fassung'], array_column($revisions['items'], 'title'), 'Newest first.');
self::assertSame(2, $revisions['total']);
self::assertSame(1, $revisions['pages']);
self::assertTrue($revisions['items'][0]['draft']);
$oldest = end($revisions);
$oldest = end($revisions['items']);
$this->pages->restoreRevision('page-1', $oldest['id'], 2);
self::assertSame('Erste Fassung', $this->pages->findForEditor('page-1')['document']['title']);
// Restoring adds a revision instead of overwriting one, so nothing is lost.
self::assertCount(3, $this->pages->revisions('page-1'));
self::assertSame('Zweite Fassung', $this->pages->revisions('page-1')[1]['title']);
self::assertSame(3, $this->pages->revisions('page-1')['total']);
self::assertSame('Zweite Fassung', $this->pages->revisions('page-1')['items'][1]['title']);
$foreign = $this->pages->saveDraft('page-2', DemoContent::starter('Fremd'), 0);
$this->expectException(InvalidPage::class);
@@ -1781,6 +1784,81 @@ final class CmsTest extends TestCase
]);
}
public function testEveryLongListSaysHowMuchThereIsAndLetsYouReachIt(): void
{
// Thirty revisions: a fixed cap would show some and say nothing about the rest.
for ($index = 1; $index <= 30; ++$index) {
$this->pages->saveDraft('page-1', DemoContent::starter('Fassung '.$index), $index - 1);
}
$first = $this->pages->revisions('page-1', 1, 25);
self::assertCount(25, $first['items']);
self::assertSame(30, $first['total']);
self::assertSame(2, $first['pages']);
self::assertSame('Fassung 30', $first['items'][0]['title'], 'Newest first.');
$second = $this->pages->revisions('page-1', 2, 25);
self::assertCount(5, $second['items']);
self::assertSame('Fassung 1', end($second['items'])['title']);
// A page beyond the end shows the last one instead of an empty screen.
self::assertSame(2, $this->pages->revisions('page-1', 99, 25)['page']);
// The same for redirects, which may run into the thousands.
for ($index = 1; $index <= 12; ++$index) {
$this->redirects->save('alt-'.$index, 'home', 301);
}
$redirects = $this->redirects->all(1, 5);
self::assertCount(5, $redirects['items']);
self::assertSame(12, $redirects['total']);
self::assertSame(3, $redirects['pages']);
self::assertCount(2, $this->redirects->all(3, 5)['items']);
}
public function testTheFirstImageOfAPageIsNotLazy(): void
{
$document = DemoContent::starter('Mit Bildern');
$document['schemaVersion'] = 3;
foreach (['erstes', 'zweites'] as $id) {
$document['blocks'][0]['children'][] = [
'id' => $id, 'type' => 'image',
'props' => ['src' => '/assets/image-placeholder.svg', 'alt' => 'Ein Bild', 'decorative' => false, 'width' => 800, 'height' => 600],
];
}
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
$html = $this->request('/')->getContent();
// The first one is usually the largest thing above the fold; deferring it delays exactly
// the pixel a visitor waits for.
self::assertMatchesRegularExpression('/<img[^>]*id="erstes"|id="erstes"[^>]*>/', $html);
$first = $this->imageTag($html, 'erstes');
$second = $this->imageTag($html, 'zweites');
self::assertStringContainsString('loading="eager"', $first);
self::assertStringContainsString('fetchpriority="high"', $first);
self::assertStringContainsString('loading="lazy"', $second);
self::assertStringNotContainsString('fetchpriority', $second);
}
public function testAPageDoesNotAskTheDatabaseOverAndOver(): void
{
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', DemoContent::pages()['home'], 0), 1);
$response = $this->request('/');
$queries = (int) $response->headers->get('X-Kant-Queries');
// A ceiling, not an exact number: the point is that settings and theme are read once per
// request instead of once per caller. Before that they were read eighteen times.
self::assertGreaterThan(0, $queries, 'The counter has to be wired up at all.');
self::assertLessThanOrEqual(12, $queries, sprintf('Die Startseite braucht %d Abfragen.', $queries));
}
/** The img tag belonging to one block id, so the attributes can be read for that one image. */
private function imageTag(string $html, string $id): string
{
$figure = strpos($html, 'id="'.$id.'"');
self::assertNotFalse($figure, 'Bild '.$id.' fehlt.');
$start = strpos($html, '<img', $figure);
self::assertNotFalse($start);
return substr($html, $start, (int) strpos($html, '>', $start) - $start);
}
public function testAPageCanUseAnotherHeaderOrNoneAtAll(): void
{
// The seeded areas plus a second header for landing pages.