Jak w kilku krokach przyśpieszyć stronę oparta na Symfony?| Cykl 100 punktów w PageSpeed

Tym artykułem chcę zacząć serię wpisów na temat optymalizacji serwisów internetowych, tak aby uzyskać 100 punktów w PageSpeed Insights. Dzisiaj zajmiemy się wskaźnikiem TTFB czyli wstępnym czasem reakcji serwera.

Idealnym pacjentem do optymalizacji TTFB będzie mój stary i zapomniany serwis https://who-called.eu/pl. Im więcej danych przybywało, tym dłużej strona się ładowała, aż doszedłem do momentu, że wstępny czas reakcji serwera wyniósł aż 4 sekundy! Dlatego trzeba było się tym szybko zająć.

Aktualny czas to 0.62 sekundy i nadal nie jest to idealny wynik, jednak zajmowałem się tylko zapytaniami SQL.

Po pierwsze: ogranicz ilość zapytań SQL

Najprostsza sytuacja. Na stronie masz X ostatnich komentarzy. Także proste zapytanie do bazy danych wyglądało tak:

public function findLatestComments(int $limit)
{
    $builder = $this->createQueryBuilder('c');
    $builder->andWhere('c.moderated != 1');
    $builder->orderBy('c.createdAt', 'DESC');
    $builder->setMaxResults($limit);
    return $builder
        ->getQuery()
        ->getResult();
}

W twigu wygląda to tak:

{% for comment in latestComments %}
  <li>
            <a href="{{ path('search_number', { number: comment.number.phoneNumber}) }}"
               title="{{ comment.number | anchorTitle }}">
                {{ comment.number.phoneNumber }}</a>

            <div>{{ comment.comment }}</div>
            <span class="vote vote-{{ comment.vote }}"> {{ names[comment.vote] }}</span>
        </li> 
{% endfor %}

Ilość zapytań: 23 czas wykonania: 1482.71 ms. Dość słabo nie? To teraz pierwsze rozwiązanie:

public function findLatestComments(int $limit)
{
    $builder = $this->createQueryBuilder('c');
    $builder->select('c, partial n.{phoneNumber, maxVotes}');
    $builder->andWhere('c.moderated != 1');
    $builder->leftJoin('c.number', 'n');
    $builder->orderBy('c.createdAt', 'DESC');
    $builder->setMaxResults($limit);
...
    

Zmiana niewielka. Dodałem joina oraz uzupełniłem selecta.

Database Queries 8 Query time 1344.02 ms

Jednak ilość zapytań jest zauważalna. Doctrine działa na tej zasadzie, że nie ładuje sobie od razu wszystkich relacji z encji do pamięci, tylko czeka na wywołanie konkretnej relacji. W tym wypadku wyciągnięcie w twigu numeru telefonu i jego oceny.

Z tego powodu od razu dodałem ładowanie tych dwóch elementów z encji numeru, przez co ilość zapytań spadła z 22 do 8. Nawet troszkę czas się polepszył.

Po drugie cache zapytań

Domyślnie cache działa per request. Dlatego jak kilka razy pobieramy obiekt, to wykorzystujemy ten załadowany w bazie. Jednak tutaj na stronie głównej jest wykres. Za każdym razem muszę pobierać tutaj ilość telefonów z maksymalną oceną (np niebezpieczny). Takich typów mam 4, które za każdym razem się wykonują. Nie jest to jakaś mega ważna rzecz na stronie dlatego można dodać jej cache.

Jak masz zapytania w QueryBilderze wystarczy, że dopiszesz linijkę:

->enableResultCache(3600, 'key')

Pierwszy parametr to klasycznie TTL w sekundach, kolejny to unikalny klucz. W ten sposób ograniczyłem ilość zapytań z 8 do 4 także wynik coraz bardziej zadowalający. Oczywiście do ostatnich komentarzu taki cache też mogę dodać:

$builder = $this->createQueryBuilder('c');
$builder->select('c, partial n.{phoneNumber, maxVotes}');
$builder->andWhere('c.moderated != 1');
$builder->leftJoin('c.number', 'n');
$builder->orderBy('c.createdAt', 'DESC');
$builder->setMaxResults($limit);
return $builder
->getQuery()
->enableResultCache(3600, 'latest_comments'. $limit)
->getResult();

I kolejne zapytanie mi odpadnie.

Ważna kwestia. Cache jest wyłączony na środowisku developerskim. Najprościej, aby je włączyć to skopiuj zawartość pliku /packages/prod/doctrine.yaml do /packagas/dev/doctrine.yaml. Wtedy sobie sprawdzisz, czy faktycznie cache działa odpowiednio.

Polecam Ci wejść w profiler i tam poszukać jakie zapytania wykonują Ci się najdłużej i czy możesz je wrzucić do cache tak jak ja w tym wypadku.

Po trzecie: indeksy

Index w bazie danych to jest taka informacja dla MySQL, że musi przechowywać dane wg tego klucza w taki sposób, aby wyszukiwanie po tym polu było szybsze.

Wadą indeksów jest to, że SELECTY są szybsze, ale INSERTY i UPDATE wolniejsze. Dlatego wszelkie indeksy należy robić z głową, dlatego ten punkt jest jako ostatni, bo jest najbardziej czasochłonny.

Gdy już przeanalizujesz swoje zapytania, wyłapiesz pola, które powinny być indeksowane to czas na dodanie indeksów. Wystarczy dopisywać poszczególne indeksy w encji:

* @ORM\Table(name="numbers", indexes={
*     @ORM\Index(name="number_idx", columns={"phone_number"}
*     )})

Po czwarte: sprawdź konfigurację serwera

W moim wypadku akurat brakło odpalonego opache. Jest to wbudowany silnik cache, który trzyma skompilowany kod PHP w pamięci. Uruchomienie tego modułu zajęło mi nawet nie 5 minut, a zbiło kolejne 0.2 sekundy!

Inne sposoby optymalizacji

Ja tymi trzema sposobami zbiłem czas wstępnej odpowiedzi serwera z 4 do 0.61s. Nadal nie jest to idealny czas, jednak pokazuje jak drobnymi poprawkami, można bardzo mocno zoptymalizować czas wstępny odpowiedzi serwera.

Jednak jeśli Twój serwis nie jest taką prostą stronką to możesz poczekać, aż napiszę kolejne artykuły lub samodzielnie poszukać informacji o

  • replikowanie baz danych
  • redis
  • mongodb
  • elasticsearch
  • varnish
  • itp… 😉

Podsumowując

Zazwyczaj próbuje napisać tutaj coś mądrego, jednak tutaj nie przychodzi nic innego jak patrz jak w trzech prostych krokach można zoptymalizować swoją stronę opartą na Symfony i… patrz jak piszesz zapytania, bo pierwszy krok bez problemu mogłem uniknąć gdym pomyślał i nie patrzył na zasadzie: „szybko szybko trzeba wypuścić, potem się poprawi” 😉

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *