PageSpeed 100/100. TYPO3 schneller machen, richtig viel schneller!
Ich hatte bereits vor langer Zeit einen Artikel darüber geschrieben, wie man sein TYPO3 Projekt schneller machen und dabei Werte von 100 bei PageSpeed Insights (aka Lighthouse) erreichen kann. Nun hat sich seit damals viel getan. Die Technik hat sich weiter entwickelt und TYPO3 ist auch selbst schneller geworden. Aber wer jetzt hofft, ein Update reiche aus um die Punkte zu erhöhen - wird schnell enttäuscht. An meinem Projekt hat sich nichts geändert mit dem Update von php73 und TYPO3 9 LTS auf php74 und TYPO3 10 LTS. Auch der PageSpeed Test hat sich weiter entwickelt, und der Fokus liegt hier stärker auf der Optimierung für mobile Endgeräte. Vor allem aber ist die Seitengeschwindigkeit im mobilen Bereich ein Faktor für das Ranking. Aus dem Grund kann es wichtig werden, eine Optimierung durchzuführen und damit auch die Nutzererfahrung insgesamt zu steigern, und eben das Ranking zu verbessern.
In diesem Artikel geht es in erster Linie wieder um die technischen Aspekte, Abwägungen und Kompromisse die mit der Optimierung des System und der Seite einhergehen. Ich würde jetzt sagen dass eine Kanne Kaffee benötigt wird, so einfach ist es aber dann doch nicht. Glücklich ist der, der einen Vollautomaten neben sich stehen hat. Je nach Projektgröße wird das ganze absurd aufwendig, und ist mit Kompromissen verbunden, die man am Ende kaum mehr einem Kunden vermitteln kann. Man kann aber vieles berücksichtigen und das ganze in kommenden Projekten anwenden, und damit bleibt so eine Optimierung dann überschaubar. Oder man setzt nur einfachere Punkte um und kommt zumindeszt in den grünen Bereich. Aber wie es immer so ist, der Ehrgeiz kam und ich hole jetzt einfach das Maximum aus dem ganzen heraus.
Einleitung
Während man im Tab "Desktop" locker über 90 Punkte haben kann, schaut es im mobilen Bereich schon deutlich anders aus. Alles, was im Desktop gut zu sein scheint, passt bei Mobile dann nicht mehr wirklich. Die am einfachsten zu behebenden Probleme in PageSpeed Insights oder in der Lighthouse Chrome App sind demnach aus den Labdaten
- Speed Index
- First Contentful Paint
- Total Blocking Time
sowie aus den Empfehlungen
- Bilder in modernen Formaten bereitstellen
- JavaScript komprimieren
- CSS komprimieren
- Darauf achten, dass der Text während der Webfont-Ladevorgänge sichtbar bleibt
- Statische Inhalte mit einer effizienten Cache-Richtlinie bereitstellen
Etwas aufwendiger kann es dann unter Umständen bei diesen Punkten aus den Labdaten
- Largest Contentful Paint
- Time to Interactive
- Cumulative Layout Shift
sowie aus den Empfehlungen
- Wichtige Anforderungen vorab laden
- Übermäßige DOM-Größe vermeiden
- Ressourcen beseitigen die das Rendering blockieren
werden.
Richtig schwierig wird es dann in der Regel bei den letzten Punkten aus den Empfehlungen:
- Nicht verwendetes CSS entfernen
- Nicht genutztes JavaScript entfernen
Das Problem bei diesen Punkten ist, dass damit CSS und JS gemeint ist, die der Browser für den aktuellen sichtbaren Bereich nicht benötigt. Abspecken ist hier nicht so einfach, denn es gibt ja auch Bereiche, die während dem laden im nicht sichtbaren Bereich sind, die aber trotzdem Funktion und Styling benötigen, wenn sie beim scrollen sichtbar werden.
Die in diesem Artikel gezeigten Lösungen sind kein Garant für das sichere erreichen der vollen Punkte, außerdem benötigt man im Normalfall auch nicht die kompletten 100 Punkte. Erfahrungsgemäß ist zwischen 80 und 100 Punkten keine deutliche Zunahme der Geschwindigkeit mehr im Browser zu merken. Außerdem sei darauf hingewiesen, dass dieser Artikel nur ein Leitfaden sein kann, da jedes Projekt anders aufgebaut ist und sich von der technischen Umsetzung dann auch zu diesem Artikel unterscheidet. Ich versuche den Artikel so aufzubauen, dass die einfach umsetzbaren Sachen zuerst abgehandelt werden. Los gehts!
Voraussetzungen
Ich gehe in diesem Artikel von folgenden Voraussetzungen und Skills aus:
- Der verwendete Webserver ist korrekt konfiguriert und der Host ist schnell. Es gibt keine langen Latenzen
- Für JavaScript und CSS wird ein Front-End-Build verwendet. In meinem Fall ist das Gulp, als Paketmanager npm und NodeJS.
- Es werden die folgenden Extensions benötigt: plan2net/webp, lochmueller/staticfilecache, t3/min
- Optionale Extension: mindshape/mindshape-cookie-consent wenn ein Cookie Consent verwendet wird.
- An Skills wird benötigt: TypoScript, HTML, CSS und JavaScript
Das Cookie Consent Tool von Mindshape nutze ich, da ich Requests einsparen und möglichst auf externe Ressourcen verzichten möchte. Einzige Ausnahme hier ist Google Analytics, aber da gehe ich später darauf ein.
Das CSS wird für dieses Projekt von 90Kb auf 37Kb verkleinert (tatsächlich entfernt) und das JavaScript wird von 489Kb auf 35Kb verkleinert. Hier wird also mehr als 450Kb an JavaScript Code dem Projekt "entnommen". Aber dazu später mehr.
Schritt 2: JavaScript komprimieren
Schritt 3: CSS komprimieren
Schritt 5: Wichtige Anforderungen vorab laden
Schritt 7: Statische Inhalte mit einer effizienten Cache-Richtlinie bereitstellen
Schritt 8: Preconnect für externe Anforderungen
Werden beim Projekt externe Ressourcen verwendet, zum Beispiel Google Analytics oder Consent Management Systeme (darauf gehe ich an späterer Stelle noch genauer ein), kann es von Vorteil sein, Verbindung zum externen Server aufzunehmen, bevor die Ressource überhaupt angefordert wird. Dafür wird das preconnect
und das dns-prefetch
Tag verwendet. Mit Angabe dieser beiden Tags wird also die Verbindung vor dem Anfragen von externen Ressourcen aufgebaut, was sich positiv auf die meisten Labdaten wie First Contentful Paint, Largest Contentful Paint, Total Blocking Time und Time to Interactive auswirkt. In meinem Fall nutze ich das für Google Analytics. Die Tags kommen in den <head>
Bereich der Seite:
page {
headerData {
30 = TEXT
30.value (
<link rel="preconnect" href="https://www.google-analytics.com" crossorigin>
<link rel="dns-prefetch" href="https://www.google-analytics.com">
)
}
}
Schritt 9, CSS: "Ressourcen beseitigen, die das Rendering blockieren" und "Nicht verwendete CSS entfernen"
Schritt 10: Nicht genutztes JavaScript entfernen
Best Practice: Google Analytics
Google Analytics ist wohl eines der am meist genutzten externen Ressourcen. Google Analytics taucht aber auch oft in den Empfehlungen wie "Ressourcen beseitigen, die das Rendering blockieren" oder "Nicht genutztes JavaScript entfernen" oder auch in den Cache Richtlinien auf. Um dem vorzubeugen, kann man Analytics auf den lokalen Server kopieren und von hier aus einbinden. Gemacht wird das ganze mit einem Cronjob, der die JavaScript Datei speichert und anschließend den TYPO3 Cache leert. Zum Zeitpunkt dieses Artikels habe ich noch nichts Fertiges für den Fall. Allerdings sollte bedacht werden, dass die neue Datei auf Änderungen hin geprüft wird (Checksumme), und nur bei einer tatsächlichen Änderung die Datei überschriebt und den Cache leert. Die Datei kann dann ganz normal im Fußbereich der Seite geladen werden:
page {
includeJSFooterlibs {
footer = EXT:basetemplate/Resources/Public/JavaScript/footer-min.js
analytics = EXT:basetemplate/Resources/Public/JavaScript/analytics.js
analytics.defer = 1
}
}
Wichtig hierbei ist, dass Analytics nach der gebauten JavaScript Datei aus dem FE-Build geladen wird, und dass "Defer" angegeben wird. Damit wird der Code erst ausgeführt, wenn alles andere auf der Seite geladen und ausgeführt wurde. Somit kann Analytics das Rendering nicht blocken. Mit unserer Cache-Richtlinie für JavaScript wird die Datei auch im Browser sehr lange gecacht, spätestens bis die Datei mit einer neuen Version aktualisiert wurde, und der Cache geleert wurde. Das eigentliche Code-Snippet befindet sich in der footer-min.js. Aber dazu nachher noch weitere Informationen, beim Thema Cookie Consent.
Best Practice: Total Blocking Time und Largest Contentful Paint verringern am Beispiel von PrismJS
Die beiden genannten Punkte aus den Labdaten des PageSpeed Tests werden immer dann größer, wenn JavaScript DOM Elemente manipuliert werden. Je mehr manipuliert werden muss, umso länger benötigt der Browser die Änderungen zu berechnen. Die beste Möglichkeit ist natürlich, das HTML exakt so auszuliefern, wie es im Browser angezeigt werden soll. Das geht nicht immer, wie hier, im Fall von PrismJS. Bei PrismJS kommen einige Faktoren zusammen, die sich auch auf andere JavaScript Bibliotheken übertragen lassen:
- DOM wird manipuliert und Total Blocking Time steigt an
- aufgrund dessen verzögert sich Largest Contentful Paint
- die Time to Interactive verzögert sich weiter
- auf Seiten wo das DOM nicht durch die Bibliothek verändert wird, erscheint die Meldung "Nicht genutztes JavaScript entfernen"
In diesem Fall muss man nun zwei Dinge im Auge behalten: Zum einen die Seiten, in denen das DOM verändert wird, und zum anderen die Seiten, die nicht von Änderungen betroffen sind, die Bibliothek aber dennoch eingebunden wird. Bei Letzterem kann man PrismJS manuell starten lassen:
window.Prism = window.Prism || {};
Prism.manual = true;
Für die Manipulation des DOM kann man hingegen einen IntersectionObserver erstellen, der in diesem Fall PrismJS nur dann ausführt, wenn das entsprechende Element im oder in den Sichtbereich des Browsers kommt:
if (document.querySelectorAll('pre').length) {
var observer = new IntersectionObserver(function(pre) {
// console.log(pre);
pre.forEach(e => {
if(e['isIntersecting'] === true) {
Prism.highlightAllUnder(e['target']);
}
})
}, { threshold: [0] });
const pres = document.querySelectorAll("pre");
pres.forEach(pre => observer.observe(pre));
}
Diese Möglichkeiten sollten beim Projekt für alle Implementierungen genutzt werden. Wenn die "Time to Interactive" sehr klein ist, kann eine Webseite auch auf dem Handy sehr schnell bedient werden.
Best Practice: Cookie Consent Management lokal nutzen
Auch externe Cookie-Consent-Management-Systeme haben Einfluss auf die "Total Blocking Time", "Largest Contentful Paint" und den "Cumulative Layout Shift". Ich hatte bisher Cookiebot als Dienstleister eingebunden, leider ist mit diesem Anbieter die Punktezahl von 100 nicht zu erreichen. Aus dem diesem Grund ist das Cooke-Consent-Management jetzt lokal auf meinem Server, in der Form der TYPO3 Extension Mindshape Cookie Consent. Die benötigte JavaScript Datei ist nativ und benötigt kein jQuery, was ich sehr gut finde. Zudem lässt sich das JavaScript der Extension direkt in den FE-Build mit aufnehmen, wodurch ein zusätzlicher Request erspart wird. Und zu guter Letzt liegt auch bereits ein Teil des Modal HTML Code beim Ausliefern der Seite vor, sodass nicht zu viel per JS am DOM geändert werden muss (Stichwort "Total Blocking Time"). hier ein kurzer Überblick wie das ganze funktioniert:
Die Extension wird mittels
composer require mindshape/mindshape-cookie-consent
installiert, danach aktiviert und die Caches geleert. Anschließend wird das statische TypoScript in das Template eingebunden.
window.analyticsLoaded = false;
window.addEventListener('cookieConsent', function (event) {
if (event.detail.hasOption('_ga')) {
if (false === window.analyticsLoaded) {
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'XX-XXXXXXXX-X', 'auto');
ga('send', 'pageview');
window.analyticsLoaded = true;
}
} else {
// do not load analytics
}
});
Der Analytics-Code wird also erst ausgeführt, wenn die Einwilligung für dieses Cookie gegeben wurde.
Best Practice: Image Lazy Loading und Responsive Images
Was ist besser als 30 Bilder beim Öffnen einer Webseite zu laden? Richtig, nicht gleich 30 Bilder zu laden. Dafür gibt es ein tolles JavaScript: LazySizes. Großes kurz erklärt: Mit LazySizes werden Bilder erst geladen, wenn diese im Sichtbereich des Browsers benötigt werden. Wenn also eine Seite sehr lange ist, und im unteren Bereich viele Bilder hat, werden diese erst geladen, wenn sie benötigt werden. Dafür reicht es aus, das JavaScript von LazySizes einzubinden, und Bild Tags künftig so zu erstellen:
<img data-src="{f:uri.image(src:image.id, width: '450', cropVariant: 'default', crop: image.crop)}" alt="{image.alternative}" title="{image.title}" class="lazyload" />
Noch besser ist es allerdings, Bilder passend zur Größe des Browserfensters auszugeben. Auf großen Bildschirmen soll die große Version und auf kleinen Bildschirmen dann die kleine Version ausgegeben werden. Entsprechend für Retina Displays, dort werden die Bilder in zweifacher Größe ausgeben. Erreichbar wird das Ganze in etwa so:
<figure>
<picture>
<!--[if IE 9]><video style="display: none"><![endif]-->
<source
data-srcset="{f:uri.image(src:image.id, width: '440', cropVariant: 'default', crop: image.crop)} 1x, {f:uri.image(src:image.id, width: '880', cropVariant: 'default', crop: image.crop)} 2x"
media="(max-width: 400px)" />
<source
data-srcset="{f:uri.image(src:image.id, width: '520', cropVariant: 'default', crop: image.crop)} 1x, {f:uri.image(src:image.id, width: '1040', cropVariant: 'default', crop: image.crop)} 2x"
media="(max-width: 763px)" />
<source
data-srcset="{f:uri.image(src:image.id, width: '280', cropVariant: 'default', crop: image.crop)} 1x, {f:uri.image(src:image.id, width: '560', cropVariant: 'default', crop: image.crop)} 2x"
media="(max-width: 991px)" />
<source
data-srcset="{f:uri.image(src:image.id, width: '380', cropVariant: 'default', crop: image.crop)} 1x, {f:uri.image(src:image.id, width: '760', cropVariant: 'default', crop: image.crop)} 2x"
media="(max-width: 1199px)" />
<source
data-srcset="{f:uri.image(src:image.id, width: '450', cropVariant: 'default', crop: image.crop)} 1x, {f:uri.image(src:image.id, width: '900', cropVariant: 'default', crop: image.crop)} 2x" />
<!--[if IE 9]></video><![endif]-->
<img data-src="{f:uri.image(src:image.id, width: '450', cropVariant: 'default', crop: image.crop)}" alt="{image.alternative}" title="{image.title}" class="lazyload" />
</picture>
<f:if condition="{image.description}">
<figcaption>
{image.description}
</figcaption>
</f:if>
</figure>
Ich verwende daher immer eigene Content-Elemente, da hier die Elemente so umgesetzt werden, dass der Redakteur selbst nicht das Layout des Elements ändern kann. Das ist wichtig, wenn man zum Beispiel ein zweispaltiges Element hat, bei dem das Bild links ist: Auf dem Desktop nimmt es 50 % des Platzes ein, auf Tablets eventuell ebenfalls 50 % und auf Smartphones 100 %. Wenn der Redakteur selbst das Layout ändern könnte, würden die Abmessungen für die jeweiligen Bildschirmbreiten nicht mehr passen.
Die Kür: JavaScript und CSS Inline verwenden
Wer alles bis hierher umsetzen konnte, und nun ein kleines CSS und ein kleines JavaScript vorliegen hat, kann das ganze auch Inline in das HTML der Webseite integrieren. Bei diesem Projekt habe ich das gemacht und teste das eine Weile aus. Ein klarer Vorteil dabei ist, dass es keine Ressourcen gibt, die das Rendering blockieren, sowie die eingesparten zwei Requests. Der Nachteil ist, dass das CSS und JavaScript im HTML der Seite eingebettet ist, und natürlich bei jeder Seite, die nicht im Cache ist, erneut übertragen wird. Hier muss man dann einfach abwägen, was mehr Sinn ergibt. Das JavaScript und CSS lassen sich relative einfach aus dem FE-Build direkt Inline importieren. Ich nutze dazu gulp-inline-source-html
und das PageRendererTemplate aus dem Core. Das Template liegt in diesem Pfad: web/typo3/sysext/core/Resources/Private/Templates/PageRenderer.html
. Das Template kann in die eigene Extension übernommen werden, und mittels TypoScript verwendet werden.
Für den Build muss dann nur noch ein zusätzlicher Task definiert werden:
gulp.task('inlineSource', function () {
return gulp.src('../../packages/basetemplate/Resources/Private/Templates/Page/PageRendererTemplate.html')
.pipe(inlineSource())
.pipe(gulp.dest('../../packages/basetemplate/Resources/Private/Templates/Page/PageRendererInline/'))
});
Hiermit wird das Template aus gulp.src()
geladen, den Content wird eingefügt und nach gulp.dest()
geschrieben. Das Template, welches geladen wird, schaut so aus:
###XMLPROLOG_DOCTYPE###
###HTMLTAG###
###HEADTAG###
###METACHARSET###
###INLINECOMMENT###
###BASEURL###
###SHORTCUT###
###TITLE###
###META###
###HEADERDATA###
###JS_LIBS###
<link rel="stylesheet" href="../../../Public/css/style.css" inline/>
</head>
###BODY###
<script src="../../../Public/JavaScript/footer-min.js" inline></script>
###JS_LIBS_FOOTER###
###FOOTERDATA###
</body>
</html>
Wie gut zu sehen ist, ist je ein Eintrag zu einer CSS und ein Eintrag zu einer JavaScript Datei enthalten. Diese Pfade zeigen auf die fertigen CSS und JavaScript Dateien aus dem FE-Build. Dieser Code wird demnach mit dem Inhalt der Dateien ersetzt. Das war es schon. Hier noch das TypoScript für das einbinden des neuen PageRendererTemplate:
config {
pageRendererTemplateFile = EXT:basetemplate/Resources/Private/Templates/Page/PageRendererInline/PageRendererTemplate.html
}
Damit geht auch dieser Artikel zu Ende. Viel Erfolg beim Nachmachen. Das Web braucht schnelle Internetseiten!