Optimalisering av Laravel-applikasjoner for høy ytelse
Laravel er kjent for mange ting, men hurtighet er ikke alltid en av dem. La oss utforske metoder for å øke ytelsen til Laravel-prosjektene dine.
Det er vanskelig å finne en PHP-utvikler som ikke har vært i kontakt med Laravel. Enten det er en juniorutvikler som setter pris på den raske utviklingssyklusen, eller en seniorutvikler som er tvunget til å lære rammeverket på grunn av markedets krav, er det en stor del av PHP-økosystemet.
Det er ingen tvil om at Laravel har gitt ny energi til PHP-verdenen. For mange ville PHP vært forlatt for lenge siden uten Laravel.
Laravel legger stor vekt på å forenkle utviklingsprosessen. Dette betyr imidlertid at rammeverket utfører betydelig arbeid i bakgrunnen for å sikre en komfortabel utvikleropplevelse. De «magiske» funksjonene som bare «fungerer», har ofte flere lag med kode som må aktiveres hver gang en funksjon kalles. Et enkelt unntak viser hvor dypt disse lagene kan gå:
Selv for det som ser ut til å være en kompileringsfeil i en av visningene, er det 18 funksjonskall involvert. Det er ikke uvanlig å se over 40 funksjonskall, spesielt hvis du benytter deg av flere biblioteker og utvidelser.
Disse lagene av kode kan gjøre Laravel treg i standardkonfigurasjonen.
Hvor treg er egentlig Laravel?
Det er vanskelig å gi et enkelt svar på dette spørsmålet. Det finnes ingen anerkjent, objektiv standard for å måle hastigheten til nettapplikasjoner. Det som er raskt for en, kan være tregt for en annen, og resultatene vil variere basert på forholdene.
En nettapplikasjons ytelse avhenger av flere faktorer: database, filsystem, nettverk, og hurtigbuffer. Selv en rask nettapplikasjon kan være treg hvis databasen er treg. Disse uforutsigbare faktorene er grunnen til at benchmarks er så populære, selv om de kan være misvisende. De gir oss likevel et referansepunkt.
Ifølge en GitHub kilde, sammenlignes PHP-rammeverk slik:
Som du ser, havner Laravel sist. Mens mange av disse rammene ikke er praktiske i virkeligheten, gir det et perspektiv på hvor treg Laravel kan være i forhold til andre populære rammeverk.
Denne «tregheten» merkes ikke alltid i daglig bruk. Det er først når applikasjonen opplever høy trafikk (200-500 samtidige brukere) at serverne begynner å bli overbelastet. Det å legge til mer maskinvare vil ikke nødvendigvis løse problemet, og kostnadene for infrastrukturen vil øke raskt.
Det finnes metoder for å få Laravel-applikasjoner til å yte raskere. Med de rette teknikkene kan du øke ytelsen betraktelig og redusere kostnadene.
Fire typer optimalisering
Optimalisering av PHP-applikasjoner kan deles inn i fire hovedkategorier:
- Språknivå: Bruk en raskere versjon av språket og unngå kode som bremser ytelsen.
- Rammenivå: Optimaliser innstillinger og funksjoner i rammeverket.
- Infrastrukturnivå: Juster PHP-prosessbehandleren, webserveren og databasen.
- Maskinvarenivå: Bytt til en raskere og kraftigere maskinvareløsning.
Alle disse optimaliseringsmetodene er viktige, men denne artikkelen fokuserer på optimaliseringer knyttet til selve rammeverket (type 2).
Det er ingen etablert standard for denne kategoriseringen. Den er laget for denne artikkelen. For å unngå forvirring, unngå å sitere denne inndelingen i jobbsammenheng.
Vær oppmerksom på n+1-databasespørringer
N+1-spørringsproblemet oppstår ofte når ORM (Object-Relational Mapping) benyttes. Laravel’s Eloquent ORM er svært praktisk, men det er lett å glemme hva som skjer i bakgrunnen.
Tenk deg en vanlig situasjon: Du skal vise en liste over bestillinger fra en liste med kunder. I et e-handelssystem eller i et rapporteringsgrensesnitt er dette en vanlig oppgave.
En kontrollerfunksjon i Laravel kan se slik ut:
class OrdersController extends Controller { // ... public function getAllByCustomers(Request $request, array $ids) { $customers = Customer::findMany($ids); $orders = collect(); foreach ($customers as $customer) { $orders = $orders->merge($customer->orders); } return view('admin.reports.orders', ['orders' => $orders]); } }
Elegant og enkelt. Men dette er en ineffektiv måte å skrive kode på i Laravel.
Når vi henter kunder med Eloquent, genereres en SQL-spørring:
SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);
Dette er som forventet. Dataene lagres i `$customers` samlingen.
Når vi går gjennom hver kunde og henter ordrene deres, utføres en ny spørring per kunde:
SELECT * FROM orders WHERE customer_id = 22;
Hvis vi har 1000 kunder, vil det totalt være 1001 spørringer (1 for å hente alle kunder, og 1000 for å hente ordrer per kunde). Dette er årsaken til navnet «n+1»-problemet.
Vi kan forbedre dette ved å bruke «ivrig lasting» (eager loading). Dette tvinger ORM til å utføre en JOIN-operasjon, slik at vi får alle nødvendige data i én spørring:
$orders = Customer::findMany($ids)->with('orders')->get();
Den resulterende datastrukturen er nestet, men ordredataene er lette å trekke ut. Den resulterende spørringen vil være som:
SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);
En enkelt spørring er mer effektiv enn tusen separate spørringer. Husk å bruke ivrig lasting, det er nesten alltid en god idé.
Cache konfigurasjonen
Laravel har flere konfigurasjonsfiler som gir rammeverket fleksibilitet. Du kan endre hvordan bilder lagres i `config/filesystems.php`, eller definere kødrivere i `config/queue.php`. Det finnes 13 konfigurasjonsfiler for ulike aspekter av rammeverket.
PHP laster og analyserer alle disse konfigurasjonsfilene for hver nye nettforespørsel. Dette er unødvendig hvis konfigurasjonen ikke har endret seg. Vi kan unngå dette med følgende kommando:
php artisan config:cache
Denne kommandoen kombinerer alle konfigurasjonsfilene til én enkelt hurtigbufferfil. Laravel leser denne enkeltfilen ved hver forespørsel.
Konfigurasjonsbufring er en delikat operasjon. Etter å ha kjørt denne kommandoen vil `env()`-funksjonen returnere null overalt utenfor konfigurasjonsfilene.
Dette er fordi med konfigurasjonsbufring forteller du rammeverket at miljøet er statisk. `.env`-filen er ment å definere miljøet.
Følg disse reglene ved konfigurasjonsbufring:
- Bruk det kun på produksjonssystemer.
- Bruk det kun hvis du er sikker på at du vil fryse konfigurasjonen.
- Hvis noe går galt, tilbakestill med `php artisan cache:clear`.
- Håp at skaden på virksomheten ikke er stor!
Reduser automatisk lastede tjenester
Laravel laster mange tjenester når applikasjonen startes. Disse tjenestene er definert i `config/app.php` under `providers`-arrayen. Det kan være mange tjenester, som vist i et eksempel nedenfor:
/* |-------------------------------------------------------------------------- | Autoloaded Service Providers |-------------------------------------------------------------------------- | | The service providers listed here will be automatically loaded on the | request to your application. Feel free to add your own services to | this array to grant expanded functionality to your applications. | */ 'providers' => [ /* * Laravel Framework Service Providers... */ Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, Illuminate\Cache\CacheServiceProvider::class, Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, Illuminate\Cookie\CookieServiceProvider::class, Illuminate\Database\DatabaseServiceProvider::class, Illuminate\Encryption\EncryptionServiceProvider::class, Illuminate\Filesystem\FilesystemServiceProvider::class, Illuminate\Foundation\Providers\FoundationServiceProvider::class, Illuminate\Hashing\HashServiceProvider::class, Illuminate\Mail\MailServiceProvider::class, Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, /* * Package Service Providers... */ /* * Application Service Providers... */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, ],
I dette eksemplet er det 27 tjenester. Du trenger kanskje ikke alle disse tjenestene. Hvis du for eksempel bygger et REST API, trenger du kanskje ikke sesjons- eller visningstjenester. Du kan også deaktivere autentiseringstjenester, pagineringstjenester og oversettelsestjenester.
Vær forsiktig når du deaktiverer tjenester. Test applikasjonen grundig før du går til produksjon.
Vær forsiktig med middleware
Når du trenger tilpasset behandling av innkommende nettforespørsler, kan du lage en middleware. Du kan legge til middleware i `app/Http/Kernel.php` i web- eller api-stackene. Dermed vil middleware være tilgjengelig for hele applikasjonen.
Etterhvert som applikasjonen vokser, kan dette bli en byrde hvis alle middlewarene kjøres i hver eneste forespørsel, selv om det ikke er noen forretningsmessig grunn til det.
Vær forsiktig med hvor du legger til middleware. Selv om det kan være praktisk å legge til en global middleware, kan ytelseskonsekvensene være store. Vær selektiv med å bruke middleware.
Unngå ORM (i noen tilfeller)
Selv om Eloquent forenkler interaksjon med databasen, går det på bekostning av ytelsen. ORM henter ikke bare data fra databasen, men instansierer også modellobjekter og fyller dem med kolonnedata.
Hvis du gjør en enkel `$users = User::all()` og det finnes 10 000 brukere, vil rammeverket hente 10 000 rader og instansiere 10 000 nye `User()` objekter. Dette er en stor mengde arbeid som utføres i bakgrunnen. Hvis databasen er et flaskehals i applikasjonen din, bør du vurdere å omgå ORM i noen tilfeller.
Dette gjelder spesielt for komplekse SQL-spørringer. I slike tilfeller kan det være mer effektivt å bruke `DB::raw()` og skrive spørringen manuelt.
En studie viser at Eloquent er mye tregere for enkle oppslag når antall oppføringer øker:
Bruk hurtigbuffer så mye som mulig
Hurtigbuffer er en av de viktigste aspektene ved optimalisering av nettapplikasjoner.
Hurtigbuffer betyr å forhåndsberegne og lagre dyre resultater (krevende med hensyn til CPU og minnebruk), og returnere dem direkte når den samme forespørselen gjøres igjen.
For eksempel i en e-handelsbutikk er det kanskje ofte slik at kundene er interessert i de produktene som nylig har kommet på lager, innenfor en viss prisklasse og for en bestemt aldersgruppe. Istedenfor å spørre databasen hver gang, kan vi lagre disse resultatene et sted vi lett kan få tilgang til.
Laravel har innebygd støtte for flere typer hurtigbuffer. I tillegg til å bruke en hurtigbuffer-driver, kan du også bruke modell-hurtigbuffer og søk-hurtigbuffer.
Vær oppmerksom på at forhåndsbygde hurtigbuffer-pakker kan forårsake flere problemer enn de løser hvis bruken er for kompleks.
Velg hurtigbuffer i minnet
Når du cacher i Laravel, har du flere alternativer for hvor du skal lagre beregningsresultatene. Disse alternativene er kjent som cache-drivere. Selv om det er mulig å bruke filsystemet for å lagre cache-resultater, er dette ikke optimalt.
Ideelt sett bør du bruke en cache i minnet (som ligger i RAM), som Redis, Memcached eller MongoDB. Slik vil hurtigbuffer fungere optimalt selv ved høy belastning.
En SSD er ikke like rask som RAM. Målinger viser at RAM er 10-20 ganger raskere enn SSD.
Redis er et foretrukket system for hurtigbuffer. Det er utrolig raskt (100 000 leseoperasjoner per sekund er vanlig). For store hurtigbuffersystemer kan det enkelt utvikles til en klynge.
Cache rutene
Akkurat som applikasjonskonfigurasjonen, endres ikke rutene mye over tid. Dette gjør dem til gode kandidater for hurtigbuffer. Spesielt hvis du har mange ruter fordelt på flere filer. Med en enkelt Laravel-kommando pakkes alle tilgjengelige ruter og gjøres tilgjengelig for rask tilgang:
php artisan route:cache
Når du legger til eller endrer ruter, bruker du:
php artisan route:clear
Bildeoptimalisering og CDN
Bilder er viktige for de fleste nettapplikasjoner. De er også store forbrukere av båndbredde og en av de største årsakene til at applikasjoner er trege. Det er viktig å optimalisere bildene.
Det anbefales å ikke lagre bilder lokalt på serveren. Det medfører risiko for tap av data, og dataoverføringen kan være treg avhengig av kundens geografiske plassering.
En løsning som Cloudinary kan endre størrelse og optimalisere bilder automatisk. Hvis det ikke er mulig, bruk en tjeneste som Cloudflare for å cache og vise bilder. Du kan også konfigurere webserveren til å komprimere data og lagre dem i nettleserens hurtigbuffer.
Her er et utdrag av en Nginx-konfigurasjon:
server { # file truncated # gzip compression settings gzip on; gzip_comp_level 5; gzip_min_length 256; gzip_proxied any; gzip_vary on; # browser cache control location ~* .(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ { expires 1d; access_log off; add_header Pragma public; add_header Cache-Control "public, max-age=86400"; } }
Selv om bildeoptimalisering ikke er direkte relatert til Laravel, er det et enkelt og kraftig triks som ofte blir oversett.
Autoloader optimering
Autoloading er en funksjon i PHP som lar klasser automatisk lastes inn når de trengs. Men prosessen med å finne og laste relevante klasser tar tid. Dette kan optimaliseres i produksjonsmiljøer med følgende kommando:
composer install --optimize-autoloader --no-dev
Bruk køer
Køer er en metode for å behandle oppgaver når det er mange av dem, og hver tar noen få millisekunder å fullføre. Et godt eksempel er å sende e-poster.
Når en bruker utfører en handling, kan det hende du sender noen e-poster. Hvis e-postgatewayen din svarer på en SMTP-forespørsel i løpet av 500 ms, vil det ta 3-4 sekunder for at ordrebekreftelsen skal starte. Dette er en dårlig brukeropplevelse.
Løsningen er å lagre oppgaver etter hvert som de kommer, fortelle brukeren at alt gikk bra, og behandle oppgavene senere. Hvis det oppstår en feil, kan oppgavene i køen prøves igjen før de erklæres som mislykkede.
Et køsystem kan komplisere oppsettet litt, men det er viktig i moderne nettapplikasjoner.
Ressursoptimalisering (Laravel Mix)
For alle grensesnittelementer i Laravel-applikasjonen, sørg for å bruke en pipeline som kompilerer og minimerer alle ressursfilene. Laravel Mix er et godt valg. Mix er en lett wrapper rundt Webpack som tar seg av CSS, SASS, JS, osv. En typisk `.mix.js`-fil kan se slik ut:
const mix = require('laravel-mix'); mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');
Dette tar seg automatisk av import, minifisering, optimalisering og kompilering når du er klar for produksjon. Mix håndterer tradisjonelle JS- og CSS-filer i tillegg til Vue- og React-komponenter.
Mer informasjon finner du her.
Konklusjon
Ytelsesoptimalisering er en blanding av kunst og vitenskap. Det er viktig å vite hvordan og hvor mye du skal optimalisere. Det er ingen grenser for hva du kan optimalisere i en Laravel-applikasjon.
Optimalisering bør gjøres når det er en god grunn, ikke bare fordi du er paranoid om ytelsen. Hvis du ikke er sikker på om du trenger å optimalisere appen din, la det være. En app som fungerer som den skal, er bedre enn en app som er optimalisert til perfeksjon, men som ikke er stabil.
For å bli en Laravel-mester, kan du sjekke ut dette nettkurset.
Måtte applikasjonene dine kjøre raskere!