Minifikacja kodu HTML w aplikacji Symfony

Kategoria: Webmastering Data publikacji:

Framework Symfony dostarcza mechanizm do zarządzania stylami CSS i skryptami JavaScript, w tym do ich minifikacji — Webpack Encore. Nie ma jednak żadnego rekomendowanego przez Symfony sposobu na minifikację kodu HTML odpowiedzi aplikacji. W ramach pracy nad kilkoma projektami przygotowałem dwa sposoby na rozwiązanie tego problemu.

Minifikacja kodu HTML wygenerowanej odpowiedzi

Najprostszym sposobem jest instalacja dowolnej biblioteki do minifikacji kodu HTML, a następnie napisanie niewielkiego event subscribera, który podmieni zawartość odpowiedzi. Do tego celu można wykorzystać na przykład bibliotekę HtmlCompress:

composer require wyrihaximus/html-compress

Klasa WyriHaximus\HtmlCompress\Factory umożliwia wskazanie oczekiwanego stosunku intensywności minifikacji do jej szybkości: w najszybszym wariancie narzędzie pomija minifikację osadzonych w HTML-u skryptów JavaScript i stylów CSS.

Oto najbardziej prymitywna implementacja event subscribera:

<?php

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use WyriHaximus\HtmlCompress\Factory;

class ResponseMinifySubscriber implements EventSubscriberInterface
{
    public function onResponseEvent(ResponseEvent $event): void 
    {
        $compressor = Factory::constructFastest();
        $response = $event->getResponse();

        $response->setContent(
            $compressor->compress($response->getContent())
        );
    }

    public static function getSubscribedEvents(): array
    {
        return [
            ResponseEvent::class => 'onResponseEvent',
        ];
    }
}

Z prymitywnością tego rozwiązania wiąże się jego wada: minifikacja jest aplikowana przy każdym załadowaniu strony, zmniejszając jej rozmiar, ale równocześnie spowalniając dostarczenie odpowiedzi HTTP do użytkownika. Warto poeksperymentować w narzędziach deweloperskich przeglądarki internetowej, aby zbadać stopień spowolnienia generowania odpowiedzi HTTP, szczególnie w przypadku dłuższych podstron.

Pewnym kompromisem może być minifikacja jedynie stron przechowywanych w pamięci podręcznej oraz implementacja cache’owania HTTP za pośrednictwem reverse proxy frameworka Symfony lub zewnętrznego mechanizmu takiego jak Varnish. Aby zniwelować narzut wynikający z procesu minifikacji, wystarczy umieścić dodatkowe sprawdzenie w event subscriberze:

<?php 

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use WyriHaximus\HtmlCompress\Factory;

class ResponseMinifySubscriber implements EventSubscriberInterface
{
    public function onResponseEvent(ResponseEvent $event): void 
    {
        $response = $event->getResponse();

        if (false === $response->isCacheable()) {
            return;
        }

        $compressor = Factory::constructFastest();

        $response->setContent(
            $compressor->compress($response->getContent())
        );
    }

    public static function getSubscribedEvents(): array
    {
        return [
            ResponseEvent::class => 'onResponseEvent',
        ];
    }
}

„Minifikacja” kodu szablonów Twig

Podczas czyszczenia i wypełniania pamięci podręcznej aplikacji Symfony na podstawie szablonów Twig generowane są klasy PHP odpowiedzialne za ostateczny kod HTML strony. Wszystkie „białe znaki” (wcięcia, spacje, tabulatory) umieszczone w oryginalnych szablonach Twig są przenoszone do wywołań polecenia echo w wygenerowanych plikach PHP.

Proces generowania klas PHP z szablonów Twiga jest realizowany przez compiler. Twig posiada swój wbudowany kompilator, ale istnieje możliwość jego podmiany na własny. Za pomocą takiego niestandardowego kompilatora można usunąć powtarzające się lub zbędne białe znaki. Proces ten nie wpływa negatywnie na czas ładowania strony, ponieważ ma miejsce podczas generowania cache’u szablonów (podczas wywołania bin/console cache:clear).

Oto implementacja kompilatora pochodząca z mojego projektu RadioLista-v3:

<?php

namespace App\Twig;

use Twig\Compiler as BaseCompiler;
use Twig\Node\Node;
use Twig\Node\TextNode;

class Compiler extends BaseCompiler
{
    public function compile(Node $node, $indentation = 0): self
    {
        $this->compressTextNodes($node);

        return parent::compile(...func_get_args());
    }

    public function subcompile(Node $node, $raw = true): self
    {
        $this->compressTextNodes($node);

        return parent::subcompile(...func_get_args());
    }

    private function compressTextNodes(Node $node): void
    {
        foreach ($node as $childNode) {
            $this->compressTextNodes($childNode);
        }

        if ($node instanceof TextNode) {
            $data = $node->getAttribute('data');

            if ('@' === $node->getTemplateName()[0]) {
                $data = preg_replace(['/^\n+/m', '/ +/'], ['', ' '], $data);
            }
            else {
                $data = preg_replace('/\s+/', ' ', str_replace("\n", '', $data));
            }

            $node->setAttribute('data', $data);
        }
    }
}

Zrealizowana powyżej „minifikacja” polega na usunięciu wszystkich przełamań wierszy oraz powtarzających się białych znaków w kodzie HTML. Dla szablonów pochodzących z zewnętrznych bundle’i, których nazwa zaczyna się znakiem procenta, stosowana jest jeszcze bardziej prymitywna zamiana — usuwane są jedynie puste linie i powtarzające się spacje.

Przedstawiona wyżej implementacja kompletnie ignoruje przypadki specjalne takie jak znacznik <pre> czy osadzone skrypty JavaScript i style CSS. Warto pamiętać też o tym, że kompilator szablonów Twiga nie ma dostępu w jednym momencie do całego kodu HTML strony, a jedynie do pojedynczych małych fragmentów (warto „zvardumpować” wartość zmiennej $data, aby to lepiej zrozumieć), dlatego nie ma możliwości wpięcia tutaj pełnoprawnego narzędzia do minifikacji.

Aby zastąpić domyślny compiler Twiga własnym, należy wywołać metodę setCompiler() na instancji klasy Twig\Environment, na przykład we własnym AppExtension:

<?php

namespace App\Twig;

use Twig\Environment;
use Twig\Extension\AbstractExtension;
// ...

class AppExtension extends AbstractExtension
{
    public function __construct(Environment $twig, Compiler $compiler)
    {
        $twig->setCompiler($compiler);
    }

    // ...
}

„Minifikacja” kodu strony oparta na własnym kompilatorze szablonów Twiga jest bardzo hackerska i niestabilna: może doprowadzić do niepoprawnego działania strony i należy jej używać ostrożnie i świadomie, z pełnym zrozumieniem. Zdecydowanie dobrym pomysłem jest pozostawienie jej włączonej w środowisku deweloperskim, aby szybko wykryć problemy.

Dodaj komentarz