Valentin Cojocaru Web developer · Cluj-Napoca EN

Valentin
Cojocaru

Construiesc aplicații web și mă ocup de serverul, DNS-ul și baza de date din spatele lor.

blog

Yii2 & PHP 8 min de citit

De ce e lent site-ul tău Yii2 și cum afli cauza

Am văzut multe aplicații Yii2 lente. Sunt lente dintr-un set mic de motive care se repetă, iar soluția nu e aproape niciodată „adaugă Redis” sau „ia un server mai mare”. Soluția e să înțelegi ce face de fapt aplicația la fiecare cerere.

Iată cum abordez un site Yii2 lent când dau de unul.

Pasul 1: pornește debug toolbar

Dacă nu rulezi modulul de debug Yii2 în dezvoltare, depanezi pe orbește. Activează-l:

// config/web.php (development only)
if (YII_DEBUG) {
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] = [
        'class' => 'yii\debug\Module',
        'allowedIPs' => ['127.0.0.1', '::1'],
    ];
}

Acum fiecare încărcare de pagină îți arată numărul de interogări, timpul total, memoria folosită și lista completă de interogări. Pasul ăsta singur găsește problema în majoritatea cazurilor.

Cei patru vinovați obișnuiți

1. Problema N+1

Încarci o listă de 50 de articole și pentru fiecare apelezi $post->author->name. Sunt 51 de interogări: una pentru listă, câte una pentru fiecare autor. Debug toolbar îți va arăta 51 de SELECT-uri aproape identice.

Soluția e eager loading:

// Instead of this:
$posts = Post::find()->all();

// Do this:
$posts = Post::find()->with(['author', 'category', 'tags'])->all();

O interogare devine trei: articolele, toți autorii asociați, toate categoriile asociate. La 50 de articole, sunt 3 interogări în loc de 151.

2. Indexuri lipsă

Interogarea pare rapidă în dezvoltare, cu 200 de rânduri. În producție, cu 200.000 de rânduri, aceeași interogare ia 4 secunde, pentru că scanează tot tabelul. Lista de interogări îți arată SQL-ul, iar EXPLAIN rulat pe el îți arată dacă se folosește un index.

EXPLAIN SELECT * FROM post
WHERE status = 'published'
  AND category_id = 5
ORDER BY published_at DESC
LIMIT 20;

Dacă vezi type: ALL în rezultat, e o scanare completă a tabelului. Adaugă un index compus pe coloanele din WHERE și ORDER BY.

3. Încarci prea multe date

ActiveRecord face ușor să încarci modele întregi când ai nevoie doar de două câmpuri. Post::find()->all() selectează fiecare coloană, inclusiv câmpul cu tot conținutul HTML, pentru fiecare rând. Într-o listă probabil ai nevoie doar de titlu, slug și rezumat.

// Select only what you need
$posts = Post::find()
    ->select(['id', 'title', 'slug', 'excerpt', 'published_at'])
    ->where(['status' => 'published'])
    ->orderBy(['published_at' => SORT_DESC])
    ->limit(20)
    ->asArray()  // returns plain arrays, skips model overhead
    ->all();

4. Chiar PHP e gâtuirea

Uneori interogările sunt în regulă, iar logica aplicației e lentă. Profilarea de bază din PHP nu e grozavă, dar la aplicațiile Yii2 timeline-ul din debug toolbar îți arată de obicei ce componentă ia cel mai mult timp. Dacă ai nevoie de mai multe detalii, Xdebug cu un profiler ca Webgrind îți dă tot graful de apeluri.

Profilare în producție fără Xdebug

Nu poți rula mereu Xdebug în producție. În schimb, loghează cererile lente cu componenta de logging din Yii2:

// Add to config
'log' => [
    'targets' => [
        [
            'class' => 'yii\log\FileTarget',
            'levels' => ['profile'],
            'logFile' => '@runtime/logs/profile.log',
        ],
    ],
],

Apoi încadrează partea costisitoare din cod:

Yii::beginProfile('buildProductList');
$products = $this->productService->getList($filters);
Yii::endProfile('buildProductList');

Unde te uiți mai departe

După ce ai găsit partea lentă, soluția e de obicei una dintre acestea: eager loading, un index, un cache sau rescrierea interogării. În ordinea asta. Dacă pui cache pe o interogare lentă, ascunzi problema și o faci mai greu de reparat mai târziu. Repară întâi interogarea și pune cache doar dacă nu e de ajuns.

Pentru paginile al căror conținut se schimbă rar (un index de blog, o listă de produse), cache-ul HTTP cu antete Cache-Control corecte sau componenta de page cache din Yii2 îți dă adesea o îmbunătățire de 10 ori, aproape fără cost:

public function behaviors(): array
{
    return [
        [
            'class' => PageCache::class,
            'only' => ['index'],
            'duration' => 300, // 5 minutes
            'variations' => [Yii::$app->request->get('page', 1)],
        ],
    ];
}

Începe cu debug toolbar. Rezolvă interogările N+1. Adaugă indexurile lipsă. Apoi pune cache, dacă mai ai nevoie.

#Yii2 #PHP #MySQL #Performance