Hur man optimerar PHP Laravel Web Application för hög prestanda?

Laravel är många saker. Men snabbt är inte en av dem. Låt oss lära oss några knep för att få det att gå snabbare!

Ingen PHP-utvecklare är oberörd av Laravel dessa dagar. De är antingen en junior- eller mellannivåutvecklare som älskar den snabba utvecklingen Laravel erbjuder, eller så är de en senior utvecklare som tvingas lära sig Laravel på grund av marknadstryck.

Hur som helst, det går inte att förneka att Laravel har återupplivat PHP-ekosystemet (jag skulle definitivt ha lämnat PHP-världen för länge sedan om Laravel inte var där).

Ett stycke (något berättigat) självberöm från Laravel

Men eftersom Laravel böjer sig bakåt för att göra saker enkelt för dig, betyder det att den undertill gör massor av arbete för att se till att du har ett bekvämt liv som utvecklare. Alla de ”magiska” funktionerna i Laravel som bara verkar fungera har lager på lager av kod som måste piskas upp varje gång en funktion körs. Även ett enkelt undantag spårar hur djupt kaninhålet är (märk var felet börjar, ända ner till huvudkärnan):

För vad som verkar vara ett kompileringsfel i en av vyerna finns det 18 funktionsanrop att spåra. Jag har personligen stött på 40, och det kan lätt bli fler om du använder andra bibliotek och plugins.

Poängen är att som standard gör detta lager på lager av kod Laravel långsam.

Hur långsam är Laravel?

Ärligt talat, det är helt enkelt omöjligt att svara på denna fråga av flera skäl.

För det första finns det ingen accepterad, objektiv, vettig standard för att mäta hastigheten på webbappar. Snabbare eller långsammare jämfört med vad? Under vilka förutsättningar?

För det andra beror en webbapp på så många saker (databas, filsystem, nätverk, cache, etc.) att det är helt enkelt dumt att prata om hastighet. En mycket snabb webbapp med en mycket långsam databas är en mycket långsam webbapp. 🙂

Men denna osäkerhet är just därför riktmärken är populära. Även om de inte betyder något (se detta och detta), ger de en referensram och hjälper oss att inte bli galna. Därför, med flera nypor salt redo, låt oss få en felaktig, grov uppfattning om hastigheten bland PHP-ramverk.

Går efter denna ganska respektabla GitHub källaså här ser PHP-ramverken upp i jämförelse:

Du kanske inte ens märker Laravel här (även om du kisar riktigt hårt) om du inte kastar ditt fall ända till slutet av svansen. Ja, kära vänner, Laravel kommer sist! Visst, de flesta av dessa ”ramverk” är inte särskilt praktiska eller ens användbara, men det berättar för oss hur trög Laravel är jämfört med andra mer populära.

Normalt förekommer inte denna ”långsamhet” i applikationer eftersom våra vardagliga webbappar sällan når höga siffror. Men när de väl gör det (säg uppemot 200-500 samtidighet) börjar servrarna att kvävas och dö. Det är den tiden då det inte ens minskar att kasta mer hårdvara på problemet, och infrastrukturräkningarna klättrar så snabbt att dina höga ideal för molnbaserad datoranvändning kraschar.

Men hej, pigga upp! Den här artikeln handlar inte om vad som inte kan göras, utan om vad som kan göras. 🙂

Goda nyheter är att du kan göra mycket för att få din Laravel-app att gå snabbare. Flera gånger snabbt. Ja, skojar inte. Du kan få samma kodbas att bli ballistisk och spara flera hundra dollar på infrastruktur-/hostingräkningar varje månad. Hur? Låt oss komma till det.

Fyra typer av optimeringar

Enligt min åsikt kan optimering göras på fyra distinkta nivåer (när det kommer till PHP-applikationer, det vill säga):

  • Språknivå: Detta innebär att du använder en snabbare version av språket och undviker specifika funktioner/stilar av kodning på språket som gör din kod långsam.
  • Ramnivå: Det här är de saker vi kommer att ta upp i den här artikeln.
  • Infrastrukturnivå: Justera din PHP-processhanterare, webbserver, databas, etc.
  • Hårdvarunivå: Flytta till en bättre, snabbare och kraftfullare hårdvaruvärdleverantör.

Alla dessa typer av optimeringar har sin plats (till exempel är PHP-fpm-optimering ganska kritisk och kraftfull). Men fokus i den här artikeln kommer att vara optimeringar rent av typ 2: de som är relaterade till ramverket.

Förresten, det finns ingen logik bakom numreringen, och det är inte en accepterad standard. Jag har precis hittat på dessa. Citera mig aldrig och säg, ”Vi behöver typ-3-optimering på vår server”, annars kommer din teamledare att döda dig, hitta mig och sedan döda mig också. 😀

Och nu kommer vi äntligen till det förlovade landet.

Var medveten om n+1 databasfrågor

n+1 frågeproblemet är vanligt när ORM används. Laravel har sin kraftfulla ORM som heter Eloquent, som är så vacker, så bekväm att vi ofta glömmer att titta på vad som händer.

Tänk på ett mycket vanligt scenario: att visa listan över alla beställningar som gjorts av en given lista över kunder. Detta är ganska vanligt i e-handelssystem och alla rapporteringsgränssnitt i allmänhet där vi behöver visa alla enheter relaterade till vissa enheter.

I Laravel kan vi föreställa oss en kontrollerfunktion som gör jobbet så här:

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]);
    }
}

Ljuv! Och ännu viktigare, elegant, vacker. 🤩🤩

Tyvärr är det ett katastrofalt sätt att skriva kod i Laravel.

Här är varför.

När vi ber ORM att leta efter de givna kunderna genereras en SQL-fråga som denna:

SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);

Vilket är precis som förväntat. Som ett resultat lagras alla returnerade rader i samlingen $kunder i kontrollfunktionen.

Nu går vi över varje kund en efter en och får deras beställningar. Detta exekverar 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 få orderdata för 1000 kunder kommer det totala antalet databasförfrågningar som körs att vara 1 (för att hämta alla kunders data) + 1000 (för att hämta orderdata för varje kund) = 1001. Detta det är där namnet n+1 kommer ifrån.

Kan vi göra bättre? Säkert! Genom att använda vad som kallas 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 en kapslad struktur, visst, men orderdata kan enkelt extraheras. Den resulterande enskilda frågan, i det här fallet, är ungefär så här:

SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);

En enda fråga är naturligtvis bättre än tusen extra frågor. Föreställ dig vad som skulle hända om det fanns 10 000 kunder att bearbeta! Eller gud förbjude om vi också ville visa varorna som finns i varje beställning! Kom ihåg att namnet på tekniken är ivrig att ladda, och det är nästan alltid en bra idé.

Cachelagra konfigurationen!

En av anledningarna till Laravels flexibilitet är de massor av konfigurationsfiler som är en del av ramverket. Vill du ändra hur/var bilderna lagras?

Tja, ändra bara filen config/filesystems.php (åtminstone när du skriver). Vill du arbeta med flera köförare? Beskriv dem gärna i config/queue.php. Jag har precis räknat och fann att det finns 13 konfigurationsfiler för olika aspekter av ramverket, vilket säkerställer att du inte kommer att bli besviken oavsett vad du vill ändra.

Med tanke på PHPs natur, varje gång en ny webbförfrågan kommer in, vaknar Laravel, startar upp allt och analyserar alla dessa konfigurationsfiler för att ta reda på hur man gör saker annorlunda den här gången. Förutom att det är dumt om inget har förändrats de senaste dagarna! Att bygga om konfigurationen på varje begäran är ett slöseri som kan (faktiskt måste) undvikas, och vägen ut är ett enkelt kommando som Laravel erbjuder:

php artisan config:cache

Vad detta gör är att kombinera alla tillgängliga konfigurationsfiler till en enda och cachen finns någonstans för snabb hämtning. Nästa gång det kommer en webbförfrågan läser Laravel helt enkelt den här enstaka filen och sätter igång.

Som sagt, konfigurationscache är en extremt känslig operation som kan blåsa upp i ditt ansikte. Det största problemet är att när du har utfärdat det här kommandot, anropar funktionen env() från överallt förutom konfigurationsfilerna att returnera null!

Det är vettigt när man tänker efter. Om du använder konfigurationscache säger du till ramverket: ”Vet du vad, jag tycker att jag har ställt in saker och ting bra och jag är 100% säker på att jag inte vill att de ska ändras.” Med andra ord, du förväntar dig att miljön ska förbli statisk, vilket är vad .env-filer är till för.

Med det sagt, här är några järnklädda, heliga, okrossbara regler för konfigurationscache:

  • Gör det bara på ett produktionssystem.
  • Gör det bara om du är riktigt, riktigt säker på att du vill frysa konfigurationen.
  • Om något går fel, ångra inställningen med php artisan cache:clear
  • Be att skadan på verksamheten inte var betydande!
  • Minska automatiskt laddade tjänster

    För att vara till hjälp laddar Laravel massor av tjänster när den vaknar. Dessa är tillgängliga i filen config/app.php som en del av arraynyckeln ’providers’. Låt oss ta en titt på vad jag har 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...
             */
            IlluminateAuthAuthServiceProvider::class,
            IlluminateBroadcastingBroadcastServiceProvider::class,
            IlluminateBusBusServiceProvider::class,
            IlluminateCacheCacheServiceProvider::class,
            IlluminateFoundationProvidersConsoleSupportServiceProvider::class,
            IlluminateCookieCookieServiceProvider::class,
            IlluminateDatabaseDatabaseServiceProvider::class,
            IlluminateEncryptionEncryptionServiceProvider::class,
            IlluminateFilesystemFilesystemServiceProvider::class,
            IlluminateFoundationProvidersFoundationServiceProvider::class,
            IlluminateHashingHashServiceProvider::class,
            IlluminateMailMailServiceProvider::class,
            IlluminateNotificationsNotificationServiceProvider::class,
            IlluminatePaginationPaginationServiceProvider::class,
            IlluminatePipelinePipelineServiceProvider::class,
            IlluminateQueueQueueServiceProvider::class,
            IlluminateRedisRedisServiceProvider::class,
            IlluminateAuthPasswordsPasswordResetServiceProvider::class,
            IlluminateSessionSessionServiceProvider::class,
            IlluminateTranslationTranslationServiceProvider::class,
            IlluminateValidationValidationServiceProvider::class,
            IlluminateViewViewServiceProvider::class,
    
            /*
             * Package Service Providers...
             */
    
            /*
             * Application Service Providers...
             */
            AppProvidersAppServiceProvider::class,
            AppProvidersAuthServiceProvider::class,
            // AppProvidersBroadcastServiceProvider::class,
            AppProvidersEventServiceProvider::class,
            AppProvidersRouteServiceProvider::class,
    
        ],

    Än en gång, jag räknade, och det finns 27 tjänster listade! Nu kan du behöva alla, men det är osannolikt.

    Till exempel råkar jag bygga ett REST API för tillfället, vilket betyder att jag inte behöver Session Service Provider, View Service Provider, etc. Och eftersom jag gör några saker på mitt sätt och inte följer ramverkets standardinställningar , Jag kan också inaktivera Auth Service Provider, Pagineringstjänsteleverantör, Översättningstjänstleverantör och så vidare. Sammantaget är nästan hälften av dessa onödiga för mitt användningsfall.

    Ta en lång och noggrann titt på din ansökan. Behöver den alla dessa tjänsteleverantörer? Men för guds skull, snälla kommentera inte blint ut dessa tjänster och skjut till produktion! Kör alla tester, kontrollera saker manuellt på dev- och iscensättningsmaskiner och var väldigt paranoid innan du trycker på avtryckaren. 🙂

    Var klok med middleware-stackar

    När du behöver lite anpassad bearbetning av den inkommande webbförfrågan är lösningen att skapa en ny mellanprogramvara. Nu är det frestande att öppna app/Http/Kernel.php och stoppa mellanvaran i webben eller api-stacken; på så sätt blir den tillgänglig i hela appen och om den inte gör något påträngande (som loggning eller avisering, till exempel).

    Men när appen växer kan denna samling av globala mellanprogram bli en tyst börda för appen om alla (eller majoriteten) av dessa finns i varje begäran, även om det inte finns något affärsmässigt skäl till det.

    Med andra ord, var försiktig med var du lägger till/applicerar en ny mellanprogramvara. Det kan vara bekvämare att lägga till något globalt, men prestationsstraffet är väldigt högt i längden. Jag vet vilken smärta du skulle behöva genomgå om du selektivt skulle använda middleware varje gång det sker en ny förändring, men det är en smärta som jag gärna skulle ta och rekommendera!

    Undvik ORM (ibland)

    Även om Eloquent gör många aspekter av DB-interaktion njutbara, kommer det på bekostnad av hastighet. Som en kartläggare måste ORM inte bara hämta poster från databasen utan också instansiera modellobjekten och hydratisera (fylla i) dem med kolumndata.

    Så om du gör en enkel $users = User::all() och det finns, säg, 10 000 användare, kommer ramverket att hämta 10 000 rader från databasen och internt göra 10 000 nya User() och fylla deras egenskaper med relevant data . Det här är enorma mängder arbete som görs bakom kulisserna, och om databasen är där du använder programmet håller på att bli en flaskhals, är det ibland en bra idé att kringgå ORM.

    Detta gäller särskilt för komplexa SQL-frågor, där du skulle behöva hoppa över många ramar och skriva stängningar efter stängningar och ändå få en effektiv fråga. I sådana fall är det att föredra att göra en DB::raw() och skriva frågan för hand.

    Åker via detta prestandastudie, även för enkla insatser. Eloquent är mycket långsammare när antalet poster ökar:

    Använd caching så mycket som möjligt

    En av de bäst bevarade hemligheterna för webbapplikationsoptimering är cachelagring.

    För den oinitierade innebär cachning förberäkning och lagring av dyra resultat (dyra i termer av CPU och minnesanvändning), och helt enkelt returnera dem när samma fråga upprepas.

    Till exempel, i en e-handelsbutik kan det stöta på att av de 2 miljoner produkterna, för det mesta är folk intresserade av de som är nylagda, inom en viss prisklass och för en viss åldersgrupp. Att söka i databasen efter denna information är slösaktigt – eftersom frågan inte ändras ofta är det bättre att lagra dessa resultat någonstans där vi snabbt kan komma åt.

    Laravel har inbyggt stöd för flera typer av cachelagring. Förutom att använda en cachningsdrivrutin och bygga cachingsystemet från grunden, kanske du vill använda några Laravel-paket som underlättar modellcache, query cachingetc.

    Men notera att utöver ett visst förenklat användningsfall kan förbyggda cachningspaket orsaka fler problem än de löser.

    Föredrar cachelagring i minnet

    När du cachelagrar något i Laravel har du flera alternativ för var du ska lagra den resulterande beräkningen som måste cachelagras. Dessa alternativ är också kända som cache-drivrutiner. Så även om det är möjligt och fullt rimligt att använda filsystemet för att lagra cacheresultat, är det inte riktigt vad caching är tänkt att vara.

    Helst vill du använda en in-memory (som lever i RAM-minnet helt) som Redis, Memcached, MongoDB, etc., så att under högre belastningar tjänar caching till en viktig användning snarare än att bli en flaskhals i sig.

    Nu kanske du tror att att ha en SSD-disk är nästan samma sak som att använda ett RAM-minne, men det är inte ens i närheten. Även informellt riktmärken visa att RAM överträffar SSD med 10-20 gånger när det kommer till hastighet.

    Mitt favoritsystem när det kommer till cachning är Redis. Dess löjligt snabbt (100 000 läsoperationer per sekund är vanliga) och kan för mycket stora cachesystem utvecklas till en klunga lätt.

    Cachelagra rutterna

    Precis som applikationskonfigurationen ändras inte rutterna mycket över tiden och är en idealisk kandidat för cachning. Detta gäller särskilt om du inte tål stora filer som jag och slutar med att dela upp din web.php och api.php över flera filer. Ett enda Laravel-kommando paketerar alla tillgängliga rutter och håller dem till hands för framtida åtkomst:

    php artisan route:cache

    Och när du lägger till eller ändrar rutter gör du helt enkelt:

    php artisan route:clear

    Bildoptimering och CDN

    Bilder är hjärtat och själen i de flesta webbapplikationer. Av en slump är de också de största konsumenterna av bandbredd och en av de största anledningarna till långsamma appar/webbplatser. Om du helt enkelt lagrar de uppladdade bilderna naivt på servern och skickar tillbaka dem i HTTP-svar, låter du en enorm optimeringsmöjlighet glida förbi.

    Min första rekommendation är att inte lagra bilder lokalt – det finns problemet med dataförlust att ta itu med, och beroende på vilken geografisk region din kund befinner sig i kan dataöverföringen gå smärtsamt långsamt.

    Gå istället för en lösning som Molnigt som automatiskt ändrar storlek och optimerar bilder i farten.

    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.

    Och om inte ens det är möjligt, gör det stor skillnad att justera din webbserverprogramvara lite för att komprimera tillgångar och styra besökarens webbläsare att cache saker. Så här skulle ett utdrag av Nginx-konfigurationen 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 har något med Laravel att göra, men det är ett så enkelt och kraftfullt knep (och försummas så ofta) som inte kunde hjälpa mig själv.

    Autoloader optimering

    Autoloading är en snygg, inte så gammal funktion i PHP som utan tvekan räddade språket från undergång. Som sagt, processen att hitta och ladda den relevanta klassen genom att dechiffrera en given namnområdessträng tar tid och kan undvikas i produktionsinstallationer där hög prestanda är önskvärt. Återigen har Laravel en enkommandolösning på detta:

    composer install --optimize-autoloader --no-dev

    Bli vänner med köer

    Köer är hur du bearbetar saker när det finns många av dem, och var och en av dem tar några millisekunder att slutföra. Ett bra exempel är att skicka e-postmeddelanden – ett utbrett användningsfall i webbappar är att skjuta av några e-postmeddelanden när en användare utför några åtgärder.

    Till exempel, i en nylanserad produkt kanske du vill att företagsledningen (ungefär 6-7 e-postadresser) ska meddelas när någon lägger en beställning över ett visst värde. Förutsatt att din e-postgateway kan svara på din SMTP-förfrågan på 500 ms, talar vi om en bra 3-4 sekunders väntan för användaren innan orderbekräftelsen kommer in. En riktigt dålig del av UX, jag är säker på att du kommer att hålla med.

    Lösningen är att lagra jobb när de kommer in, berätta för användaren att allt gick bra och bearbeta dem (några sekunder) senare. Om det finns ett fel kan de köade jobben försökas igen några gånger innan de förklaras ha misslyckats.

    Kredit: Microsoft.com

    Medan ett kösystem komplicerar installationen lite (och lägger till en del övervakningskostnader), är det oumbärligt i en modern webbapplikation.

    Tillgångsoptimering (Laravel Mix)

    För alla front-end-tillgångar i din Laravel-applikation, se till att det finns en pipeline som kompilerar och minimerar alla tillgångsfiler. De som är bekväma med ett buntsystem som Webpack, Gulp, Parcel, etc., behöver inte bry sig, men om du inte redan gör detta, Laravel Mix är en solid rekommendation.

    Mix är ett lättviktigt (och förtjusande, i ärlighetens namn!) omslag runt Webpack som tar hand om alla dina CSS, SASS, JS, etc.-filer för produktion. En typisk .mix.js-fil kan vara så liten som denna 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');

    Detta tar automatiskt hand om import, minifiering, optimering och hela grejen när du är redo för produktion och kör npm run-produktion. Mix tar inte bara hand om traditionella JS- och CSS-filer, utan även Vue- och React-komponenter som du kan ha i ditt applikationsarbetsflöde.

    Mer information här!

    Slutsats

    Prestandaoptimering är mer konst än vetenskap – det är viktigt att veta hur och hur mycket man ska göra än vad man ska göra. Som sagt, det finns ingen ände på hur mycket och vad du kan optimera i en Laravel-applikation.

    Men vad du än gör, jag skulle vilja ge dig några avskedsråd – optimering bör göras när det finns en solid anledning, och inte för att det låter bra eller för att du är paranoid om appens prestanda för 100 000+ användare medan det är i verkligheten det finns bara 10.

    Om du inte är säker på om du behöver optimera din app eller inte, behöver du inte sparka på de ökända bålgetingens bo. En fungerande app som känns tråkig men gör precis vad den måste är tio gånger mer åtråvärd än en app som har optimerats till en mutant hybrid supermaskin men faller platt då och då.

    Och för nybörjare att bli en Laravel-mästare, kolla in det här onlinekurs.

    Må dina appar köra mycket, mycket snabbare! 🙂