Förbättra prestandan i din Laravel-applikation
Laravel är en mångsidig plattform, men snabbhet är inte alltid dess starkaste sida. Låt oss utforska olika strategier för att optimera prestandan och få din Laravel-applikation att flyga!
I dagens läge är det svårt för PHP-utvecklare att undgå Laravel. Oavsett om det är en junior eller mellannivåutvecklare som uppskattar den snabba utvecklingen som Laravel möjliggör, eller en senior utvecklare som tvingas lära sig Laravel på grund av marknadskraven, har Laravel blivit en central del av PHP-världen.
Det är obestridligt att Laravel har gett nytt liv till PHP-ekosystemet. Personligen hade jag nog lämnat PHP-världen för länge sedan om det inte vore för Laravel.
Laravel har all anledning att vara stolt över sig självt, eftersom det lägger stor vikt vid att underlätta utvecklingsprocessen. Men den här ansträngningen att göra det enkelt för utvecklare medför en hel del arbete i bakgrunden. Alla ”magiska” funktioner i Laravel som bara tycks fungera smidigt, är i själva verket beroende av flera lager av kod som aktiveras varje gång en funktion körs. Till och med en enkel felhantering kan avslöja hur djupt kodstrukturen går (med startpunkten för felet som spåras hela vägen till kärnan):
Ett kompileringsfel i en vy kan leda till en spårning av 18 funktionsanrop. I mina projekt har jag stött på upp till 40 anrop, och det kan bli ännu fler om du använder externa bibliotek och plugins.
Detta lager av kod gör att Laravel som standard kan kännas lite långsam.
Hur snabbt är egentligen Laravel?
Det är faktiskt svårt att ge ett enkelt svar på den frågan av flera anledningar.
För det första saknas en allmänt accepterad standard för att mäta prestanda hos webbapplikationer. Snabbare eller långsammare jämfört med vad och under vilka förutsättningar?
För det andra är webbapplikationer beroende av många faktorer som databaser, filsystem, nätverk och cache. Det är därför oklokt att tala om hastighet utan att ta hänsyn till dessa faktorer. En snabb webbapplikation med en trög databas kommer ändå att upplevas som långsam.
Trots detta är riktmärken fortfarande populära, även om de kanske inte alltid är tillförlitliga (se denna och denna). De ger ändå en referenspunkt som kan hjälpa oss att undvika fullständig förvirring. Låt oss därför med viss försiktighet ta en titt på ett ungefärligt mått på hur PHP-ramverk presterar.
Baserat på denna ganska respekterade GitHub-källa, ser jämförelsen mellan PHP-ramverk ut så här:
Om du inte tittar noga kanske du missar att se Laravel. Ja, Laravel ligger i botten på den här listan. Även om flera av dessa ”ramverk” inte är särskilt praktiska, ger detta oss en indikation på hur Laravel presterar jämfört med andra mer populära alternativ.
Vanligtvis märks denna ”långsamhet” inte i vanliga applikationer eftersom våra vardagliga webbappar sällan utsätts för extrem trafik. Men när de väl gör det (låt säga med 200-500 samtidiga användare) kan servrarna börja sacka och till och med krascha. Då hjälper det inte att lägga till mer hårdvara, och kostnaderna för infrastrukturen skenar.
Men misströsta inte! Den här artikeln fokuserar på vad du faktiskt kan göra för att förbättra prestandan.
Den goda nyheten är att det finns många sätt att påskynda din Laravel-applikation, ibland rejält. Det är faktiskt möjligt att få en och samma kodbas att prestera betydligt bättre och spara hundratals dollar i infrastruktur- och hostingkostnader. Hur då? Låt oss utforska det.
Fyra nivåer av optimering
Enligt min mening kan optimering delas in i fyra olika nivåer, i synnerhet när det gäller PHP-applikationer:
- Språknivå: Använda en snabbare version av PHP samt undvika vissa kodningsmönster som saktar ner prestandan.
- Ramverksnivå: De optimeringar som vi kommer att diskutera i den här artikeln.
- Infrastrukturnivå: Finjustera din PHP-processhanterare, webbserver, databas och så vidare.
- Hårdvarunivå: Uppgradera till en snabbare och mer kraftfull serverleverantör.
Alla dessa optimeringsnivåer spelar en viktig roll. Till exempel är PHP-fpm-optimering både kritisk och effektiv. Men den här artikeln kommer främst att fokusera på optimeringar som rör själva ramverket, alltså typ 2-optimeringar.
Jag vill också nämna att numreringen inte har någon specifik logik och att det inte är en etablerad standard. Det är bara jag som har hittat på dem. Så citera mig inte och säg ”Vi behöver typ-3-optimering på vår server” – annars kommer din teamledare förmodligen att bli arg och sedan skylla på mig. 😀
Och nu är vi äntligen framme vid de konkreta tipsen.
Var medveten om N+1-frågor
N+1-frågor är ett vanligt problem när ORM (Object-Relational Mapping) används. Laravel har en kraftfull ORM som heter Eloquent. Det är så användarvänligt att vi ofta glömmer att titta på vad som händer under huven.
Låt oss ta ett typiskt exempel: att visa alla beställningar som gjorts av en viss lista av kunder. Detta är ett vanligt scenario i e-handelssystem och andra rapporteringsgränssnitt där vi behöver visa information som är relaterad till olika enheter.
I Laravel kan vi föreställa oss en controller-funktion som hanterar detta på följande sätt:
class OrdersController extends Controller
{
// ...
public function getAllByCustomers(Request $request, array $ids) {
$customers = Customer::findMany($ids);
$orders = collect(); // new collection
foreach ($customers as $customer) {
$orders = $orders->merge($customer->orders);
}
return view('admin.reports.orders', ['orders' => $orders]);
}
}
Elegant och användarvänligt! 🤩🤩
Tyvärr är detta ett problematiskt sätt att skriva kod i Laravel.
Här är anledningen:
När vi ber ORM att leta efter de angivna kunderna genereras en SQL-fråga som ser ut så här:
SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);
Vilket är som förväntat. Alla rader som returneras lagras sedan i kollektionen $customers i controllern.
Därefter itererar vi genom varje kund för att hämta deras beställningar. Detta genererar följande fråga …
SELECT * FROM orders WHERE customer_id = 22;
… lika många gånger som det finns kunder.
Med andra ord, om vi behöver hämta beställningsdata för 1000 kunder kommer det totala antalet databasfrågor som körs att vara 1 (för att hämta kunderna) + 1000 (för att hämta varje kunds beställningar) = 1001. Det är härifrån som namnet n+1 kommer.
Kan vi göra det bättre? Självklart! Genom att använda ”eager loading” kan vi tvinga ORM att utföra en JOIN och returnera all nödvändig data i en enda fråga! Så här:
$orders = Customer::findMany($ids)->with('orders')->get();
Den resulterande datastrukturen är hierarkisk, men orderdatan kan lätt extraheras. Den enskilda frågan som genereras är något i stil med detta:
SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);
En enda fråga är alltid mer effektiv än tusentals separata frågor. Tänk dig vad som skulle hända med 10 000 kunder! Eller ännu värre, om vi skulle vilja visa varorna som ingår i varje beställning. Kom ihåg att metoden heter ”eager loading”, och det är nästan alltid en bra idé.
Cachelagra konfigurationen
En av anledningarna till Laravels flexibilitet är de många konfigurationsfiler som är integrerade i ramverket. Vill du ändra var bilderna lagras?
Ändra bara filen config/filesystems.php. Behöver du arbeta med flera köhanterare? Definiera dem i config/queue.php. Det finns totalt 13 konfigurationsfiler för olika aspekter av ramverket, vilket ger dig möjlighet att anpassa det efter dina behov.
Med tanke på PHPs natur kommer Laravel, vid varje ny webbförfrågan, att starta upp och tolka alla dessa konfigurationsfiler för att anpassa sig. Detta är onödigt om konfigurationen inte har ändrats på flera dagar! Att rekonstruera konfigurationen för varje begäran är en ineffektivitet som bör undvikas. Lösningen är ett enkelt kommando som Laravel erbjuder:
php artisan config:cache
Detta kommando kombinerar alla konfigurationsfiler till en enda cache som kan hämtas snabbt. Nästa gång en webbförfrågan anländer läser Laravel bara den här enda filen och fortsätter.
Det är viktigt att vara medveten om att konfigurationscache kan vara en känslig operation. Det största problemet är att när du har utfärdat kommandot returnerar funktionen env()
null för alla anrop utom i själva konfigurationsfilerna!
Detta är egentligen ganska logiskt. Om du använder konfigurationscache talar du om för ramverket att du är säker på att konfigurationen är korrekt och att du inte vill ändra den. Med andra ord förväntar du dig att miljön är statisk, vilket är det som .env-filer är avsedda för.
Därför bör du ha följande regler i åtanke:
- Använd bara cachelagring av konfiguration i en produktionsmiljö.
- Gör det bara om du är helt säker på att du vill frysa konfigurationen.
- Om något går fel, ångra inställningen med
php artisan cache:clear
. - Hoppas att skadan på verksamheten inte var för stor!
Minska antalet automatiskt laddade tjänster
För att underlätta laddar Laravel ett antal tjänster när applikationen startar. Dessa tjänster är listade i config/app.php under arraynyckeln ’providers’. Låt oss ta en titt på hur det ser ut i mitt fall:
/*
|--------------------------------------------------------------------------
| 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,
],
Jag räknade till 27 tjänster. Du kanske behöver allihopa, men det är osannolikt.
I mitt fall bygger jag till exempel ett REST API, vilket innebär att jag inte behöver Session Service Provider, View Service Provider osv. Jag har också anpassat vissa delar och använder inte ramverkets standardinställningar, så jag kan även inaktivera Auth Service Provider, Pagination Service Provider, Translation Service Provider och så vidare. Totalt sett är nästan hälften av dessa onödiga för mina behov.
Ta en noggrann titt på din applikation. Behöver den verkligen alla dessa tjänsteleverantörer? Men för guds skull, ta inte bara bort dem och tryck till produktion! Kör alla tester, kontrollera funktionaliteten manuellt på utvecklings- och testservrar och var extremt försiktig innan du rullar ut ändringarna. 🙂
Var försiktig med Middleware-stackar
När du behöver anpassa bearbetningen av webbförfrågningar är lösningen att skapa ny middleware. Det kan vara lockande att lägga till middleware i webb- eller api-stacken i app/Http/Kernel.php så att den är tillgänglig i hela applikationen, särskilt om den inte gör något uppseendeväckande (som loggning eller avisering).
Men när applikationen växer kan dessa globala middleware bli en börda om de appliceras på varje begäran, även om det inte finns något affärsmässigt behov av det.
Var medveten om var du lägger till ny middleware. Det kan vara enkelt att lägga till middleware globalt, men konsekvenserna för prestanda kan vara stora. Det kan kännas jobbigt att selektivt applicera middleware vid varje ändring, men det är en insats jag rekommenderar!
Undvik ORM (ibland)
Även om Eloquent underlättar interaktionen med databasen, har det en inverkan på hastigheten. ORM måste inte bara hämta poster från databasen utan också instansiera modellobjekt och fylla dem med kolumndata.
Om du utför en enkel $users = User::all()
och det finns 10 000 användare, kommer ramverket att hämta 10 000 rader från databasen och skapa 10 000 nya User() objekt och fylla deras egenskaper med relevanta data. Det är ett omfattande arbete som utförs i bakgrunden. Om databasen är en flaskhals kan det vara en bra idé att ibland kringgå ORM.
Detta gäller särskilt för komplexa SQL-frågor där du måste använda många olika funktioner för att få en effektiv fråga. I sådana fall kan det vara bättre att använda DB::raw()
och skriva frågan manuellt.
Enligt denna prestandastudie, är Eloquent även för enkla insättningar mycket långsammare när antalet poster ökar:
Använd cachelagring så mycket som möjligt
Cachelagring är en av de mest effektiva sätten att optimera webbapplikationer.
Cachelagring innebär att du förberäknar och lagrar komplexa resultat (komplexa vad gäller CPU- och minnesanvändning) och returnerar dem när samma begäran upprepas.
I en e-handelsbutik är det till exempel vanligt att kunderna oftast är intresserade av nya produkter inom ett visst prisintervall och en viss åldersgrupp. Att söka i databasen för denna information är onödigt. Eftersom frågan sällan ändras, är det bättre att lagra dessa resultat på en plats där de kan hämtas snabbt.
Laravel har inbyggt stöd för flera typer av cachelagring. Förutom att använda en cache-drivrutin och bygga ett cachelagringssystem från grunden kan du även använda Laravel-paket som underlättar modellcache, frågecache och så vidare.
Men var medveten om att färdiga cachepaket ibland kan skapa fler problem än de löser.
Prioritera cachelagring i minnet
När du cachelagrar data i Laravel har du flera alternativ för var du ska spara den cachelagrade informationen. Dessa alternativ kallas cache-drivrutiner. Även om det är möjligt att använda filsystemet för att lagra cacheresultat, är det inte optimalt för syftet med cachelagring.
Idealiskt sett bör du använda en minnesbaserad lösning som Redis, Memcached eller MongoDB så att cachelagringen kan bidra till att öka prestandan under högre belastning snarare än att själv bli en flaskhals.
Du kanske tror att en SSD-disk är lika snabb som RAM-minne, men så är inte fallet. Riktmärken visar att RAM är 10-20 gånger snabbare än SSD-diskar.
Mitt favoritsystem för cachelagring är Redis. Det är extremt snabbt (100 000 läsningar per sekund är vanligt) och kan enkelt skalas till kluster för stora cachesystem.
Cachelagra dina rutter
Precis som applikationskonfigurationer ändras routrarna sällan, vilket gör dem till en bra kandidat för cachelagring. Detta gäller särskilt om du inte gillar stora filer och delar upp din web.php och api.php i flera filer. Ett enda Laravel-kommando paketerar alla tillgängliga rutter och håller dem redo för snabb åtkomst:
php artisan route:cache
Och när du lägger till eller ändrar rutter kan du enkelt använda:
php artisan route:clear
Bildoptimering och CDN
Bilder är viktiga för de flesta webbapplikationer. De är också den största orsaken till ökad bandbreddsanvändning och långsamma appar/webbplatser. Om du bara lagrar uppladdade bilder direkt på servern och returnerar dem i HTTP-svaret missar du en stor möjlighet till optimering.
Jag rekommenderar att du inte lagrar bilder lokalt – det finns risk för dataförlust. Beroende på din kunds geografiska plats kan dataöverföringen även bli långsam.
Använd istället en tjänst som Cloudinary som automatiskt anpassar och optimerar bilder.
Om det inte är möjligt, använd något som Cloudflare för att cachelagra och visa bilder medan de lagras på din server.
Om inte ens det är möjligt kan du göra justeringar i din webbserverprogramvara för att komprimera resurser och instruera webbläsare att cachelagra information. Här är ett exempel på hur en Nginx-konfiguration kan se ut:
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";
}
}
Jag är medveten om att bildoptimering inte direkt rör Laravel, men det är en enkel och effektiv metod som ofta förbises, så jag kände att det var viktigt att nämna.
Autoloader-optimering
Autoladdning är en användbar funktion i PHP som hjälpt till att rädda språket. Processen att hitta och ladda en relevant klass via ett namnrymd kan ta tid och kan optimeras i produktionsinstallationer där hög prestanda är önskvärt. Laravel har en enkel lösning för detta:
composer install --optimize-autoloader --no-dev
Använd köer
Köer används för att hantera processer där det finns många uppgifter som var och en tar lite tid att slutföra. Ett bra exempel är att skicka e-postmeddelanden. I webbapplikationer är det vanligt att skicka ett antal e-postmeddelanden när en användare utför en åtgärd.
Till exempel, när du lanserar en ny produkt, vill du kanske meddela ledningsgruppen (cirka 6-7 e-postadresser) när någon lägger en beställning som överstiger ett visst belopp. Om din e-postgateway tar 500 ms att svara på din SMTP-förfrågan, betyder det att användaren får vänta 3-4 sekunder innan beställningen bekräftas, vilket ger en dålig användarupplevelse.
Lösningen är att lagra uppgifter när de kommer in, informera användaren om att allt är i sin ordning och bearbeta uppgifterna senare. Om ett fel uppstår kan de köade uppgifterna försöka igen ett par gånger innan de markeras som misslyckade.
Kredit: Microsoft.com
Även om kösystem komplicerar installationen något (och medför extra övervakningskostnader) är det oumbärligt i en modern webbapplikation.
Tillgångsoptimering (Laravel Mix)
För alla front-end-resurser i din Laravel-applikation bör du se till att du har en pipeline som kompilerar och minimerar alla resursfiler. De som är bekanta med verktyg som Webpack, Gulp eller Parcel behöver inte bekymra sig, men om du inte redan gör detta är Laravel Mix en bra rekommendation.
Mix är ett lättviktigt och användarvänligt verktyg som bygger på Webpack och hanterar dina CSS-, SASS- och JS-filer inför produktionslansering. En typisk .mix.js-fil kan vara så här kort och fortfarande göra underverk:
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
Mix tar automatiskt hand om import, minimering, optimering när du kör npm run production
. Mix hanterar även Vue- och React-komponenter.
Läs mer här!
Slutsats
Prestandaoptimering är en kombination av konst och vetenskap. Det handlar mer om hur och hur mycket du ska optimera än om vad du ska optimera. Det finns alltid utrymme för förbättringar i en Laravel-applikation.
Men det viktigaste rådet jag kan ge dig är att optimera när det finns ett tydligt behov och inte bara för att det låter bra eller för att du är rädd för att applikationen ska prestera dåligt med 100 000 användare, när du i verkligheten bara har 10.
Om du inte är säker på om du behöver optimera din applikation bör du inte leta efter problem. En fungerande applikation som gör det den ska, är bättre än en applikation som är optimerad till det yttersta men som kraschar ibland.
Om du vill bli en mästare på Laravel, kolla in den här onlinekursen.
Må dina applikationer köras mycket, mycket snabbare! 🙂