VINUM Archiv-Seite: Von 320 Magazincovern zu Lazy-Loading mit Jahresfilter
Wie wir die Ladezeit der VINUM-Archivseite drastisch verbessert haben -- durch serverseitigen Jahresfilter, Lazy-Loading und JPG-Re-Encoding in TYPO3.
320 Cover auf einen Schlag – und keines davon klein
Das Online-Archiv von VINUM.eu listet alle bisher erschienenen Magazin-Ausgaben seit 2014. Über die Jahre sind dort 356 Ausgaben zusammengekommen, 320 davon mit hochauflösendem Cover. Optisch beeindruckend – technisch problematisch.
Die Herausforderung
Ein Blick in die Produktion hat den Ist-Zustand schnell sichtbar gemacht:
- 320 Cover-Dateien, davon 110 als PNG (Schnitt 2,1 MB, max. 15 MB) und 210 als JPG (Schnitt 5,9 MB, max. 22,7 MB).
- Alle Cover wurden in einer einzigen Response in voller Originalgröße ausgeliefert.
- Der Jahresfilter im Dropdown war rein clientseitig implementiert: Über JavaScript wurden die nicht passenden Karten per
display: noneausgeblendet – die Bilder im DOM blieben aber geladen. - Kein einziges
loading="lazy"an einem<img>-Tag.
Im ungünstigsten Fall musste der Browser also über 1 GB an Bildmaterial nachladen, nur um eine einzige Ausgabe anzuzeigen. Auf Mobilgeräten war das Archiv kaum benutzbar, und selbst auf dem Desktop dauerte das initiale Rendering mehrere Sekunden.
Interessant: Im ursprünglichen Kundenticket war von “den großen PNGs” die Rede. Die Datenbankanalyse hat aber klar gezeigt, dass die JPGs die eigentlichen Schwergewichte waren. Ohne diesen Blick in die Produktionsdaten wäre die Optimierung auf die falsche Stelle gelaufen.
Unsere Lösung
1. Jahresfilter vom Browser in den Controller verlegen
Statt 320 Karten zu rendern und 285 davon im Browser zu verstecken, filtert jetzt der Server. Die listAction im IssueController akzeptiert einen optionalen ?year-Parameter und liefert standardmäßig nur das aktuelle Jahr aus – aktuell 35 Ausgaben statt 320:
public function listAction(?string $year = null): ResponseInterface
{
$years = $this->issueRepository->findYears();
$selectedYear = $year ?? (string)($years[0] ?? '');
$issues = $this->issueRepository->findByYear($selectedYear);
$this->view->assignMultiple([
'years' => $years,
'selectedYear' => $selectedYear,
'issues' => $issues,
]);
return $this->htmlResponse();
}
Im Repository werden die Jahre über einen QueryBuilder ermittelt – bewusst nicht über Extbase, um nicht jedes Issue-Objekt zu hydratieren, nur um daraus eine Jahreszahl zu lesen:
public function findYears(): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vinum_domain_model_issue');
return $queryBuilder
->select('year')
->distinct()
->from('tx_vinum_domain_model_issue')
->where(
$queryBuilder->expr()->neq('cover_image', $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0),
)
->orderBy('year', 'DESC')
->executeQuery()
->fetchFirstColumn();
}
Das Dropdown im Template wird serverseitig mit <f:form.select> befüllt und submittet beim change-Event. Deep-Links wie /archiv/?tx_vinum_issues[year]=2024 funktionieren ohne cHash-Fehler, weil die Argumente getypt sind.
2. Cover-Breite deckeln und auf JPG normalisieren
Der bestehende SafeImageViewHelper der VINUM-Plattform unterstützt bereits maxWidth, fileExtension und loading-Attribute – die wurden nur noch nie genutzt:
<vinum:safeImage
src="{issue.coverImage.uid}"
treatIdAsReference="true"
maxWidth="700"
fileExtension="jpg"
loading="lazy" />
Drei Hebel in einer Zeile:
maxWidth="700"reduziert übergroße Originale auf eine sinnvolle Darstellungsgröße. Kleinere Originale werden nicht hochskaliert.fileExtension="jpg"zwingt TYPO3, auch PNGs als JPG auszuspielen. Für Magazincover, die ohnehin keine Transparenzen enthalten, ist das deutlich effizienter.loading="lazy"ist das native Lazy-Loading der Browser – kein JavaScript, kein Framework, keine Abhängigkeit.
Die Re-Encoding-Qualität haben wir global in typo3/config/system/settings.php festgesetzt:
'GFX' => [
'jpg_quality' => 90,
],
90 ist ein bewährter Wert für Cover-Bilder: visuell kaum vom Original zu unterscheiden, aber typischerweise 5–10× kleiner als das JPG-Original aus der Druckvorstufe.
3. Client-Filter behutsam zurückbauen
Im Frontend-JavaScript (Archive.js) blieb der Filter nach Ausgabennummer erhalten – der arbeitet jetzt auf maximal 35 Karten pro Jahr und ist daher unkritisch. Entfernt wurde nur die Logik, die das Jahres-Dropdown clientseitig befüllt und die Cover-Karten ein- und ausblendet. Weniger Code, weniger Sonderfälle.
Das Ergebnis
Die Archivseite lädt nun in einem Bruchteil der ursprünglichen Zeit:
- Statt 320 Cover werden initial nur die ~35 Ausgaben des aktuellen Jahres geladen.
- Jedes Cover ist auf maximal 700 px Breite normalisiert, als JPG mit Qualität 90, und wird nativ lazy geladen.
- Deep-Links auf konkrete Jahre funktionieren ohne JavaScript und bleiben für Suchmaschinen indexierbar.
- Die originalen Cover-Dateien im
fileadminbleiben unangetastet – die kleineren Varianten landen ausschließlich im TYPO3-Image-Cache.
Spannend an diesem Projekt war weniger die einzelne Technik (Lazy-Loading, ImageProcessor, server-seitiger Filter sind alle Standard) als der Weg dorthin: Erst die Produktionsdaten analysieren, dann die Annahmen aus dem Ticket prüfen – und dann gezielt am richtigen Hebel ansetzen.
Brauchen Sie eine ähnliche Optimierung?
Performance-Probleme auf gewachsenen TYPO3-Seiten lassen sich fast immer auf eine Handvoll klassischer Muster zurückführen: clientseitige Filter über vollständige Listen, unkomprimierte Bilder, fehlendes Lazy-Loading, ungebatchte Repository-Calls. Wir analysieren Ihre konkrete Situation – inklusive Blick in die Datenbank und Logs – und benennen die Hebel mit dem besten Aufwand-Nutzen-Verhältnis.
Über den Autor
Christopher Zechendorf
Christopher Zechendorf leitet die ext.dev GmbH und bringt über 25 Jahre Erfahrung in Webentwicklung, CMS-Systemen und Infrastruktur mit.