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/webplochmueller/staticfilecachet3/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 1: Bilder in modernen Formaten bereitstellen

Diese Empfehlung dürften vermutlich viele beim ersten Test der Seite erhalten. Hier geht es einfach darum, anstelle von JPG oder PNG zum Beispiel das Format WebP, welches sich deutlich besser komprimieren lässt als andere Formate, und dabei die Bildqualität gut bleibt, zu verwenden Es gibt für TYPO3 eine sehr gute Extension die es erlaubt, alle vorhandenen Bilder weiterzunutzen. Technisch gesehen werden die bereits vorhandenen Bilder erneut ins WebP Format konvertiert.

Vorab sollte geprüft werden, ob die vorhandene ImageMagick oder GraphicsMagick Version auf dem Server Bilder zu WebP konvertieren kann. Das geht relativ leicht:

 

# Für GraphicsMagick
gm version | grep WebP

# Für ImageMagick
convert version | grep webp

 

Es sollte "Ja" oder eine Liste von Formaten ausgegeben werden, in denen WebP enthalten ist. Sofern diese Voraussetzung gegeben ist, lässt sich die Erweiterung mittels

 

composer require "plan2net/webp"

 

installieren.

Im Anschluss werden über das Install-Tool die "Processed Files" gelöscht. In der .htaccess wird dann im Rewrite Bereiche folgendes hinzugefügt:

 

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTP_ACCEPT} image/webp
    RewriteCond %{DOCUMENT_ROOT}/$1.$3.webp -f
    RewriteRule ^((fileadmin)/.+)\.(png|jpg|jpeg)$ $1.$3.webp [L]
</IfModule>

<IfModule mod_headers.c>
    Header append Vary Accept env=REDIRECT_accept
</IfModule>

AddType image/webp .webp

 

Nachdem alle Caches geleert wurden sollten im Frontend die Bilder bereits als WebP ausgeliefert werden. Prüfen lässt sich das relativ einfach mit der Entwickler Konsole des Browsers:

Schritt 2: JavaScript komprimieren

Diese Empfehlung dürfte vermutlich die einfachste sein, sofern das JavaScript sauber ist und durch die Komprimierung keine Fehler auftreten. Je nach Projekt gibt es mehrere Möglichkeiten, das JavaScript zu komprimieren. Ich zeige hier zwei Möglichkeiten. Zusätzlich zum Komprimieren von JavaScript sollte auch gleich in Erwägung gezogen werden, alle Dateien zu einer einzigen Datei zusammenzuführen. Das kann dann je nach Projekt schon schwierig werden. Grundsätzlich ist das Einbinden einer einzigen Datei am Dokumentende die beste Strategie, aber oft auch die schwierigste. Von daher ist es ratsam, das Projekt schon von Beginn an so aufzubauen, damit man mit einer einzigen JavaScript Datei am Ende des Dokuments klarkommt. 

Komprimieren mit TYPO3

Dieser Schritt entfällt, da ich das JavaScript des Projektes Inline aus einem FE-Build nutze.  Wer keinen FE Build nutzt, verfährt wie folgt. Der folgende Code kann in das setup.typoscript des Projekts hinterlegt werden:

 

config {
  concatenateJs = 1
  compressJs = 1
}

 

Kann man einzelne Dateien nicht komprimieren oder zusammenführen lassen, gibt es auch eine Anweisung zum Deaktivieren der Datei:

 

page {
  includeJSFooterlibs {
    jsfile1 = EXT:template/Resources/Public/JS/file1.js
    jsFile2.excludeFromConcatenation = 1
  }
}

 

Tipp: Wenn man am Projekt arbeitet, ist es natürlich sinnvoll, die Komprimierung und Zusammenführung zu vermeiden, wenn man im TYPO3 Backend eingeloggt ist. Zusätzlich wäre also folgender Code-Block erforderlich:

 

[backend.user.isLoggedIn]
    config {
      concatenateJs = 0
      compressJs = 0
    }
[end]

 

Zusätzlich dazu kann in der LocalConfiguration.php Compression Level eingestellt werden. Hierzu benötigt man den folgenden Eintrag:

 

$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] = 9

 

Anschließend wird in der .htaccess die gzip-Komprimierung aktiviert:

 

<FilesMatch "\.js\.gzip$">
	AddType "text/javascript" .gzip
</FilesMatch>
<FilesMatch "\.css\.gzip$">
	AddType "text/css" .gzip
</FilesMatch>
AddEncoding gzip .gzip

 

 

Komprimieren mit einem FE Build

Ich nutze für das Zusammenführen und komprimieren einen FE-Build. Das sei jedem empfohlen, da man hier genau steuern kann, welche JS Dateien eingebunden werden sollen, und auch an welcher Position das stattfinden soll. Zudem kann man Map-Dateien erstellen lassen, um in der Entwickler-Konsole des Browsers sehen zu können, welcher Code aus dem JS zum Beispiel einen Fehler verursacht. In meinem FE-Build schaut das dann in etwa so aus:

 

var paths = {
    scriptsFooter: [
        './src/JavaScript/bootstrap-native-custom.js',
        './node_modules/simplelightbox/dist/simple-lightbox.js',
        './node_modules/lazysizes/plugins/unveilhooks/ls.unveilhooks.js',
        './src/JavaScript/cookie-consent.js',
        './src/JavaScript/prism.js',
        './src/JavaScript/lazysizes.js',
        './src/JavaScript/scripts.js',
    ],
    styles: [
        './src/Scss/style.scss'
    ],
    css: [
        './node_modules/drift-zoom/dist/drift-basic.css',
        '../../web/typo3conf/ext/ext/powermail/Resources/Public/Css/Basic.css',
        '../../web/typo3conf/ext/mindshape_cookie_hint/Resources/Public/Css/dark-top.css',
        './src/fonts/icons/css/animation.css',
        './src/fonts/icons/css/fa.css',
        './src/CSS/cookie_consent.css'
    ],
    rteStyles: [
        './src/Scss/rte.scss',
        './src/Scss/rte-colors.scss'

    ],
    destJS: '../../packages/basetemplate/Resources/Public/JavaScript/',
    destCSS: '../../packages/basetemplate/Resources/Public/css/'
};

gulp.task('scripts', function () {
    return gulp.src(paths.scriptsFooter, { allowEmpty: true })
        .pipe(concat('footer.js'))
        .pipe(gulp.dest(paths.destJS))
        .pipe(minify())
        .pipe(gulp.dest(paths.destJS));
});

 

Verwendet werden die Module gulp-concat und gulp-minify. Wie oben zu sehen ist, werden an dieser Stelle alle JS Dateien gezogen, die beim Projekt benötigt werden. An dieser Stelle werden übrigens auch alle JS Dateien aus den Extensions angegeben, sofern vorhanden.

Wenn das JavaScript dann mit dem FE-Build gebaut wurde, wird es via TypoScript in die Seite geladen, und zwar im Fußbereich der Seite:

 

page {
    includeJSFooter >
    includeJSFooterlibs >
    includeJS >
    includeJSLibs >

    includeJSFooterlibs {
        footer = EXT:basetemplate/Resources/Public/JavaScript/footer-min.js
        analytics = EXT:basetemplate/Resources/Public/JavaScript/analytics.js
        analytics.defer = 1
    }
}

 

Wie zu sehen ist, setze ich alle anderen Möglichkeiten zum Einbinden von JavaScript zurück. Ab diesem Schritt muss dann auch klar sein, dass nur noch das hier angegebene JavaScript geladen wird. Mit dem Beispiel von weiter oben lässt sich auch hier eine Condition erstellen, damit bei eingeloggtem Backend Benutzer nicht die minimierte Version des JavaScript geladen wird. Im Prinzip entfällt bei mir auch dieser Schritt, da ich das JavaScript später Inline nutzen werden. 

Schritt 3: CSS komprimieren

Im Wesentlichen entfällt auch dieser Schritt bei mir, da ich das CSS später Inline einbinden werde. Ansonsten gilt: Beim CSS kann man sich eigentlich ganz gut am JavaScript von oben orientieren. Der Task für den FE-Build schaut entsprechend wie folgt aus:

 

gulp.task('styles', function () {
    var scssStream = gulp.src(paths.styles, { allowEmpty: true })
        .pipe(sass({
            outputStyle: 'nested'
        }).on('error', sass.logError));

    var cssStream = gulp.src(paths.css, { allowEmpty: true });

    return merge(cssStream, scssStream)
        .pipe(sourcemaps.init())
        .pipe(concat('style.css'))
        .pipe(sourcemaps.write('./'))
        .pipe(gulp.dest(paths.destCSS))
        .pipe(rename({ suffix: '.min' }))
        .pipe(cleanCSS({compatibility: 'ie8'}))
        .pipe(gulp.dest(paths.destCSS));
});

 

Ich nutze hierfür gulp-sourcemaps, gulp-sass, gulp-rename, gulp-clean-css sowie merge-stream. Letzters wird benötigt, wenn nicht nur SCSS, sondern auch CSS im Build verwendet wird. Die Einbindung in das TYPO3 läuft dann wieder wie folgt:

 

page {
    includeCSS >
    includeCSS {
        screen = EXT:basetemplate/Resources/Public/css/style.css
        screen.media = screen
    }
}

 

Somit ist sichergestellt, dass nur CSS Dateien eingebunden werden, die wir tatsächlich einbinden wollen.

Schritt 4: Fonts Optimieren und darauf achten, dass der Text während der Webfont-Ladevorgänge sichtbar bleibt

Hier kann es schon das erste Mal kritisch werden. Die Meldung "Darauf achten, dass der Text während der Webfont-Ladevorgänge sichtbar bleibt" an sich ist relativ einfach wegzubekommen. Allerdings wird dieser Posten später erneut aufschlagen, wenn die Fonts nicht grundlegend optimiert werden. Wie auf dem Bild zu sehen, werden einige Schriften verwendet. Das größte Problem bei diesem Projekt waren zum einen die Font (zusätzliche andere Fonts auf Unterseiten), sowie die FontAwesome Fonts. Hier wurden die zusätzlichen Fonts "Solid" und "Brands" genutzt. Diese Fonts sind letztlich dafür verantwortlich, dass auch das CSS relativ schnell groß wird. Und dieses Thema ist später sehr wichtig. Es wird nämlich ein möglichst kleines CSS benötigt.

  • fontawesome.css > 72Kb
  • brands.css > 1Kb
  • solid.css > 1Kb
  • fa-solid-900.woff2 > 78Kb
  • fa-brands-400.woff2 > 77Kb
  • fa-regular-400.woff2 > 13Kb
  • Requests: 4

Addiert man nun alle Daten zusammen, ergibt das in Summe 242Kb und einige Requests die der Browser ausführen muss.

Aber von Anfang an, primär geht es erst mal um die Empfehlung  "Darauf achten, dass der Text während der Webfont-Ladevorgänge sichtbar bleibt". Die Fonts liegen alle lokal auf dem Projektserver und werden via CSS/SCSS eingebunden. Das sieht dann in etwa wie im folgenden aus:

 

@font-face {
    font-family: 'fa';
    src: url('/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.eot?49596511');
    src: url('/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.eot?49596511#iefix') format('embedded-opentype'),
    url('/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.woff2?49596511') format('woff2'),
    url('/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.woff?49596511') format('woff'),
    url('/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.ttf?49596511') format('truetype'),
    url('/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.svg?49596511#fa') format('svg');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

 

Wichtig hier ist die Zeile font-display: swap;, diese muss hinzugefügt werden. Danach sollte die Empfehlung erledigt sein.

Wir rufen uns nun nochmal die 242Kb von oben in Erinnerung. Ziel ist es, die Menge an Daten zu drücken und weniger Requests zu erreichen. Im Falle von FontAwesome bin ich nach der Optimierung auf folgende Werte gekommen:

  • fa.css > 3Kb
  • fa.woff2 > 5Kb

Macht in der Summe 8 KB und am Ende nur ein Request. Schaffen lässt sich so etwas nur durch einen Kompromiss. Alles herauswerfen, was nicht benötigt wird. In diesem Projekt beschränkt sich die Verwendung von Icons auf 17 Stück. Mit dem Tool von Fontello lässt sich ein Custom Build der Icons zusammenstellen. Einfach alle Icons anklicken und die Daten ins Projekt übernehmen. 

Für den Bereich der normalen Schriften habe ich auch einen Kompromiss gemacht. Anstelle von vielen Schriften und vielen Schriftschnitten habe ich mich auf eine Schrift und zwei Schriftschnitten beschränkt. Der Weg zum Ziel ist also die Waage zwischen Design und technischer Funktionalität zu balancieren.

Schritt 5: Wichtige Anforderungen vorab laden

Das ist doch wieder etwas einfacher und sollte erledigt werden, wenn klar ist, welche Ressourcen wie Schriften oder externe Bibliotheken verwendet werden müssen. Bei externen Ressourcen sollte man sich aber auf jeden Fall immer folgendes Fragen: "Kann ich das auch lokal hosten?". Wann immer diese Frage mit "ja" beantwortet werden kann, sollte man Schriften und andere Sachen lokal hosten. 

Bei diesem Projekt geht es konkret um die letzten Font-Dateien, die nach den Kompromissen noch übrig sind. Diese Font-Dateien werden klassisch im CSS geladen. Nun sorgen wir dafür, dass der Browser die Dateien schon vorab lädt, und diese dann bereits zur Verfügung stehen, wenn das CSS geladen wird. Damit kann man auch prima "zuckende" Texte beheben. Außerdem wirkt sich dieser Punkt positiv auf "First Contentful Paint" aus den Labdaten aus. Der Preload der Dateien wird mit einem Meta-Tag im Head der Seite gemacht:

 

page {
    headerData {
        30 = TEXT
        30.value (
    <link rel="preload" href="/typo3conf/ext/basetemplate/Resources/Public/Fonts/open_sans/open-sans-v17-latin-300.woff2?49596511" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="/typo3conf/ext/basetemplate/Resources/Public/Fonts/open_sans/open-sans-v17-latin-600.woff2?49596511" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="/typo3conf/ext/basetemplate/Resources/Public/Fonts/fa.woff2?49596511" as="font" type="font/woff2" crossorigin>
        )
    }
}

 

 

Schritt 6: Erstreaktionszeit des Servers verringern

Hier gibt es mehrere Möglichkeiten wie man ans Ziel kommen kann. Zum einen könnte man natürlich versuchen, das ganze mit besserer und schnellerer Hardware zu kompensieren. Oder man setzt am Webserver und an der Datenbank an und optimiert hier weiter (was sicher auch kein Fehler ist). Aber wenn es richtig schnell werden soll, ist ein einfaches HTML File noch immer die beste Möglichkeit. Deshalb heißt die Lösung hier: StaticFileCache. Diese Extension erstellt beim ersten Zugriff auf eine Seite ein fertiges HTML File, das später dann ausgeliefert wird. Das funktioniert natürlich nur, wenn die Seite selbst keine dynamischen Inhalte verwendet. Es sollte also darauf geachtet werden, dass möglichst viele Seiten gecacht werden können. Eine Seite, auf der zum Beispiel ein Powermail-Formular angelegt ist, kann so nicht gecacht werden. Die Installation und Konfiguration der Extension ist denkbar einfach. Die Installation wird mit 

 

composer require lochmueller/staticfilecache

 

erledigt. Danach wird die Extension aktiviert, und die .htaccess Datei wird bearbeitet. Am besten Direkt nach 

 

RewriteEngine On

 

wird folgender Block eingefügt:

 

### Begin: StaticFileCache (preparation) ####

# Document root configuration
RewriteRule .* - [E=SFC_ROOT:%{DOCUMENT_ROOT}]
# RewriteRule .* - [E=SFC_ROOT:%{DOCUMENT_ROOT}/t3site] # Example if your installation is installed in a directory
# NOTE: There are cases (apache versions and configuration) where DOCUMENT_ROOT do not exists. Please set the SFC_ROOT to the right directory without DOCUMENT_ROOT then!

# Cleanup URI
RewriteCond %{REQUEST_URI} ^.*$
RewriteRule .* - [E=SFC_URI:/%{REQUEST_URI}]
RewriteCond %{REQUEST_URI} ^/.*$
RewriteRule .* - [E=SFC_URI:%{REQUEST_URI}]
RewriteCond %{REQUEST_URI} ^/?$
RewriteRule .* - [E=SFC_URI:/]

# Cleanup HOST
RewriteCond %{HTTP_HOST} ^([^:]+)(:[0-9]+)?$
RewriteRule .* - [E=SFC_HOST:%1]

# Disable cache for EXT:solr indexing requests
RewriteCond %{HTTP:X-Tx-Solr-Iq} .+
RewriteRule .* - [E=SFC_HOST:invalid-host]

# Get scheme
RewriteRule .* - [E=SFC_PROTOCOL:http]
RewriteCond %{SERVER_PORT} ^443$ [OR]
RewriteCond %{HTTP:X-Forwarded-Proto} https
RewriteRule .* - [E=SFC_PROTOCOL:https]

# Get port
RewriteRule .* - [E=SFC_PORT:80]
RewriteCond %{ENV:SFC_PROTOCOL} ^https$ [NC]
RewriteRule .* - [E=SFC_PORT:443]
RewriteCond %{SERVER_PORT} ^[0-9]+$
RewriteRule .* - [E=SFC_PORT:%{SERVER_PORT}]
RewriteCond %{HTTP:X-Forwarded-Port} ^[0-9]+$
RewriteRule .* - [E=SFC_PORT:%{HTTP:X-Forwarded-Port}]

# Full path for redirect
RewriteRule .* - [E=SFC_FULLPATH:typo3temp/tx_staticfilecache/%{ENV:SFC_PROTOCOL}_%{ENV:SFC_HOST}_%{ENV:SFC_PORT}%{ENV:SFC_URI}/index]

# Extension (Order: br, gzip, default)
RewriteRule .* - [E=SFC_EXT:]
RewriteCond %{HTTP:Accept-Encoding} br [NC]
RewriteRule .* - [E=SFC_EXT:.br]
RewriteCond %{ENV:SFC_ROOT}/%{ENV:SFC_FULLPATH}%{ENV:SFC_EXT} !-f
RewriteRule .* - [E=SFC_EXT:]
RewriteCond %{ENV:SFC_EXT} ^$
RewriteCond %{HTTP:Accept-Encoding} gzip [NC]
RewriteRule .* - [E=SFC_EXT:.gz]
RewriteCond %{ENV:SFC_EXT} ^\.gz$
RewriteCond %{ENV:SFC_ROOT}/%{ENV:SFC_FULLPATH}%{ENV:SFC_EXT} !-f
RewriteRule .* - [E=SFC_EXT:]

# Write Extension to SFC_FULLPATH
RewriteRule .* - [E=SFC_FULLPATH:%{ENV:SFC_FULLPATH}%{ENV:SFC_EXT}]

### Begin: StaticFileCache (main) ####

# We only redirect URI's without query strings
RewriteCond %{QUERY_STRING} ^$

# It only makes sense to do the other checks if a static file actually exists.
RewriteCond %{ENV:SFC_ROOT}/%{ENV:SFC_FULLPATH} -f

# NO frontend or backend user is logged in. Logged in users may see different
# information than anonymous users. But the anonymous version is cached. So
# don't show the anonymous version to logged in users.
RewriteCond %{HTTP_COOKIE} !staticfilecache [NC]

# We only redirect GET requests
RewriteCond %{REQUEST_METHOD} GET

# Rewrite the request to the static file.
RewriteRule .* %{ENV:SFC_ROOT}/%{ENV:SFC_FULLPATH} [L]

# Do not allow direct call the cache entries
RewriteCond %{ENV:SFC_URI} ^/typo3temp/tx_staticfilecache/.*
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule .* - [F,L]

# Handle application cache
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^.*\.sfc$ %{ENV:CWD}index.php?eID=sfc_manifest [QSA,L]

### Begin: StaticFileCache (options) ####

# Set proper content type and encoding for gzipped html.
<FilesMatch "\.gzip$">
   SetEnv no-gzip 1
   SetEnv no-brotli 1
   <IfModule mod_headers.c>
      Header set Content-Encoding gzip
   </IfModule>
</FilesMatch>
<FilesMatch "\.gz$">
   SetEnv no-gzip 1
   SetEnv no-brotli 1
   <IfModule mod_headers.c>
      Header set Content-Encoding gzip
   </IfModule>
</FilesMatch>
<FilesMatch "\.br$">
   SetEnv no-gzip 1
   SetEnv no-brotli 1
   <IfModule mod_headers.c>
      Header set Content-Encoding br
   </IfModule>
</FilesMatch>

# if there are same problems with ForceType, please try the AddType alternative
# Set proper content type gzipped html
<FilesMatch "\.gzip$">
   ForceType text/html
    AddType "text/html" .gzip
</FilesMatch>
<FilesMatch "\.js\.gzip$">
   ForceType text/javascript
    AddType "text/javascript" .gzip
</FilesMatch>
<FilesMatch "\.css\.gzip$">
   ForceType text/css
    AddType "text/css" .gzip
</FilesMatch>
<FilesMatch "\.xml\.gzip$">
   ForceType text/xml
    AddType "text/xml" .gzip
</FilesMatch>
<FilesMatch "\.rss\.gzip$">
   ForceType text/xml
   # AddType "text/xml" .gzip
</FilesMatch>
<FilesMatch "\.gz$">
   ForceType text/html
    AddType "text/html" .gz
</FilesMatch>
<FilesMatch "\.xml\.gz$">
   ForceType text/xml
    AddType "text/xml" .gz
</FilesMatch>
<FilesMatch "\.rss\.gz$">
   ForceType text/xml
   # AddType "text/xml" .gz
</FilesMatch>
<FilesMatch "\.br$">
   ForceType text/html
    AddType "text/html" .br
</FilesMatch>
<FilesMatch "\.xml\.br$">
   ForceType text/xml
   # AddType "text/xml" .br
</FilesMatch>
<FilesMatch "\.rss\.br$">
   ForceType text/xml
   # AddType "text/xml" .br
</FilesMatch>

# Avoid .br files being delivered with Content-Language: br headers
<IfModule mod_mime.c>
   RemoveLanguage .br
</IfModule>

### End: StaticFileCache ###

 

Anschließend muss noch der TYPO3 Cache geleert werden. Statische Seiten werden nur von gecacht, welche nicht schon von TYPO3 selbst gecacht wurden. Im groben und ganzen war es das auch schon. Anbei noch zwei Screens, einmal die Zeit ohne StaticFileCache und einmal mit. Wichtig: Der Browser-Cache muss jeweils deaktiviert sein, es soll ja die Seite getestet werden, und nicht wie schnell der Browser die Sachen aus dem eigenen Cache lädt :-)

Schritt 7: Statische Inhalte mit einer effizienten Cache-Richtlinie bereitstellen

Dieser Empfehlung kann man wieder recht gut folgen. Ich hatte bei diesem Projekt schon entsprechend Vorbereitungen getroffen und die Cache-Richtlinien auf 30 Tage eingestellt. Das war damals noch in Ordnung, heute soll es aber mehr sein. Also alles, was nicht zeitkritisch ist, soll mindestens für 6 Monate im Browsercache gespeichert werden. Wichtig hierbei ist jedoch, dass eingebundenes CSS und JavaScript mit einem Timestamp-Hash in der URL eingebunden wird. Das passiert automatisch, wenn man CSS und JS über includeCSS und includeJS via TypoScript einbindet. Bindet man solche Dateien selbst über headerData ein, muss man einfach selbst einen Hash an die URL hängen, und bei einer neuen Version der Datei dieses Hash ändern.

Wichtige Bilder die ausgetauscht werden müssen, sollten in dem jeweiligen Element dann einfach unter einem anderen Namen eingebunden werden. Wird das Bild nur über die Dateiliste getauscht, und der Name des Bildes bleibt gleich, wird der Besucher beim nächsten Besuch nicht die neue Version sehen können. Hier ein Beispiel wie die Cache-Richtlinien angepasst werden können. Das geschieht in der .htaccess des Projekts:

 

# This affects Frontend and Backend and increases performance.
<IfModule mod_expires.c>

	ExpiresActive on
	ExpiresDefault                                      "access plus 1 month"

	ExpiresByType text/css                              "access plus 1 year"

	ExpiresByType application/json                      "access plus 0 seconds"
	ExpiresByType application/ld+json                   "access plus 0 seconds"
	ExpiresByType application/schema+json               "access plus 0 seconds"
	ExpiresByType application/vnd.geo+json              "access plus 0 seconds"
	ExpiresByType application/xml                       "access plus 0 seconds"
	ExpiresByType text/xml                              "access plus 0 seconds"

	ExpiresByType image/vnd.microsoft.icon              "access plus 1 week"
	ExpiresByType image/x-icon                          "access plus 1 week"

	ExpiresByType text/x-component                      "access plus 1 month"

	ExpiresByType text/html                             "access plus 0 seconds"

	ExpiresByType application/javascript                "access plus 1 year"
	ExpiresByType application/x-javascript              "access plus 1 year"
	ExpiresByType text/javascript                       "access plus 1 year"

	ExpiresByType application/manifest+json             "access plus 1 week"
	ExpiresByType application/x-web-app-manifest+json   "access plus 0 seconds"
	ExpiresByType text/cache-manifest                   "access plus 0 seconds"

	ExpiresByType audio/ogg                             "access plus 6 months"
	ExpiresByType image/bmp                             "access plus 6 months"
	ExpiresByType image/gif                             "access plus 6 months"
	ExpiresByType image/jpeg                            "access plus 6 months"
	ExpiresByType image/png                             "access plus 6 months"
	ExpiresByType image/svg+xml                         "access plus 6 months"
	ExpiresByType image/webp                            "access plus 6 months"
	ExpiresByType video/mp4                             "access plus 6 months"
	ExpiresByType video/ogg                             "access plus 6 months"
	ExpiresByType video/webm                            "access plus 6 months"

	ExpiresByType application/atom+xml                  "access plus 1 hour"
	ExpiresByType application/rdf+xml                   "access plus 1 hour"
	ExpiresByType application/rss+xml                   "access plus 1 hour"

	ExpiresByType application/vnd.ms-fontobject         "access plus 6 months"
	ExpiresByType font/eot                              "access plus 6 months"
	ExpiresByType font/opentype                         "access plus 6 months"
	ExpiresByType application/x-font-ttf                "access plus 6 months"
	ExpiresByType application/font-woff                 "access plus 6 months"
	ExpiresByType application/x-font-woff               "access plus 6 months"
	ExpiresByType font/woff                             "access plus 6 months"
	ExpiresByType application/font-woff2                "access plus 6 months"

	ExpiresByType text/x-cross-domain-policy            "access plus 1 week"

</IfModule>

 

 

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"

Diese Empfehlung ist zusammen mit der nächsten Empfehlung, wo es um das JavaScript geht, eine der größten Herausforderungen. Um die Zusammenhänge zu erläutern, habe ich die beiden Empfehlungen "Ressourcen beseitigen, die das Rendering blockieren" sowie "Nicht verwendete CSS entfernen" zusammen genommen. Je mehr nicht verwendetes CSS im aktuellen Sichtbereich vorhanden ist, umso mehr wirkt sich das auf die Empfehlung "Ressourcen beseitigen" aus. Konkret bedeutet dass: Je mehr CSS wir aus dem Projekt entfernen, umso besser werden also die Gesamtwerte. Wie viel CSS man entfernen kann, hängt sicher auch vom Projekt und der Kompromissbereitschaft ab, aber auch, welche Komponenten verwendet und eingebunden werden. Bereits weiter oben habe ich das vorgehen schon für FontAwesome gezeigt, mit der Möglichkeit, sich das Icon-Font selbst zu bauen. Ich habe das CSS für dieses Projekt um 50Kb reduziert. Binde ich das CSS weiter mittels includeCSS ein, sind beide Empfehlungen noch im Test sichtbar, allerdings mit sehr geringen Werten. Ich erreiche hier in der Regel 99 Punkte. Das ist schon Top für ein Projekt. Später zeige ich aber noch die Inline Variante, um die 100 garantiert zu erhalten.

Best Practice zum Verkleinern von CSS

Eine konkrete Anleitung gibt es leider nicht, aber für dieses Projekt habe ich zuerst den Ist-Zustand ermittelt. Dieser war in etwa so:

  • Bootstrap komplett
  • FontAwesome diverse Fonts
  • 3 weitere Fonts mit diversen Schriftschnitten
  • AOS Animation Library
  • Prismjs Code Highlighter
  • jQuery Fancybox
  • Bootstrap Select

Danach habe ich mir überlegt, was für das Projekt tatsächlich benötigt wird. Das war in diesem Fall dann:

  • Bootstrap mit Custom Build: Grid, Nav, Dropdown, Badge, Buttons und Carousel. Zusätzlich einbinden muss man noch Functions, Variables, MixIns, Root, Reboot und Utilities
  • FontAwesome mit nur wenigen Icons
  • Prismjs, ebenfalls sehr abgespeckt mit nur wenigen Sprachen
  • Neu dazu gekommen ist Simple Lightbox

Übrig bleibt nur noch ein schmales CSS, welches man später auch Inline einbinden kann. Beim letzten Artikel habe ich noch recht viel mit "CSS Above the Fold" gearbeitet, finde das aber eher unpraktisch mit der Handhabung, vor allem, wenn das Projekt sich weiter entwickelt. Dabei ging es darum, das CSS, welches sich im sichtbaren Bereich befindet, Inline einzubinden, und den Rest später per Datei nachzuladen. Bei dieser Vorgehensweise macht man sich nicht wirklich bewusst, was wirklich benötigt wird für ein Projekt, und was überflüssig ist, oder einfacher umgesetzt werden kann.

Schritt 10: Nicht genutztes JavaScript entfernen

Jetzt wird es wild. Wir verlassen den Pfad des einfachen und machen uns auf, in eine Welt voller Kompromisse und alternativen Lösungen. Im Prinzip verhält es sich beim JavaScript ähnlich wie beim CSS. Einfach alles herauswerfen was nicht benötigt wird. Aber am Ende ist das doch nicht so einfach, wie man denkt. Was wird benötigt, was wird wirklich benötigt, und was wird tatsächlich benötigt? Auch hier wieder eine Liste, was bisher bei diesem Projekt eingebunden ist, und was künftig davon verwendet werden soll.

  • Modenizr
  • Detectizr
  • jQuery
  • jQuery.bridget
  • popper
  • jQuery.easing
  • picturefill
  • bootstrap
  • bootstrap-select
  • Driftjs
  • Swiperjs
  • AOS Animation Library
  • jQuery Fancybox
  • Isotope
  • jQuery DateTimePicker (Powermail)
  • Parsley (Powermail)
  • Tabs  (Powermail)
  • Form  (Powermail)
  • PrismJS
  • Cookiebot
  • Analytics

Ja... so hab ich auch geschaut. Da sammelt sich mit der Zeit wirklich was an. Ein halbes MB nur JavaScript. Na denn, einmal ausmisten und die Liste was bei diesem Projekt wirklich gebraucht wird:

  • PrismJS
  • Analytics

Huch? Nur die beiden Sachen? Natürlich nicht, aber das ist tatsächlich das einzige, was aus der alten Liste übrig bleibt. Ich sagte bereits am Anfang, man muss Kompromisse machen. Aber wie schaut das im Detail aus?

Modernizr, Detectizr, jQuery und alle Plugins, Drift (kein Plan was das sein soll), AOS (das war wirklich ein Kompromiss) Isotope und das ganze andere braucht man nicht wirklich. Boostrap? Benötigt jQuery. Also eine fette Library nur wegen Bootstrap? Nein, braucht man nicht. Man kann auch Bootstrap rauswerfen, und Bootstrap.Native einbinden. Klein, leicht und ohne jQuery. Für den Rest muss man dann zurück zu den Basics, was eigenes JavaScript angeht. Für die Fancybox kam die Simple Lightbox als Ersatz. Hier nochmal in der Übersicht, welche Module nach der Auf- und Umbauaktion enthalten sind:

Das schaut wirklich sehr übersichtlich aus. Und klein. keine 40Kb groß. Wer es bis hierhin geschafft hat, dürfte sich nun im Test bei deutlich über 90 Punkte sehen. Vermutlich wird an der 100 gekratzt. Im weiteren Verlauf zeige ich einige Best Practices, um von der oberen Liste auf die untere zu kommen.

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.

Für jede Root-Seite des Projekts ist im Listenmodul ein Eintrag zu finden. Hier werden die diversen Cookies angelegt und kategorisiert. Alle Texte sind ebenfalls darin zu erstellen.

Hier wird der Cookie-Eintrag für Analytics angelegt. Leider gibt es keine Vorlagen die der Extension Beiligen, aber nach kurzer Zeit ist relativ klar wie das ganze funktioniert. Neben dem Anlegen von Cookies im Backend muss dann auch im Frontend die Verknüpfung zum eigentlichen JavaScript Code hergestellt werden. Im Fall von Analytics schaut das wie folgt aus:

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!