Razno Tehnologija Vodič

Kako da optimizujete svoje PHP aplikacije

optimizacija PHP koda

Planiranje brze i pouzdane PHP aplikacije zahteva da od početka imate jasnu sliku o tome šta sve utiče na njene performanse. Bez obzira da li pravite sopstveni CMS sistem ili e-commerce platformu, važno je da od početka razvoja aplikacije znate koji to faktori utiču na njenu brzinu i na koji način da pravilno optimizujete svoju PHP aplikaciju. 

Zato ćemo u ovom tekstu predstaviti neke od najvažnijih faktora koji određuju kako vaša aplikacija radi i objasniti kako da je učinite maksimalno efikasnom.

Faktori koji utiču na performanse PHP koda

Svi programski jezici dele sličnu logiku i imaju dosta zajedničkih funkcionalnosti. Ipak, neki programski jezici su pogodniji za određenu vrstu zadataka od drugih. 

Recimo, JavaScript je pogodniji za razvoj front-end aplikacija, dok je Python uglavnom našao primenu u AI i data science projektima. Isto tako, PHP je svoju populanost stekao kao prvi izbor za razvoj back-end web aplikacija. Isto važi i za sve ostale programske jezike koji su zbog nekih specifičnih karakteristika našli primenu u različitim nišama razvoja aplikacija.

Sve te različitosti su posledica velikog ekosistema aplikacija koji je kroz decenije evoluirao u okruženje koje poznajemo danas.

Upravo u tom ekosistemu postoje različiti faktori koji utiču na performanse našeg koda. S obzirom da se u ovom tekstu primarno bavimo PHP-om, navešćemo neke od najvažnijih faktora koji utiču na performanse PHP koda.

  • Kvalitet koda: Ovo bez sumnje može biti primenjeno na svaki programski jezik. Pisanje čistog, dobro strukturiranog koda koji je pritom lak za održavanje, može u velikoj meri uticati na performanse vaše aplikacije. Primena najboljih praksi prilikom pisanja koda, izbegavanje suvišnih linija koda i korišćenje efikasnih algoritama će rezultirati i bržim izvršavanjem koda.
  • Serverski resursi: Konfiguracija serverskog hardvera i softvera igra važnu ulogu u optimizaciji performansi vašeg PHP koda. Pobrinite se da na raspolaganju imate dovoljno procesorske snage, memorije i skladišnog prostora za rad vaše aplikacije. Optimizacija serverskih podešavanja kao što su PHP memory_limit, max_execution_time može da napravi značajnu razliku u performansama vaše PHP aplikacije. Takođe, izuzetno je važno i na kakvom serveru se nalazi vaša aplikacija. Za najbolje performase preporučujemo skalabilnu i brzu Performance Max platformu.
  • Baza podataka: Loše optimizovani upiti ka bazi podataka i loše indeksiranje mogu prilično da uspore vašu PHP aplikaciju. Smanjenje broja poziva ka bazi podataka, korišćenje odgovarajućeg indeksiranja i implementiranje strategija keširanja mogu dosta da ubrzaju performanse vaše PHP aplikacije.
  • Izvršavanje koda: Upravljanje sa više istovremenih zahteva može negativno da utiče na resurse vaše aplikacije. Primenom efikasnog upravljanja sesijama, raspoređivanjem opterećenja (load balancing) i asinhrono procesiranje zahteva može povoljno da utiče na raspoređivanje opterećenja i poboljšanje performansi aplikacije.
  • Mrežni saobraćaj: Spore mrežne konekcije mogu da budu uzrok loših performansi vaše PHP aplikacije. Smanjenje spoljnih zahteva i kompresovanjem fajlova može da smanji kašnjenje.

Kako da ubrzate svoje PHP aplikacije

Iako optimizacija PHP koda zahteva da obratite pažnju na više faktora, postoje i neke stvari koje, ako ih primenite, brzo daju dobre rezultate. Jedan od njih je ažuriranje verzije PHP-a na noviju verziju.

Svaka nova verzija PHP-a donosi značajna poboljšanja u odnosu na prethodnu verziju, što vrlo često podrazumeva da novija verzija ima bolje optimizovan kod koji kao takav ima dosta uticaja na brzinu rada vaše PHP aplikacije.

Na primer, PHP 7 je doneo značajna poboljšanja u odnosu na PHP 5.6, praktično duplirajući brzinu PHP aplikacija. Ovo pre svega može da zahvali Zend engine-u 3.0 koji se pojavio sa ovom verzijom, ali i poboljšanom korišćenju memorije. Ove dve novine su bile dovoljne da značajno ubrzaju performanse PHP-a.

Isto tako, sa verzijom PHP 8 je po prvi put predstavljen Just-In-Time (JIT) kompajler, koji dinamički kompajlira isvršne delove vašeg PHP koda u mašinski jezik, na taj način dodatno ubrzavajući vreme izvršavanja aplikacije. Ova verzija PHP-a je došla i sa drugim značajnim poboljšanjima, kao što su imenovani argumenti i nullsafe operator, koja mogu da vam pomognu da pišete čistiji i efikasniji kod.

Saveti za optimizaciju PHP performansi

Svi znamo koliko je važno da naša aplikacija radi brzo, ali nije uvek potpuno jasno šta je sve potrebno da uradimo da bi se to postiglo. Kao i kod svakog drugog kompleksnog sistema, ne možete unapred da znate šta sve treba optimizovati dok ne otkrijete gde je tačno uzrok sporijeg rada vaše aplikacije. 

Ovde možemo da napravimo paralelu sa automobilom: sigurno nećete menjati ceo motor osim ukoliko niste sigurni da baš on pravi problem. U stvarnosti je veoma retko da je problem u celom motoru. Uglavnom je problem u jednom ili nekoliko delova motora. Tako je i sa vašim kodom.

Zato pre bilo kakvih izmena u kodu moramo precizno da utvrdimo koje to tačke prouzrokuju sporiji rad aplikacije.

Profilisanje aplikacije

Da bismo otkrili uzrok sporijeg rada aplikacije, najdirektniji put je da uradimo njeno profilisanje. Alati kao što su Xdebug i Blackfire.io beleže vreme izvršavanja i potrošnju memorije za svaku funkciju i svaku liniju koda.

Xdebug primer (php.ini konfiguracija)

zend_extension=xdebug.so

xdebug.mode=debug,profile

xdebug.output_dir="/path/do/xdebug_profajla"

Iznad je prikazan primer podešavanja Xdebug-a koji generiše tzv. cachegrind fajlove koji sadrže sve informacije o izvršenim funkcijama. Te fajlove kasnije učitavate u alate kao što su KCacheGrind ili Webgrind da biste vizuelno videli hijerarhiju poziva i precizno uočili gde aplikacija gubi najviše vremena u radu.

Umesto da nagađate šta je uzrok sporijeg rada, primenom navedenog podešavanja odmah ćete imati tačan broj milisekundi i količinu memorije koju troši svaka funkcija. Na osnovu toga možete da ciljate konkretne problematične delove koda.

Tehnički gledano, Xdebug se kači na Zend Engine i presreće sve pozive funkcija, beležeći pritom tačno vreme ulaska i izlaska iz svake funkcije, kao i količinu memorije koju ona troši. Zatim te podatke zapisuje u formatu pogodnom za prikazivanje u alatima kao što je KCacheGrind. 

Kada ih vizuelizujete, dobićete grafički prikaz gde možete videti koliki procenat ukupnog vremena i memorije je otišao na svaku pojedinačnu funkciju, kao i puteve poziva (koju funkciju je pozvala koja druga funkcija). Tako postaje jasno šta treba prvo da optimizujete u svojoj PHP aplikaciji.

Mikro-optimizacije u PHP kodu

Čim utvrdite koje funkcije ili petlje troše najviše vremena, naša preporuka je da pređete na mikro-optimizacije. Čak i naizgled banalne razlike u PHP-u mogu da imaju veliki uticaj ako se te operacije ponavljaju mnogo puta.

a) is_null() vs. isset()

U PHP-u često imamo potrebu da proverimo da li je neka promenljiva već definisana i ima li vrednost, a upravo tu mnogi programeri posežu za funkcijom is_null(). Međutim, iskusnije kolege sugerišu da isset() može da bude brža i efikasnija opcija. Hajde sada da vidimo zašto je to tako, i kako ta naizgled sitna razlika može da utiče na performanse.

// Sporije i nepotrebno

if (is_null($var)) {

    // ...

}

// Efikasnije

if (!isset($var)) {

    // ...

}

Hajde da odmah vidimo kako primer efikasnijeg koda iznad radi i na koji način on ubrzava vašu aplikaciju.

  • Kada pozovete is_null($var), PHP mora prvo da proveri da li promenljiva uopšte postoji, pa tek onda da utvrdi da li joj je vrednost null. Dakle, u pozadini se vrši više koraka i izvrši se više internog koda pre nego što dobijete odgovor.
  • isset($var) direktno gleda u interni symbol table — strukturu koju PHP održava kako bi znao koje promenljive su definisane u kojem opsegu (scope) i šta im je vrednost. Ako tamo nađe da promenljiva ne postoji ili je null, vraća false bez ikakve dodatne logike.
  • U petljama sa mnogo iteracija, ovo vraćanje u dublje provere za is_null() može da izazove vidljiv overhead. Sa druge strane, isset() najčešće rešava slučaj u samo jednom, dosta kraćem koraku.

b) == vs. ===

U PHP-u postoje dva načina za poređenje vrednosti: labavo poređenje == i strogo poređenje ===. Iako na prvi pogled izgledaju slično, ta mala razlika u jednom znaku može da napravi razliku i u određenim slučajevima može da utiče na brzinu rada vaše PHP aplikacije. 

// Implicitna konverzija

if ('10' == 10) {

    // ...

}

// Brže i sigurnije

if ('10' === 10) {

    // ...

}

Na nivou jedne uslovne provere, ta razlika u brzini == i === nije velika, ali čim se takvo poređenje nađe u velikoj petlji ili na mestu koje se često poziva, dodatni korak konverzije kod labavog poređenja postaje primetan.

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda.

  • Operator == radi labavo poređenje, što znači da, ako tipovi nisu isti, pokušava da konvertuje jednu stranu u tip one druge strane (npr. string '10' u broj 10). Taj proces konverzije uključuje proveru da li je string numerički i koliko to znakova zauzima, i tek nakon toga donosi zaključak da li su vrednosti jednake.
  • Operator === radi striktno poređenje na nivou bajtova bez ikakve konverzije tipova. Ako tipovi nisu identični, momentalno vraća false. Time se preskaču dodatni koraci za obradu i konverziju, a i šanse za grešku (poput '10' == 10 je true) se smanjuju.

Ovo postaje bitno u velikim petljama ili u važnim logičkim mestima koda gde svaka dodatna operacija može da se oseti na ukupnoj brzini.

c) array_map / array_filter umesto ručnih petlji

Petlje su sastavni deo mnogih algoritama, ali u PHP-u svaka ručna implementacija ume da sakrije nepotreban interpretativni overhead. Kada prolazite kroz velike nizove, čak i mala razlika po ciklusu može da se nagomila u značajan gubitak vremena. Zato je ključno da, gde god je moguće, iskoristite ugrađene C-funkcije koje nudi PHP, poput array_map ili array_filter, jer one obavljaju čitavu obradu u jednom dahu unutar optimizovanog dela jezgra. Evo kratkog poređenja:

// Ručno

$rezultat = [];

foreach ($niz as $stavka) {

    $rezultat[] = strtolower($stavka);

}

// Ugrađena funkcija

$rezultat = array_map('strtolower', $niz);

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • Kod ručno napisane petlje, PHP interpretira svaki korak (inicijalizaciju, uslov, inkrementaciju i ono što radite unutar tela petlje), što kod velikih nizova može dosta da utiče na brzinu izvršavanja vašeg koda..
  • array_map i array_filter su nativne C funkcije pozvane unutar PHP-a, koje preuzimaju kompletan niz i jednim potezom obavljaju obradu bez vraćanja u interpretativni mod za svaku stavku. Tako dobijate brži prelaz kroz niz i manje overhead-a.

Kada na ovaj način obradite desetine ili stotine hiljada elemenata, razlika može biti prilično velika i možete uštedeti dosta vremena prilikom izvršavanja vašeg koda.

OPcache i JIT kompajliranje

PHP je interpretiran jezik, tako da se svaka .php datoteka prilikom svakog zahteva parsira i kompajlira u interne opcode-ove. Ovo može postati usko grlo ukoliko vaša aplikacija ima mnogo fajlova ili koristi neki framework.

Opcode-ove možete da zamislite (operacione kodove) kao jednostavnije, unutrašnje instrukcije koje PHP stvara nakon što pročita i razume vaš kod. Umesto da direktno izvršava originalni tekst, PHP pravi ovaj skraćeni niz komandi (opcode) i potom ih pokreće. 

Ako koristite OPcache, već jednom napravljeni opcode-ovi se čuvaju  u memoriji, pa se neće ponovo praviti za svako učitavanje iste skripte. Tako se štedi vreme i smanjuje opterećenje servera.

Umesto da se kod kompajlira prilikom svakog request-a, OPcache jednom uradi kompajliranje i čuva rezultate u memoriji. Na taj način, pri narednim zahtevima, nema potrebe za ponovnim parsiranjem istih fajlova.

Primer konfiguracije u php.ini:

zend_extension=opcache.so

opcache.enable=1

opcache.memory_consumption=128

opcache.max_accelerated_files=10000

opcache.revalidate_freq=60

opcache.enable_cli=1

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • Kada PHP učitava neki fajl prvi put, Zend Engine ga pretvara u opcode (niz instrukcija za izvršavanje), prolazeći pri tome kroz leksičku i sintaksnu analizu i pravljenje apstraktnog sintaksnog stabla (AST). Bez keširanja, sve to se radi svaki put kada neko zatraži taj fajl.
  • Sa OPcache-om, jednom kreirani opcode-ovi se čuvaju u memoriji (konkretno u nizu memorijskih slotova definisanih parametrima kao što su memory_consumption i max_accelerated_files). Kada isti fajl ponovo zatraži klijent, PHP više ne prolazi kroz kompletan proces parsiranja – dovoljno je samo da uzme gotove opcode-ove iz memorije.
  • revalidate_freq daje instrukciju OPcache-u koliko često da proverava da li su se fajlovi na disku izmenili (tipično za produkciju dovoljna je ređa provera). Na taj način štedite CPU, jer se ne radi stalna verifikacija svake sekunde.

Ovaj pristup je najkorisniji kod većih projekata koji se oslanjaju na desetine ili stotine .php fajlova, jer svaki request inače mora da prođe kroz celu strukturu. Sa OPcache-om, jednom keširani fajl primetno ubrzava vreme od klijentskog zahteva do odgovora.

JIT (Just-In-Time) kompajliranje

JIT (Just-In-Time) kompajliranje predstavlja dodatni korak u optimizaciji PHP-a, uveden s verzijom 8, gde se prevod koda ne zaustavlja samo na opcode-ovima. Umesto da Zend Engine svaki put interpretira opcode, JIT na osnovu dinamike izvođenja koda (tzv. vrućih putanja) prevodi te najčešće korišćene elemente direktno u mašinski kod za vaš procesor. 

Kada sledeći put dođe do tih delova, PHP više ne prolazi kroz interpretativni sloj, već se odmah izvršava mašinski kod, baš kao da je aplikacija napisana u C ili nekom drugom kompajliranom jeziku.

Drugim rečima, JIT radi po principu detekcije hot spotova u aplikaciji: tokom rada prati koje funkcije ili blokovi koda se najčešće izvršavaju i upravo te delove kompajlira u letu (u realnom vremenu). 

Rezultat toga je da operacije koje se često ponavljaju, kao što su matematičke intenzivne kalkulacije, obrade velikih nizova ili konverzije slika, dobijaju značajno ubrzanje, jer je deo interpretacije skraćen. 

Ako vaše PHP aplikacije provode najviše vremena čekajući odgovor baze ili praveći IO operacije, efekti JIT-a biće manji, ali za CPU-intenzivne zadatke, može da napravi osetnu razliku u performansama.

Primer minimalnog podešavanja za JIT u php.ini:

opcache.jit_buffer_size=64M

opcache.jit=tracing

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • opcache.jit_buffer_size definiše memoriju rezervisanu za kompajlirane segmente koda, gde PHP smešta mašinske instrukcije.
  • Podešavanje opcache.jit=tracing znači da PHP analizira tok izvođenja i pravi statistiku o tome koji delovi se najviše pozivaju. Kada proceni da je neka funkcija ili kodni blok naročito učestao, kompajlira ga u letu direktno u instrukcije za procesor.

Na nivou niskog sloja, JIT zaobilazi interpretativni mehanizam i izvršava kod slično kao da ste ga preveli pomoću C kompajlera, samo što se sve to dešava dinamički dok PHP radi.

Optimizacija baze podataka i keširanje podataka

Loše osmišljen SQL često je glavni uzrok sporijeg rada PHP aplikacija, čak i u situacijama kada je sam PHP kod dobro optimizovan. Zamislite situaciju gde iz baze selektujete sve kolone iz tabele od nekoliko miliona redova (SELECT * FROM products), pa tek onda u PHP-u filtrirate podatke. U tom slučaju veliki deo vremena otpada na prenos i obradu kompletne tabele, dok ste uz optimizovanije upite i indekse mogli da dobijete samo relevantne redove i kolone.

Kada u upitu jasno naznačite koje kolone vas zanimaju (npr SELECT id, name, price) i usmerite pretragu (npr WHERE category_id = 5), vi uslovljavate bazu da učita i preda manji, fokusiran dataset. Uz to, kreiranje indeksa na ključnim kolonama (poput category_id, created_at, itd.) omogućava da baza preskoči prelistavanje svih redova i da umesto toga direktno pristupi traženim vrednostima. 

U suštini, sve radi efikasnije: i sam SQL server koji manje čita sa diska i manje obrađuje, i vaš PHP kod koji dobija značajno manji broj rezultata, pa ih brže obrađuje ili prikazuje.

Ako se neki upiti ponavljaju (npr. generišu se za svaku stranicu sa listom proizvoda), dodatno se isplati keširati njihove rezultate u brzu memoriju (Memcached, Redis). Na taj način, baza se ne opterećuje iznova istim zahtevom, a vaša aplikacija odgovara neuporedivo brže.

Dva primera SQL upita:

-- Neoptimizovano

SELECT * FROM products;

-- Optimizovano (cilja samo potrebne kolone i limit)

SELECT id, name, price 

FROM products 

WHERE category_id = 5 

ORDER BY created_at DESC 

LIMIT 50;

Prvi upit preuzima sve, drugi radi selektivno. Osim toga, kreiranje indeksa na category_id i created_at koloni može drastično ubrzati pretragu.

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • U neoptimizovanom slučaju, baza zaista čita sve kolone, za sve redove u tabeli, a posle toga PHP-u vraća veliku količinu podataka koje vi tek kasnije filtrirate. To može biti problem kada imate milione redova.
  • U optimizovanom primeru, baza koristi SQL upit koji direktno cilja samo neophodne kolone i ograničava rezultate na određeni broj (LIMIT). Ako postoji indeks na category_id i created_at, baza će onda koristiti tu strukturu (poput B-stabla) da brzo pronađe upravo one redove koji vam trebaju. To značajno smanjuje IO operacije i vreme obrade podataka.

Keširanje rezultata

Keširanje rezultata u memoriji omogućava da izbegnete stalno ponavljanje identičnih upita koji opterećuju bazu. Zamislite da gotovo svaka stranica na vašem web sajtu prikazuje popularne proizvode i da za to svaki put radite upit u bazu, sortirate rezultate i vraćate JSON ili HTML. 

Uz Memcached ili Redis, možete samo jednom da uradite taj upit i zatim da sačuvate dobijene podatke u memoriji. Pri svim sledećim pozivima, aplikacija će najpre da proveri da li postoji već keširan rezultat pod određenim ključem (npr. popularniProizvodi). Ako postoji, odmah ga vraća, bez ikakve komunikacije sa bazom.

Tehnički posmatrano, Memcached i Redis su servisi kojima putem TCP/UDP porta prosleđujete ključ-vrednost (kod Redis-a i složenije strukture, poput listi i skupova). 

Na primer, pri dobijanju rezultata iz baze, možete da ih pretvorite u JSON i zajedno sa vremenom trajanja (TTL – Time To Live) upisati u keš. Kada klijent sledeći put zatraži iste podatke, dovoljno je da pročitate sadržaj iz keša i pošaljete korisniku. To značajno skraćuje vreme obrade (jer se preskaču SQL upiti i sortiranje) i rasterećuje bazu.

Po isteku podešenog vremena, keširani podaci se uklanjaju, čime se sprečava da korisnici dobiju zastarele podatke. Ukoliko aplikacija detektuje da su se podaci u međuvremenu promenili (npr. cena proizvoda je ažurirana), može sama da obriše ključ iz keša ili osveži njegovo vreme trajanja. Na ovaj način, keširani rezultati ostaju ažurni, a baza i server postaju brži i efikasniji.

Redis primer:

$redis = new Redis();

$redis->connect('127.0.0.1', 6379);

// Čuvanje rezultata

$popularniProizvodi = [...]; 

$redis->set('popularniProizvodi', json_encode($popularniProizvodi), 300); 

// 300 sekundi (5 minuta) će važiti ovaj podatak

// Kasniji dohvat

$popularniProizvodi = json_decode($redis->get('popularniProizvodi'), true);

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • Redis internо funkcioniše kao brzi key–value store u memoriji, znači nema potrebe da se opet povezuje s bazom i pravi upit.
  • Kada biste svaki put dohvatali te podatke iz baze, morali biste da radite SQL upit, da baza pretražuje tabele i formira odgovor. Sa keširanim podacima, dobijate instant odgovor iz memorije.
  • Ovaj mehanizam naročito ima smisla kod scenarija gde se ne očekuju česte izmene podataka koje vraća upit (ili barem ne tako česte u odnosu na broj čitanja), jer onda retko morate da ažurirate keš, a često čitate iz njega.
  • Memcached radi na sličnom principu, ali nudi isključivo key–value pristup bez složenih struktura podataka koje Redis ima (npr. liste, setove, sorted setove). Izbor često zavisi od toga da li vam treba bogatija struktura (Redis) ili samo običan, ali brz keš (Memcached).

Pametno rukovanje autoloading-om i lazy loading-om

Kada radite na kompleksnim PHP projektima, često se desi da imate desetine ili stotine klasa i biblioteka. Možda pritom koristite neki veći framework, nekoliko dodatnih paketa i sopstvene module. 

Ako autoload nije optimalno podešen, PHP pri svakom zahtevu može da vrši nepotrebna pretraživanja direktorijuma i pokušava da učita sve definisane klase, čak i one koje se realno ne koriste odmah. 

Rezultat je duže inicijalno vreme učitavanja aplikacije (takozvani „bootstrap“ aplikacije), jer se gubi vreme na detektovanju i include-ovanju suvišnih fajlova.

U tom slučaju preporuka je da:

Optimizujete autoload Composer-a:

Kada pokrenete:

composer dump-autoload --optimize

Composer analizira sve klasa/fajl odnose u vašem projektu i kreira jedinstvenu mapu (class map) u kojoj je eksplicitno navedeno gde se tačno nalazi koja klasa. Tom mapom se eliminiše potreba za tzv. lutajućim pretragama po folderima, jer se pri prvom zahtevu jasno zna putanja do fajla za svaku klasu.

Interno, to znači da spl_autoload_register prvo proveri generisanu mapu i skoro momentalno pronađe fajl, umesto da se oslanja na više fallback ruta i file_exists provera.

Lazy loading (PSR-4 pristup):

Savremeni framework-ovi i biblioteke uglavnom poštuju PSR-4 standard, pri čemu se klase učitavaju po lazy loading principu – dakle, samo onda kad se zaista pozovu. Umesto da na početku projekta uključite sve moguće module i biblioteke, autoloader dinamiku prepušta prvom new Klasa() ili referenciranju naziva klase. Na taj način, ako neka komponenta nije pozvana, ni njen fajl se neće učitati, čime se bitno skraćuje početno vreme izvršavanja.

Tehnički gledano, ovakav pristup kombinuje prednosti:

  • Precizne mape klasa (tako da se ne vrši beskrajna pretraga direktorijuma).
  • Deferred load-a (ako klasa nikad nije pozvana u konkretnom request-u, nema razloga da se njen fajl uopšte čita).

Zato je dobra praksa, naročito kad se doda nova biblioteka ili promeni struktura projekta, da ponovo pokrenete composer dump-autoload --optimize. Tako obezbeđujete da vaš autoloader uvek radi najbrže što može i ne zadržava aplikaciju nebitnim učitavanjem fajlova koji vam u tom momentu nisu ni potrebni.

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • autoload obično radi tako što, pri pokušaju instanciranja neke klase, PHP poziva funkciju registrovanu u spl_autoload_register. Ta funkcija pretražuje odgovarajuće direktorijume i kreira putanju (na osnovu namespace-a, na primer).
  • Ako niste optimizovali autoload, svaki put kada se zatraži klasa, autoloader može da prođe kroz više koraka i file_exists provera da bi našao pravi fajl. Kod većih projekata, to brzo naraste u stotine ili hiljade pokušaja.
  • Optimized class map kreira jednu veliku asocijativnu mapu (obično u fajlu autoload_classmap.php) gde svaki namespace/klasa ima direktnu putanju. Kad se zatraži klasa, odmah se zna gde je, pa nema nepotrebnog lutanja.

Ograničavanje veličine odgovora i gzip kompresija

Često se dešava da neke stranice ili API endpoint-i vraćaju kompletne spiskove sa velikim brojem podataka (npr. hiljade ili desetine hiljada redova), i to sve u jednom odgovoru. 

Takav pristup ima dve glavne posledice: povećava vreme generisanja odgovora na PHP strani (jer treba proći i obraditi toliki niz) i produžava samo slanje tih podataka kroz mrežu (veća veličina odgovora znači više paketa i duže vreme učitavanja za korisnika).

U tom slučaju možete uraditi sledeće:

Uvesti paginaciju ili segmentaciju – korisniku ne treba svih 10.000 stavki odjednom, već mu je dovoljno, recimo, 50 ili 100 po stranici (ili neki prilagođen limit).

Kako to izgleda u praksi:

  • U REST API-ju, umesto da vraćate sve, obično koristite URL parametre poput ?page=2&limit=50.
  • Na taj način, sam SQL upit u bazi može da sadrži LIMIT 50 OFFSET 50, pa baza isporučuje samo taj segment podataka (na primer, drugu stranicu).
  • Onda i PHP kod, koji formatira taj rezultat u JSON ili HTML, obrađuje svega nekoliko desetina redova, a ne više hiljada.
  • Klijent (browser ili mobilna aplikacija) dobija manju količinu podataka, što znači manju veličinu odgovora (JSON/HTML). Tada je i prenos brži, pa se poboljšava korisničko iskustvo –  liste se učitavaju veoma brzo, a korisnik može po potrebi da zahteva narednu stranicu.

Tehnički posmatrano, smanjenje dataset-a direktno smanjuje CPU i memorijske potrebe na backend-u, ali i broj bajtova koje treba poslati preko mreže. Što je dataset manji, to je odziv od servera brži. 

Na nivou mreže, manji broj bajtova znači i manje paketa koje klijent mora da prihvati i parsira, što smanjuje ukupnu latenciju. Кada su u igri veći brojevi korisnika, ovakav pristup pravljenja numerisanih odgovora postaje još važniji da bi aplikacija ostala responsivna i stabilna.

Ako želite da smanjite veličinu HTML, JSON, CSS ili JS fajlova koje vaša aplikacija šalje korisnicima, možete uključiti tzv. gzip (ili srodne formate) kompresiju na nivou servera ili u php.ini.

Kako to izgleda u praksi:

  1. Klijent (browser ili API servis) najavi da razume kompresiju dodavanjem zaglavlja Accept-Encoding: gzip u svoj zahtev.
  2. Server (Apache, Nginx ili PHP), ako je podešen da podržava gzip, onda kompresuje odgovor. U zaglavlju odgovora setuje Content-Encoding: gzip kako bi klijent znao da treba da ga dekompresuje.
  3. Klijent dobija kompresovan (značajno manji) fajl i potom ga raspakuje pre prikazivanja. To se sve odvija automatski, bez dodatnog posla za korisnika.

 Kako radi gzip kompresija:

  • Gzip analizira tok bajtova koje generiše PHP (ili statični fajlovi na serveru) i primenjuje algoritam koji prepoznaje sekvence koje se ponavljaju i njih zamenuje kraćim referencama. Ovo ima najveću primenu kod tekstualnih formata (HTML, JSON, XML, CSS, JS), gde se ponavljanja javljaju često. Kod binarnih fajlova (npr. slika ili PDF) efekat je manji jer su već delimično kompresovani.
  • Dobijeni rezultat je fajl koji može biti i nekoliko puta manji. To smanjuje broj mrežnih paketa koji moraju da se prenesu od servera do klijenta, pa korisnik brže dobija sav sadržaj.
  • Browseri i mnogi API klijenti lako dekompresuju taj sadržaj u memoriji, potpuno transparentno, pa krajnji korisnik ne primećuje ništa osim bržeg učitavanja.

Ovo sve ima smisla jer kada se manje podataka prenosi kroz mrežu, smanjuje se i latencija (naročito kod većih fajlova ili slabije veze). U zavisnosti od broja istovremenih korisnika, možete da postignete veliku uštedu u protoku, što pozitivno utiče i na troškove i na brzinu odgovora vaše aplikacije.

Ispravno upravljanje radom sa sesijama

Ako vaša PHP aplikacija često koristi sesije – recimo, čuvate informacije o korisniku, njegovoj korpi, preferencijama i slično – podrazumevani mehanizam je da se te sesije čuvaju u fajlovima (session handler = files). 

U tom slučaju, svaki put kada se sesija učitava ili čuva, PHP otvara odgovarajući fajl i postavlja lock, čime se onemogućava drugi zahtev sa istim session ID-jem da pristupi tom fajlu dok se ne završi prvi. 

To može da dovede do zagušenja, pogotovo ako više korisnika (ili više upita od strane istog korisnika) istovremeno pokuša da čita/piše sesiju.

U tom slučaju savetujemo da:

Prebacite sesije u Redis ili Memcached:
U php.ini ili sličnoj konfiguraciji, podesite:

session.save_handler = redis

session.save_path = „tcp://127.0.0.1:6379“

  1. Umesto fajlova, sesije će se čuvati u memoriji Redis/Memcached servera. Pristup memoriji je višestruko brži od IO operacija na disku, a i mehanizam zaključavanja (ili concurrency) je efikasniji, tako da retko dolazi do čekanja.
  2. Minimizujte podatke u $_SESSION:

Iako je sesija praktično samo privremeno skladište za korisničke podatke, pazite da ne čuvate ogromne nizove, objekte ili binarne podatke. Svako zapisivanje velikog sadržaja znači više memorije (ili fajla, ako ostanete na file-based). Pored toga, svako učitavanje sesije sa sobom vuče sve te podatke i time ujedno produžava odziv.

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • File-based sesije: PHP za svaku aktivnu sesiju kreira poseban fajl. Kada skripta počne, PHP otvori fajl, zaključa ga (file lock), očita njegov sadržaj, i eventualno ga ažurira pri kraju. Ako više zahteva pokuša da koristi isti session ID (npr. više AJAX poziva istovremeno), oni moraju da čekaju dok prvi ne završi rad s fajlom. Na većem broju zahteva, to brzo postane usko grlo.
  • Redis/Memcached sesije: umesto fajla, svi podaci sesije nalaze se u memorijskom kešu. Pristup se dešava gotovo trenutno, a istovremeni zahtevi se rešavaju daleko elegantnije, bez upadanja u velike redove za čekanje. Budući da Redis i Memcached rade preko TCP-a/UDP-a i održavaju sve u RAM-u, nema potrebe za stalnim otvaranjem i zaključavanjem fajlova na disku.
  • Svaki PHP zahtev po default-u na početku vuče $_SESSION (bilo iz fajla, bilo iz memorije), a na kraju ispisuje sve izmene nazad. U memorijskom režimu (Redis/Memcached), to čitanje i pisanje je veoma brzo, dok u file-based režimu može da podrazumeva duži IO. Usporavanje je posebno vidljivo kada imate mnogo istovremenih zahteva ili veće sesije.

Uklanjanje nepotrebnog koda i definisanje jasnih granica (SOLID principi)

Ako u svom projektu imate mnogo koda koji više ne koristite (zastarele klase, stare biblioteke, funkcije koje su davno zamenjene i slično), njihovo prisustvo i dalje utiče na parsiranje i autoload mehanizam. 

Čak i ako se neka klasa ne koristi često, činjenica da je prisutna u kodu znači da autoloader može (u najgorem slučaju) da je proverava, a i sam fajl može da se učita pri pokretanju (u zavisnosti od načina na koji je kod organizovan).

U tom slučaju savetujemo da:

  1. Povremeno čistite projekat od neiskorišćenih fajlova i klasa. Smanjite veličinu koda i broj fajlova koje treba indeksirati.
  2. Razbijte pretrpane klase na manje, logičnije module. Ako imate jednu klasu od nekoliko hiljada linija, postoji velika šansa da se učitava sve ili ništa, iako vam treba samo 5% njenih funkcionalnosti.
  3. Primenite SOLID principe (Single Responsibility, Open/Closed, itd.) tako da svaka klasa ima jasnu svrhu. Tako ćete izbeći scenario gde se na svakom pokretanju aplikacije insistira na gomili srodnih metoda koje su vam retko kad sve potrebne odjednom.

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • Kada je neka klasa veoma obimna, autoload ili sam interpretator može da provede više vremena parsirajući njen fajl, čak i ako će se na kraju iskoristiti samo jedan njen deo.
  • Ako imate lazy autoload (PSR-4, composer autoload optimizovan i sl.), manji fajlovi s pojedinačnim klasama brže se proveravaju i učitavaju. Ideja je da se izbegne čitanje i kompajliranje nečega što se stvarno ne koristi.
  • SOLID principi dodatno pomažu, jer kad je kod pravilno raspoređen, nepotrebne funkcionalnosti se ne uvlače bespotrebno u opseg. To čini ceo projekat fleksibilnijim i bržim za inicijalno pokretanje.

Drugim rečima, kad se držite čistog, urednog projekta i periodično uklanjate zaostale fajlove, automatski poboljšavate performanse svoje PHP aplikacije. Manje koda ujedno znači manje fajlova za učitavanje, brži autoload i manje šanse da nepotrebne klase zauzmu resurse u startu.

Korišćenje background radnika (queue) za duge procese

Ukoliko vaša PHP aplikacija ima neke komplikovane i dugotrajne operacije, kao što su generisanje velikih izveštaja, import ogromnih CSV fajlova ili slanje velikog broja e-mailova, idealno rešenje je da ih odvojite u pozadinske procese (tzv. radnike). Tako glavni deo aplikacije ostaje brz i responzivan, jer se dug posao ne odvija u toku HTTP zahteva.

U tom slučaju savetujemo da:

  1. Odaberite queue sistem – najčešće se koriste RabbitMQ, Beanstalkd ili Redis (u formi liste).
  2. Implementirajte radnika (worker) – to je PHP CLI proces koji stalno sluša na taj red (queue). Kad stigne novi zadatak, on ga pokreće.
  3. Glavna aplikacija samo prosleđuje zadatak u red i odmah vraća korisniku potvrdu (npr. „Zadatak je uspešno prosleđen na obradu“).

Da vidimo kako ovo utiče na brzinu izvršavanja vašeg PHP koda:

  • Bez queue-a, dugotrajne operacije moraju da se izvrše u okviru istog zahteva, pa korisnik čeka dok ceo posao ne bude gotov (što može da potraje 10, 20 ili više sekundi). Za to vreme, PHP-FPM ili web server proces bude zauzet i može da blokira ostale zahteve.
  • Kada imate queue, web aplikacija bukvalno samo generiše poruku (zadatak) sa neophodnim parametrima i stavlja je u red. Taj korak traje kratko (obično nekoliko milisekundi). Nakon toga možete odmah da odgovorite klijentu.
  • Worker koji je pokrenut u pozadini (kao zaseban skript), stalno proverava da li postoji nov zadatak u redu. Čim pronađe jedan, kreće sa obradom – bilo da je to čitanje velikog fajla i obrada, generisanje PDF-a ili slanje mejlova. nakon što završi taj zadatak obavesti bazu ili setuje neko polje, pa glavni deo aplikacije zna da je zadatak završen.
  • Za korisnika to znači brži odziv (ne mora da čeka da se posao obavi sinhrono), a za vas bolju iskorišćenost resursa. Glavni web server ili PHP-FPM radnik nije zaglavljen u dugoj operaciji, što poboljšava skalabilnost i sprečava gušenje drugih korisničkih zahteva.

Dodatne napomene o optimizaciji PHP-a

Na kraju, važno je da napomenemo da neke sitne, ali bitne, direktive u php.ini mogu da utiču na performanse i stabilnost vaše PHP aplikacije:

  • memory_limit
    Ovo podešavanje govori PHP-u koliki maksimalni iznos memorije jedan proces sme da iskoristi. Ako skripta pređe ovu granicu, PHP generiše grešku i prekida izvršavanje. To je korisno jer sprečava da pojedinačna skripta proguta svu memoriju sistema.

Ako je prenisko, možete dobiti mnogo grešaka: “Allowed memory size of X bytes exhausted…”kada aplikacija pokuša da potroši više memorije nego što je dozvoljeno, što može da bude problem na sajtovima sa velikim prometom ili složenim operacijama.

Ako je previsoko, skripte će možda da koriste više memorije nego što je stvarno potrebno, a to može povećati troškove za hosting (naročito ako plaćate po resursima) i otežati upravljanje potrošnjom.

  • max_execution_time
    Ovo je ograničenje na vreme (u sekundama) koliko jedna PHP skripta može da radi pre nego što server prekine njen rad. Namenjeno je sprečavanju beskonačnih petlji ili izuzetno dugih operacija koje mogu da zaključaju sistem.

Ako imate ozbiljno duge operacije (npr. generisanje složenih PDF-ova ili obradu ogromnih fajlova), bolje je da takve procese prebacite u pozadinske radnike (queue sistem), umesto da dižete max_execution_time na veoma visoke vrednosti. Na taj način aplikacija zadržava brzinu i odziv ka krajnjim korisnicima.

  • error_reporting
    Podešava nivo detalja o greškama koje će PHP prijavljivati i/ili prikazivati. U razvoju obično želite E_ALL (sve greške i upozorenja), jer vam pomaže da primetite svaku potencijalnu grešku. U produkciji, međutim, često smanjujete prikaz grešaka kako ne biste korisnicima izbacivali sve interne upozorenja i da ne biste punili log gomilom „notice” ili „warning” poruka.

Previše „notice” poruka (tipa „Undefined index…”) može bespotrebno da poveća opterećenje sistema, jer za svaku mora da se generiše i zapiše log. Obično se u produkciji biraju niži nivoi prijavljivanja, dok se potpuni debug i prijava svih grešaka ostavlja za razvojno okruženje.

Zaključak

Optimizacija PHP aplikacija ne svodi se na jednu, „magičnu“ izmenu, već podrazumeva pažljiv pristup koji obuhvata različite segmente razvoja i izvršavanja koda. Pored čistog i efikasnog koda, potrebno je uključiti i pravilan rad sa bazom podataka, upotrebu keširanja, fokus na mikro-optimizacije u PHP jeziku, kao i veće stvari poput OPcache, JIT kompajliranja ili prelaska na pozadinske radnike.

Dodatno, od velike koristi su i autoload optimizacija, pametno rukovanje sesijama, paginacija pri vraćanju velikih dataset-ova, kao i kompresija odgovora (gzip). Ne treba zaboraviti ni na pravilno podešavanje PHP-a (memory_limit, max_execution_time, error_reporting) i uredno organizovanje sopstvenog koda prema SOLID principima.

Uz primenu svih ovih tehnika i alata, dobićete bržu, stabilniju i skalabilniju PHP aplikaciju, lakšu za održavanje i spremniju za ozbiljniji nivo opterećenja. 

Na kraju, ključno je da se svaka optimizacija sprovodi na osnovu konkretnih podataka, najčešće dobijenih profilisanjem, kako biste targetirali baš ona mesta gde su performanse vaše PHP aplikacije zaista najugroženije.

Ostavi komentar

Vaša adresa neće biti objavljena