Von veröffentlicht am

Cache für den Blog aktivieren

Caching mit Statamic

Basics: Teil 8, 15 min.

dalle-2022-11-01-11.41.36.png

Statamic gilt als sehr schnelles CMS. Es schneidet beim Lighthouse Score in der Kategorie Performance meist sehr gut ab und dies trotz bescheidenen Server-Ressourcen. Dies liegt an einer effektiven Caching-Strategie. Geschwindigkeit ist prinzipiell mit jedem CMS möglich, Statamic wurde jedoch von anfang an darauf optimiert.

Caching ermöglicht eine schnelle Auslieferung der Webseite und schont den Server. Beide Eigenschaften sind wünschenswert und ermöglichen es Seiten mit hohem Traffic Aufkommen günstig zu betrieben.

Stache und Cache

Der Stache sind Indexes, die Statamic einen effizienten Zugriff auf die Inhalte ermöglicht. Einen tieferen Einblick in den Stache findest du in folgendem Blogpost. Es ist der erste Cache von Statamic, dazu gedacht, die Datenbank zu ersetzen. Der Stache wird von Statamic im Laravel Application Cache abgelegt und schafft es Dateien schnell durchsuchbar zu machen. Statamic verwendet Caching also bereits für seine interne Datenablage.

Caching

Wenn wir von Caching reden, dann meinen wir meist, dass gewisse Operationen einmal durchgeführt werden und anschliessend das Resultat zwischengespeichert wird. Bei einer neuerlichen Anfrage wird die Berechnung übersprungen und auf das bereits bekannte Resultat zugegriffen. Dadurch kann man sich Rechenzeit und Rechenleistung sparen. Dies erhöht die Geschwindigkeit, in der eine Seite auf dem Server zusammengestellt wird.

Statamic bietet mehrere Möglichkeiten fürs Caching an. Zum einen lässt sich mit Static Caching das fertige Resultat einer Route speichern z.B. /about-us, zum anderen lassen sich mit Antlers Cache-Tag auch nur Teile des Layouts cachen, z.B. der Header.

Grundsätzlich empfiehlt sich Caching nur für Aspekte der Seite, die keine dynamischen Elemente haben. Also keine Formulare, keine zufällige Sortierung, keine zeit-basierten Filter oder ähnliches. All dies ist mit Caching nicht möglich, da das Resultat der zugrundeliegender Funktionen nicht deterministisch ist.

Cache Antlers Tag

Das {{ cache }} Antlers-Tag ermöglicht es gewisse Aspekte des Layouts zu cachen. In unserem Blog möchten wir gerne die Seiten der einzelnen Artikeln zwischenspeichern. Diese haben keine dynamischen Aspekte und bieten sich für Caching an. Wir wrappen unsere view /resources/views/posts/show.antlers.html in ein {{ cache }} Tag.

{{ cache for="1 week" key="post_entries"}}
    <article>
        <h1 class="font-black text-4xl mb-6">{{title}}</h1>
        <div class="mb-2 text-sm">
            Von <address class="inline" rel="author">{{ author:name }}</address>
            veröffentlicht am
            <time pubdate datetime="{{updated_at format="Y-m-d"}}">{{updated_at format="d.M Y" }}</time>
        </div>
        <img src="{{feature_image}}" alt="{{feature_image:alt}}" class="responsive min-w-full">
        <p class="italic my-4 text-xl">{{lead}}</p>
        <div>
            {{ partial:default}}
        </div>
    </article>
{{ /cache }}

Danach wird der Aspekt der Webseite im Cache gespeichert. Rufen wir einen einzelnen Post mehrmals im Browser auf, dann sehen wir dass die Responsetime nach dem ersten Aufruf kürzer wird von 650ms auf 450ms. Wir erhöhen der Speed der Seite um knapp 30%. Der Cache hat jedoch seinen Preis. Aktualisieren wir unseren Artikel, dann sind diese Änderungen erst eine Woche Später auf der Webseite sichtbar. Der Cache wird erst nach "1 Week" verworfen und neu erstellt.

Wir können den Cache jederzeit mit php artisan cache:clear manuell löschen. Dies macht aber die Handhabung der Webseite mühsam. Wir wollen den Cache bei jeder Aktualisierung der Seite verwerfen. Dazu erstellen wir einen Eventlistener und löschen den Cache nach jedem speichern. Zugegeben, dies ist für einen Bog Over the Top, soll aber das Prinzip illustrieren. Wir generieren zuerst einen Eventlistener für den Event EntrySaved.

php please make:listener ClearCache --event="\Statamic\Events\EntrySaved"

Nun löschen wir den Cache für den Key post_entries nach jedem Save eines Artikels. Dadurch werden die zwischengespeicherten Html-Snippets für die Artikel aus dem Cache gelöscht und erst beim nächsten Aufruf wieder erstellt.

<?php

namespace App\Listeners;

use Illuminate\Support\Facades\Cache;
use Statamic\Events\EntrySaved;

class ClearCache
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  \Statamic\Events\EntrySaved  $event
     * @return void
     */
    public function handle(EntrySaved $event)
    {
        if ($event->entry->collectionHandle() === 'posts') {
            Cache::forget('post_entries');
        }
    }
}

Damit dies funktioniert, müssen wir die Klasse noch registrieren. Dazu editieren wir die Datei /app/Providers/EventServiceProvider.php und fügen unsere Klasse hinzu.

<?php

namespace App\Providers;

use App\Listeners\ClearCache;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
use Statamic\Events\EntrySaved;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array<class-string, array<int, class-string>>
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        EntrySaved::class => [
            ClearCache::class
        ]
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

Voila - wenn wir nun einen Artikel speichern, wird auch der Cache gelöscht.

Static Caching: be a rocket

Wir haben gesehen, dass wir mit dem cache Tag einzelne Aspekte des Layouts zwischenspeichern können. Dies wird meist zur Feinjustierung einer Seite eingesetzt. Meist ist es einfacher zuerst das statische Caching einzuschalten. Dabei wird das gesamte Html eines Seitenaufrufs zwischengespeichert. Also nicht nur Aspekte eines Layouts, sondern alles.

Um Statisches Caching zu aktivieren muss die STATAMIC_STATIC_CACHING_STRATEGY .env Variable auf half gesetzt werden. Detailliert lässt sich Statisches Caching in der Datei config/statamic/static_caching.php konfigurieren.

Die half Strategie speichert die Seite im Laravel Application Cache. Wird die Seite ein zweites Mal aufgerufen, dann liefert Statamic das Resultat aus dem Cache zurück. Aktivieren wir diese Strategie, dann sinkt die Responsetime unserer Seite markant, auf einer dev Maschine von ~300ms auf ~60ms - ein Boost von Faktor fünf!

Ähnlich wie beim cache Antlers-Tag verlieren wir dadurch jegliche Dynamik der Seite. Ist die Seite mal berechnet, dann bleibt sie im Cache, bis dieser invalidiert wird. Ein Formular mit crfs Tag wird nicht mehr funktionieren, genau so wenig wie die Startseite mit der Liste von Beiträgen aktualisiert wird. Es ist wichtig sich dessen bewusst zu sein. Sonst wundert man sich warum Statamic einfach nicht funktioniert.

Wann eine Seite aus dem Cache invalidiert/gelöscht wird, kann granular eingestellt werden. Zum einen gibt es die Möglichkeit den Cache nach einer festgelegten Zeit zu löschen. Dazu setzen wir eine expiry Zeit in Minuten. Die folgende Konfiguration config/statamic/static_caching.php löscht den Cache jede Stunde.

return [
    'strategies' => [
        'half' => [
            'driver' => 'application',
            'expiry' => 60,
        ],
	]
];

Zum anderen kann der Cache nach einem Event gelöscht werden. Standardmässig wird der Cache eines Artikels nach dessen Anpassung gelöscht - wenn ich also ein Post editieren, dann wird dieser aus dem Cache gelöscht. In unserem Blog reicht dies aber nicht. Wir verwenden die collection:posts auch auf der Startseite und müssen auch diese nach dem Speichern eines Posts aus dem Cache entfernen. Die folgende Konfiguration löscht auch die Startseite, nachdem ein Post angepasst wurde.

return [
    'invalidation' => [
        'class' => null,
        'rules' => [
            'collections' => [
                'posts' => [
                    'urls' => [
                        '/'
                    ]
                ],
            ],
        ],
    ],
];

Vorsicht: stimmen die Konfigurierte APP_URL nicht mit der verwendeten überein, dann funktionieren die rules nicht.

nocache Antlers-Tag

Den Cache automatisiert zu löschen hilft Änderungen an der Seite zu deployen. Wenn wir was ändern, wird dies auch direkt angewendet. Es bringt uns aber nicht die Dynamik zurück, die wir z.b. für Formulare benötigen. Hier hilft uns das {{ nocache }} Tag. Dieses Antlers-Tag schliesst einzelne Aspekte vom Caching aus. Wir können also unser Formular in ein nocache Tag wrappen und es funktioniert wieder.

Dieses Tag ist sehr hilfreich, um von Statischem Caching zu profitieren und trotzdem dynamische Aspekte zu ermöglichen.

Url vom Cache auschliessen

Alternativ zum nocache Antlers-Tag können gewisse URL's vom Caching ausgenommen werden. So können beispielsweise Seiten mit einem hohen Anteil an dynamischen Aspekten nicht gecached werden, während der der Rest der Seite statisch zurückgegeben wird. Die folgende Konfiguration schliesst die Home Seite vom Caching aus.

return [
    'exclude' => [
        "/"
    ],
];

Full messure

Bis jetzt haben wir die Strategie half angewendet. Dies macht die Seite schon sehr schnell. Es geht aber noch schneller. Bei der half Strategie muss Laravel geladen werden und die Informationen aus dem Laravel Application Cache geholt werden. Dies bedeutet, dass php ausgeführt wird, was unnötig Zeit kostet. Wir können die Seite von Statamic auch als einfache Html Seiten speichern und den Server anweisen diese auszuliefern - die full Strategie. Dadurch wird das ausliefern der Webseite auf das Lesen einer Datei reduziert. Die Responsetime hängt jetzt nur noch vom Filesystem und der Latenz ab.

Natürlich hat auch diese Option ihre Kosten. Zusätzlich zu den Einschränkungen der half Strategie, kommt folgendes hinzu: der Cache kann nicht mehr nach einer bestimmten Zeit invalidiert werden und das nocache Tag ist nur noch eingeschränkt nutzbar.

Um die Full Messure zu aktivieren, muss die STATAMIC_STATIC_CACHING_STRATEGY .env Variabe auf full gestellt werden. Zusätzlich müssen rewrite rules in der /public/.htaccess Datei angelegt werden.

RewriteCond %{DOCUMENTROOT}/static/%{REQUESTURI}%{QUERYSTRING}\.html -s
RewriteCond %{REQUEST_METHOD} GET
RewriteRule .* static/%{REQUESTURI}%{QUERY_STRING}\.html [L,T=text/html]

Durch die Rewrite Rules werden Request falls möglich direkt auf die Datei /public/static/{file}.html weitergeleitet. Dies reduziert die Responsetime auf einer dev Maschine auf wenige Millisekunden.

Best practice

Wann du welche Strategie anwendest, hängt wie immer vom Anwendungsfall ab. Grundsätzlich ist zu empfehlen die Seite ohne Caching zu entwickeln und dann das Caching graduell aktivieren. Am einfachsten ist es zuerst die half Strategie zu aktivieren und dann mit dem nocache Tag Aspekte vom Caching auszunehmen. Dies ist relativ einfach und schafft einen beschleunigt die Seite auf Schallgeschwindigkeit. Werden dadurch die gesteckten Geschwindkeitsziele nicht erreicht oder ist der Druck auf den Server immer noch zu gross, dann kann die full Strategie aktiviert werden. nun fliegt die Seite mit Lichtgeschwindigkeit. Es ist wahrscheinlich, dass die Konfiguration nun etwas komplexer wird. Gewisse Urls müssen vom Caching ausgeschlossen werden, andere Funktionen können mit JS implementiert werden.

Mit dem SSG Plugin lässt sich die gesamte Seite auch as statisches HTML dumoen und auf sagen wir netlfy deployen. nun sind wir auf Warp-Geschwindigkeit, aber die Betreung wird auch deutlich aufwendiger.