Cachen von Content-Element-Vorschau im TYPO3-Backend

Eine wunderbare Funktion im Backend von TYPO3, dass Redakteure eine kleine Vorschau der Inhaltselemente bekommen. Mit wenig Code kann man sogar für eigene CEs wie Akkordeons, Teaser odgl. eine Vorschau einbauen. Bis das böse Erwachen kommt…

Böses Erwachen: Uncached Content

In einem Projekt beklagten sich die Redakteuere, dass das TYPO3-Backend so langsam sei. Eine Analyse anhand einer Beispielseite (ca 75 Inhaltselemente) zeigte schnell die Ursache: zum Anzeigen des „Web > Page“-Moduls wurden rund 2800 SQL-Queries abgefeuert. Uff… Aber wozu? Die weitere Analyse zeigte, dass für die Vorschau der 60 Textmedia-Elemente bereits 1900 SQL-Queries auflaufen.

Rechnet man kurz durch, kommt man schnella uf die Menge:

  • 1900 SQL-Queries für 60 Elemente => 32 SQL-Queries pro Inhaltselement
  • Textmedia-Elemente sind mit Dateien/Bildern verknüpft, d.h. es kommen Abfragen der FileReference, des Files und der Meta-Daten hinzu.
    So werden es mit 10 Bildern schnell die 32 SQL-Queries…

Was im Kleinen harmlos ist, kann bei größeren Zahlen, also vielen Inhaltselementen oder vielen verknüpften Datensätzen (Bilder, Slides,…), somit schnell böse enden.

Und im Backend wird derlei Inhalt standardmäßig nicht gecacht, d.h. jeder Aufruf des Seiten-Modul für diese Seite erzeugt (nahezu) dieselbe hohe Menge an Abfragen.

Caching für Content-Element-Vorschau

Recht schnell kam die Idee, dass der Großteil einer solch inhaltslastigen Website sich vermutlich eher selten ändert, hingegen oft auf die Seite im Backend geschaut wird, um einen kleinen Teil zu verändern. Der Rest wäre also sehr gut aus einem Cache zu laden.

Kern des Ganzen: AbstractCachedPreview-Klasse

Das Handling des ganzen Caches wurde in eine abstrakte Klasse ausgelagert. Die einzelnen Preview-Klassen können diese erweitern und sich auf ihre Vorschau konzentrieren.

<?php

declare(strict_types=1);

namespace Me\MyExtension\Hooks\Preview;

use TYPO3\CMS\Backend\View\PageLayoutView;
use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

/**
 * Cached previewRenderer
 *
 * If using many CEs with many relation (e.g. images), the default-previewRendering causes a huge amount of SQL-queries.
 * This class allows previewRenderers using CF to avoid unnecessary queries for fetching unchanged content.
 */
abstract class AbstractCachedPreview implements PageLayoutViewDrawItemHookInterface
{
    /**
     * @var string
     */
    protected string $cType = '';

    /**
     * @var FrontendInterface
     */
    private FrontendInterface $cache;

    /**
     * @var array
     */
    private array $cacheTags = [];

    /**
     * @var string
     */
    private string $cacheIdentifier = '';

    /**
     * @var int
     */
    private int $cacheLifetime = 604800;

    /**
     * Rendering of CTypes preview
     *
     * The preview should *NOT* have edit-link or other BE links. These links would have tokens in the URLs and would get invalid due to caching.
     * Instead, the whole preview-HTML will be linked.
     *
     * @param array $row Record row of tt_content
     * @param PageLayoutView $parentObject
     * @return string Rendered (uncached) preview
     */
    abstract protected function renderPreview(array $row, PageLayoutView $parentObject): string;

    public function __construct()
    {
        $cacheManager = new CacheManager();
        $cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);

        $this->cache = $cacheManager->getCache('myextension_preview');
    }

    /**
     * Preprocesses the preview rendering of the content element.
     *
     * @param PageLayoutView $parentObject Calling parent object
     * @param bool $drawItem Whether to draw the item using the default functionalities
     * @param string $headerContent Header content
     * @param string $itemContent Item content
     * @param array $row Record row of tt_content
     */
    public function preProcess(
        PageLayoutView &$parentObject,
        &$drawItem,
        &$headerContent,
        &$itemContent,
        array &$row
    ): void {
        if ($row['CType'] === $this->cType) {
            $itemContent = $this->getRenderedPreview($row, $parentObject);
            $drawItem = false;
            $headerContent = '';
        }

        $headerContent = '';
    }

    /**
     * Getter for the rendered preview
     *
     * Tries to get the rendered preview from the cache, otherwise, it will be rendered and cached.
     *
     * @param array $row Record row of tt_content
     * @param PageLayoutView $parentObject Calling parent object
     * @return string Rendered preview
     */
    protected function getRenderedPreview(array $row, PageLayoutView $parentObject): string
    {
        $cacheIdentifier = $this->cacheIdentifier ?: implode('-', ['tt_content', $row['uid'], $row['sys_language_uid'], $row['assets']]);

        if (($value = $this->cache->get($cacheIdentifier)) === false) {
            $value = $this->renderPreview($row, $parentObject);
            $this->addCacheTags(['tt_content_' . $row['uid']]);
            $this->cache->set($cacheIdentifier, $value, $this->cacheTags, $this->cacheLifetime);
        }

        return $parentObject->linkEditContent($value, $row);
    }

    /**
     * Adds tags to this preview's cache entry
     *
     * @param array $tags An array of tags
     */
    public function addCacheTags(array $tags): void
    {
        $this->cacheTags = array_merge($this->cacheTags, $tags);
    }
}

Konkrete Vorschau

Die Vorschau selbst hängt natürlich vom CType ab.
Zu beachten ist hier: keine BE-Links setzen! Diese Links hätten nur eine begrenzte Gültigkeitsdauer – die durch Cachen schnell überschritten wäre. Stattdessen wird die gesamte gecachte Vorschau automatisch mit einem „Edit“-Link versehen (s.o.).

Hier die beispielhafte Implementierung für ‚Textmedia‘:

<?php

declare(strict_types=1);

namespace Me\MyExtension\Hooks\Preview;

use TYPO3\CMS\Backend\View\PageLayoutView;

/**
 * Cached textmedia-previewRenderer
 *
 * If using many CEs with many images, the default-previewRendering causes a huge amount of SQL-queries.
 * This previewRenderer uses CF to avoid unnecessary queries for fetching unchanged content.
 */
class TextmediaPreview extends AbstractCachedPreview
{
    /**
     * @var string
     */
    protected string $cType = 'textmedia';

    /**
     * @param array $row
     * @param PageLayoutView $parentObject
     * @return string Rendered (uncached) preview
     */
    protected function renderPreview(array $row, PageLayoutView $parentObject): string
    {
        $contentType = $parentObject->CType_labels[$row['CType']];
        $previewHtml = '<strong>' . htmlspecialchars($contentType) . '</strong>' . '<br />';

        if ($row['bodytext']) {
            $previewHtml .= $parentObject->renderText($row['bodytext']) . '<br />';
        }

        if ($row['assets']) {
            $previewHtml .= $parentObject->getThumbCodeUnlinked($row, 'tt_content', 'assets') . '<br />';
        }

        return $previewHtml;
    }
}

Cache konfigurieren & Hook registrieren

Damit TYPO3 auch alles findet, brauchts jetzt noch ein paar Zeilen in der ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['textmedia'] = Me\MyExtension\Hooks\Preview\TextmediaPreview::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['myextension_preview'] ??= [];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['myextension_preview']['groups'] ??= ['pages'];

Fazit

Im konkreten Projekt konnten wir damit die Performance im Backend spürbar steigern.


Verwendete TYPO3-Version: v11

2 Comments

  1. Hallo,

    danke für den Artikel, werde ich bei uns auch mal einbauen :)
    Habt ihr vielleicht mal überlegt, dass in den TYPO3 Core zu integrieren, wäre sicher ne schöne Sache?

    Reply
    • Ja, Überlegungen gibt’s. Die Idee des Cachens ist jedenfalls bei einigen via twitter/Mastodon aufgefallen. Auch gab’s bereits ein paar Ideen, wie man es sauberer/effizienter machen könnte etc.pp.
      Am Ende ist’s wie so oft: es braucht jemanden, der’s macht – oder zumindest koordiniert 🤷

      Reply

Hinterlasse einen Kommentar.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.