Hvordan optimerer man PHP Laravel-webapplikationen til høj ydeevne?

Laravel er mange ting. Men hurtig er ikke en af ​​dem. Lad os lære nogle tricks for at få det til at gå hurtigere!

Ingen PHP-udvikler er uberørt af Laravel disse dage. De er enten en junior- eller mellemniveauudvikler, der elsker den hurtige udvikling, Laravel tilbyder, eller de er en seniorudvikler, der bliver tvunget til at lære Laravel på grund af markedspres.

Uanset hvad, er der ingen tvivl om, at Laravel har revitaliseret PHP-økosystemet (jeg ville helt sikkert have forladt PHP-verdenen for længe siden, hvis Laravel ikke var der).

Et udsnit af (noget berettiget) selvros fra Laravel

Men da Laravel bøjer sig bagover for at gøre tingene nemmere for dig, betyder det, at den nedenunder laver tonsvis af arbejde for at sikre, at du har et behageligt liv som udvikler. Alle de “magiske” funktioner i Laravel, der bare ser ud til at virke, har lag på lag af kode, som skal piskes op, hver gang en funktion kører. Selv en simpel undtagelse sporer, hvor dybt kaninhullet er (bemærk, hvor fejlen starter, helt ned til hovedkernen):

For hvad der ser ud til at være en kompileringsfejl i en af ​​visningerne, er der 18 funktionskald at spore. Jeg er personligt stødt på 40, og der kunne sagtens være flere, hvis du bruger andre biblioteker og plugins.

Pointen er, at dette lag på lag af kode som standard gør Laravel langsom.

Hvor langsom er Laravel?

Helt ærligt, det er af flere grunde umuligt at besvare dette spørgsmål.

For det første er der ingen accepteret, objektiv, fornuftig standard til at måle hastigheden af ​​webapps. Hurtigere eller langsommere i forhold til hvad? Under hvilke forhold?

For det andet afhænger en webapp af så mange ting (database, filsystem, netværk, cache osv.), at det er almindeligt fjollet at tale om hastighed. En meget hurtig webapp med en meget langsom database er en meget langsom webapp. 🙂

Men denne usikkerhed er netop grunden til, at benchmarks er populære. Selvom de intet betyder (se dette og dette), giver de en referenceramme og hjælper os med at blive gale. Lad os derfor, med flere knivspids salt klar, få en forkert, groft ide om hastigheden blandt PHP-frameworks.

Går efter denne ret respektable GitHub kildeher er hvordan PHP-rammerne stiller sig op, når de sammenlignes:

Du lægger måske ikke engang mærke til Laravel her (selvom du skeler rigtig hårdt), medmindre du kaster din sag lige til enden af ​​halen. Ja, kære venner, Laravel kommer sidst! Indrømmet, de fleste af disse “rammer” er ikke særlig praktiske eller endda nyttige, men det fortæller os, hvor træg Laravel er sammenlignet med andre mere populære.

Normalt forekommer denne “langsomhed” ikke i applikationer, fordi vores hverdagswebapps sjældent rammer høje tal. Men når de gør det (f.eks. op mod 200-500 samtidighed), begynder serverne at kvæles og dø. Det er tidspunktet, hvor selv at kaste mere hardware efter problemet ikke reducerer det, og infrastrukturregninger stiger så hurtigt, at dine høje idealer om cloud computing styrter sammen.

Men hey, glæd jer! Denne artikel handler ikke om, hvad der ikke kan gøres, men om hvad der kan gøres. 🙂

Den gode nyhed er, at du kan gøre meget for at få din Laravel-app til at gå hurtigere. Flere gange hurtigt. Ja, ingen sjov. Du kan få den samme kodebase til at gå ballistisk og spare flere hundrede dollars på infrastruktur-/hostingregninger hver måned. Hvordan? Lad os komme til det.

Fire typer optimeringer

Efter min mening kan optimering udføres på fire forskellige niveauer (når det kommer til PHP-applikationer, det vil sige):

  • Sprogniveau: Det betyder, at du bruger en hurtigere version af sproget og undgår specifikke funktioner/stile af kodning på det sprog, der gør din kode langsom.
  • Rammeniveau: Dette er de ting, vi vil dække i denne artikel.
  • Infrastruktur-niveau: Tuning af din PHP-procesmanager, webserver, database osv.
  • Hardware-niveau: Flytning til en bedre, hurtigere og mere kraftfuld hardware-hostingudbyder.

Alle disse typer optimeringer har deres plads (for eksempel er PHP-fpm-optimering ret kritisk og kraftfuld). Men fokus i denne artikel vil være optimeringer udelukkende af type 2: dem, der er relateret til rammen.

I øvrigt er der ingen begrundelse bag nummereringen, og det er ikke en accepteret standard. Jeg har lige fundet på disse. Du må aldrig citere mig og sige: “Vi har brug for type-3-optimering på vores server,” ellers vil din teamleder dræbe dig, finde mig og så også dræbe mig. 😀

Og nu ankommer vi endelig til det forjættede land.

Vær opmærksom på n+1 databaseforespørgsler

n+1-forespørgselsproblemet er et almindeligt problem, når der bruges ORM’er. Laravel har sin kraftfulde ORM kaldet Eloquent, som er så smuk, så praktisk, at vi ofte glemmer at se på, hvad der foregår.

  Sleep Better fortæller dig, hvor godt du sover

Overvej et meget almindeligt scenarie: visning af listen over alle ordrer afgivet af en given liste over kunder. Dette er ret almindeligt i e-handelssystemer og alle rapporteringsgrænseflader generelt, hvor vi skal vise alle enheder relateret til nogle enheder.

I Laravel kan vi forestille os en controller-funktion, der udfører jobbet som dette:

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

Sød! Og endnu vigtigere, elegant, smuk. 🤩🤩

Desværre er det en katastrofal måde at skrive kode i Laravel på.

Her er hvorfor.

Når vi beder ORM om at lede efter de givne kunder, genereres en SQL-forespørgsel som denne:

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

Hvilket er præcis som forventet. Som et resultat bliver alle de returnerede rækker gemt i samlingen $kunder inde i controllerfunktionen.

Nu går vi over hver kunde en efter en og får deres ordrer. Dette udfører følgende forespørgsel. . .

SELECT * FROM orders WHERE customer_id = 22;

. . . lige så mange gange, som der er kunder.

Med andre ord, hvis vi skal have ordredata for 1000 kunder, vil det samlede antal udførte databaseforespørgsler være 1 (for at hente alle kundernes data) + 1000 (for at hente ordredata for hver kunde) = 1001. Dette er hvor navnet n+1 kommer fra.

Kan vi gøre det bedre? Sikkert! Ved at bruge det, der er kendt som ivrig indlæsning, kan vi tvinge ORM til at udføre en JOIN og returnere alle de nødvendige data i en enkelt forespørgsel! Sådan her:

$orders = Customer::findMany($ids)->with('orders')->get();

Den resulterende datastruktur er selvfølgelig en indlejret struktur, men ordredataene kan let udtrækkes. Den resulterende enkelte forespørgsel, i dette tilfælde, er noget som dette:

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

En enkelt forespørgsel er selvfølgelig bedre end tusind ekstra forespørgsler. Forestil dig, hvad der ville ske, hvis der var 10.000 kunder at behandle! Eller gud forbyde, hvis vi også ville vise de varer, der var indeholdt i hver ordre! Husk, navnet på teknikken er ivrig læsning, og det er næsten altid en god idé.

Cache konfigurationen!

En af grundene til Laravels fleksibilitet er de tonsvis af konfigurationsfiler, der er en del af rammen. Vil du ændre, hvordan/hvor billederne gemmes?

Nå, bare skift filen config/filesystems.php (i det mindste i skrivende stund). Vil du arbejde med flere kødrivere? Du er velkommen til at beskrive dem i config/queue.php. Jeg har lige talt og fundet ud af, at der er 13 konfigurationsfiler til forskellige aspekter af rammen, hvilket sikrer, at du ikke bliver skuffet, uanset hvad du vil ændre.

I betragtning af PHP’s natur vågner Laravel, hver gang der kommer en ny webforespørgsel, op, starter alt og analyserer alle disse konfigurationsfiler for at finde ud af, hvordan man gør tingene anderledes denne gang. Bortset fra at det er dumt hvis intet har ændret sig de sidste par dage! Genopbygning af konfigurationen på hver anmodning er spild, der kan (faktisk skal) undgås, og vejen ud er en simpel kommando, som Laravel tilbyder:

php artisan config:cache

Hvad dette gør er at kombinere alle de tilgængelige konfigurationsfiler til en enkelt, og cachen er et sted til hurtig hentning. Næste gang der er en webanmodning, vil Laravel simpelthen læse denne enkelte fil og komme i gang.

Når det er sagt, er konfigurationscache en ekstremt delikat operation, der kan sprænge i dit ansigt. Det største problem er, at når du har udstedt denne kommando, vil env()-funktionen kalder fra alle steder, undtagen konfigurationsfilerne, returnere null!

Det giver mening, når du tænker over det. Hvis du bruger konfigurationscache, fortæller du rammen: “Ved du hvad, jeg synes, jeg har sat tingene pænt op, og jeg er 100 % sikker på, at jeg ikke vil have dem til at ændre sig.” Med andre ord forventer du, at miljøet forbliver statisk, hvilket er hvad .env-filer er til for.

Med det sagt, her er nogle jernbeklædte, hellige, ubrydelige regler for konfigurationscache:

  • Gør det kun på et produktionssystem.
  • Gør det kun, hvis du er virkelig, virkelig sikker på, at du vil fryse konfigurationen.
  • Hvis noget går galt, skal du fortryde indstillingen med php artisan cache:clear
  • Bed til, at skaden på virksomheden ikke var væsentlig!
  • Reducer automatisk indlæste tjenester

    For at være hjælpsom indlæser Laravel et væld af tjenester, når den vågner. Disse er tilgængelige i config/app.php-filen som en del af ‘providers’-arraynøglen. Lad os se på, hvad jeg har i mit tilfælde:

    /*
        |--------------------------------------------------------------------------
        | 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,
    
        ],

    Endnu en gang talte jeg, og der er 27 tjenester på listen! Nu kan du få brug for dem alle, men det er usandsynligt.

      Sådan deaktiverer du AT&T-meddelelser backup og synkronisering

    For eksempel er jeg tilfældigvis ved at bygge en REST API i øjeblikket, hvilket betyder, at jeg ikke har brug for Session Service Provider, View Service Provider osv. Og da jeg gør et par ting på min måde og ikke følger rammestandarderne , Jeg kan også deaktivere Auth Service Provider, Pagineringsservice Provider, Oversættelses Service Provider og så videre. Alt i alt er næsten halvdelen af ​​disse unødvendige for min brugssag.

    Tag et langt og grundigt kig på din ansøgning. Har den brug for alle disse tjenesteudbydere? Men for guds skyld, vær venlig ikke blindt at kommentere disse tjenester og skubbe til produktion! Kør alle testene, tjek tingene manuelt på dev- og iscenesættelsesmaskiner, og vær meget paranoid, før du trykker på aftrækkeren. 🙂

    Vær klog med middleware stakke

    Når du har brug for en skræddersyet behandling af den indkommende webanmodning, er det at oprette en ny middleware svaret. Nu er det fristende at åbne app/Http/Kernel.php og sætte middlewaren i nettet eller api-stakken; på den måde bliver den tilgængelig på tværs af appen, og hvis den ikke gør noget påtrængende (som at logge eller underrette, for eksempel).

    Men efterhånden som appen vokser, kan denne samling af global middleware blive en tavs byrde for appen, hvis alle (eller størstedelen) af disse er til stede i hver anmodning, selvom der ikke er nogen forretningsmæssig grund til det.

    Med andre ord skal du være opmærksom på, hvor du tilføjer/anvender en ny middleware. Det kan være mere praktisk at tilføje noget globalt, men præstationsstraffen er meget høj i det lange løb. Jeg kender den smerte, du skulle gennemgå, hvis du selektivt skulle anvende middleware, hver gang der er en ny ændring, men det er en smerte, jeg gerne vil tage og anbefale!

    Undgå ORM (til tider)

    Selvom Eloquent gør mange aspekter af DB-interaktion behagelige, kommer det på bekostning af hastighed. Som kortlægger skal ORM ikke kun hente poster fra databasen, men også instansiere modelobjekterne og hydrere (udfylde dem) dem med kolonnedata.

    Så hvis du laver en simpel $users = User::all() og der er f.eks. 10.000 brugere, vil frameworket hente 10.000 rækker fra databasen og internt lave 10.000 nye User() og udfylde deres egenskaber med de relevante data . Dette er enorme mængder af arbejde, der bliver udført bag kulisserne, og hvis databasen er, hvor din applikation er ved at blive en flaskehals, er det til tider en god idé at omgå ORM.

    Dette gælder især for komplekse SQL-forespørgsler, hvor du skal springe over mange hoops og skrive lukninger efter lukninger og stadig ende med en effektiv forespørgsel. I sådanne tilfælde foretrækkes det at lave en DB::raw() og skrive forespørgslen i hånden.

    Går forbi dette præstationsundersøgelse, selv for simple indsatser Eloquent er meget langsommere, efterhånden som antallet af poster stiger:

    Brug caching så meget som muligt

    En af de bedst bevarede hemmeligheder ved webapplikationsoptimering er caching.

    For de uindviede betyder caching at forudberegne og gemme dyre resultater (dyre med hensyn til CPU og hukommelsesforbrug) og blot returnere dem, når den samme forespørgsel gentages.

    For eksempel kan det i en e-handelsbutik støde på, at af de 2 millioner produkter, det meste af tiden er folk interesserede i dem, der er frisk på lager, inden for et bestemt prisinterval og for en bestemt aldersgruppe. Det er spild at forespørge i databasen efter disse oplysninger – da forespørgslen ikke ændres ofte, er det bedre at gemme disse resultater et sted, hvor vi hurtigt kan få adgang.

    Laravel har indbygget understøttelse af flere typer caching. Ud over at bruge en caching-driver og opbygge caching-systemet fra bunden, vil du måske bruge nogle Laravel-pakker, der letter model caching, forespørgselscacheetc.

    Men bemærk, at ud over en vis forenklet use case, kan forudbyggede caching-pakker forårsage flere problemer, end de løser.

    Foretrækker caching i hukommelsen

    Når du cacher noget i Laravel, har du flere muligheder for, hvor du skal gemme den resulterende beregning, der skal cachelagres. Disse muligheder er også kendt som cache-drivere. Så selvom det er muligt og helt rimeligt at bruge filsystemet til at gemme cacheresultater, er det ikke rigtig, hvad caching er beregnet til at være.

    Ideelt set ønsker du at bruge en in-memory (bor helt i RAM’en) cache som Redis, Memcached, MongoDB osv., så under højere belastninger tjener caching en vital anvendelse i stedet for selv at blive en flaskehals.

    Nu tror du måske, at det at have en SSD-disk er næsten det samme som at bruge en RAM-stick, men det er ikke engang tæt på. Selv uformelle benchmarks viser, at RAM overgår SSD med 10-20 gange, når det kommer til hastighed.

    Mit yndlingssystem, når det kommer til caching, er Redis. Det er latterligt hurtigt (100.000 læseoperationer pr. sekund er almindelige), og kan for meget store cachesystemer udvikles til en klynge let.

    Cache ruterne

    Ligesom applikationskonfigurationen ændrer ruterne sig ikke meget over tid og er en ideel kandidat til caching. Dette gælder især, hvis du ikke kan tåle store filer som mig og ender med at dele din web.php og api.php over flere filer. En enkelt Laravel-kommando pakker alle tilgængelige ruter og holder dem ved hånden til fremtidig adgang:

    php artisan route:cache

    Og når du ender med at tilføje eller ændre ruter, skal du blot gøre:

    php artisan route:clear

    Billedoptimering og CDN

    Billeder er hjertet og sjælen i de fleste webapplikationer. Tilfældigvis er de også de største forbrugere af båndbredde og en af ​​de største grunde til langsomme apps/hjemmesider. Hvis du blot gemmer de uploadede billeder naivt på serveren og sender dem tilbage i HTTP-svar, lader du en massiv optimeringsmulighed slippe forbi.

      Sådan udsætter du e-mails i Gmail

    Min første anbefaling er ikke at gemme billeder lokalt – der er problemet med tab af data at håndtere, og afhængigt af hvilken geografisk region din kunde befinder sig i, kan dataoverførslen være smertefuldt langsom.

    Gå i stedet efter en løsning som Skyet der automatisk ændrer størrelsen og optimerer billeder i farten.

    Hvis det ikke er muligt, skal du bruge noget som Cloudflare til at cache og servere billeder, mens de er gemt på din server.

    Og hvis selv det ikke er muligt, gør det en stor forskel at justere din webserversoftware lidt for at komprimere aktiver og dirigere den besøgendes browser til at cache ting. Sådan ser et uddrag af Nginx-konfigurationen ud:

    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";
        }
    }

    Jeg er klar over, at billedoptimering ikke har noget med Laravel at gøre, men det er et så simpelt og kraftfuldt trick (og bliver så ofte forsømt), som ikke kunne lade være.

    Autoloader optimering

    Autoloading er en pæn, ikke så gammel funktion i PHP, der uden tvivl reddede sproget fra undergang. Når det er sagt, tager processen med at finde og indlæse den relevante klasse ved at dechifrere en given navneområdestreng tid og kan undgås i produktionsinstallationer, hvor høj ydeevne er ønskelig. Igen har Laravel en enkeltkommando løsning til dette:

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

    Bliv venner med køer

    Køer er, hvordan du behandler ting, når der er mange af dem, og hver af dem tager et par millisekunder at fuldføre. Et godt eksempel er at sende e-mails – en udbredt brugssag i webapps er at skyde et par notifikations-e-mails af, når en bruger udfører nogle handlinger.

    For eksempel, i et nyligt lanceret produkt, vil du måske have virksomhedens ledelse (ca. 6-7 e-mailadresser) til at blive underrettet, hver gang nogen afgiver en ordre over en bestemt værdi. Hvis vi antager, at din e-mail-gateway kan svare på din SMTP-anmodning på 500 ms, taler vi om en god 3-4 sekunders ventetid for brugeren, før ordrebekræftelsen træder i kraft. Et rigtig dårligt stykke UX, jeg er sikker på, at du vil enig.

    Løsningen er at gemme jobs, efterhånden som de kommer ind, fortælle brugeren, at alt gik godt, og behandle dem (et par sekunder) senere. Hvis der er en fejl, kan de i køen stillede job prøves igen et par gange, før de erklæres for mislykkede.

    Kreditering: Microsoft.com

    Mens et køsystem komplicerer opsætningen lidt (og tilføjer nogle overvågningsomkostninger), er det uundværligt i en moderne webapplikation.

    Aktiv optimering (Laravel Mix)

    For eventuelle frontend-aktiver i din Laravel-applikation skal du sørge for, at der er en pipeline, der kompilerer og minimerer alle aktivfilerne. De, der er komfortable med et bundlersystem som Webpack, Gulp, Parcel, osv., behøver ikke at bekymre sig, men hvis du ikke allerede gør dette, Laravel Mix er en solid anbefaling.

    Mix er en letvægts (og dejlig, helt ærligt!) indpakning omkring Webpack, som tager sig af alle dine CSS, SASS, JS osv. filer til produktion. En typisk .mix.js-fil kan være så lille som denne og stadig gøre underværker:

    const mix = require('laravel-mix');
    
    mix.js('resources/js/app.js', 'public/js')
        .sass('resources/sass/app.scss', 'public/css');

    Dette tager sig automatisk af import, minificering, optimering og hele støjen, når du er klar til produktion og kører npm run-produktion. Mix tager sig af ikke kun traditionelle JS- og CSS-filer, men også Vue- og React-komponenter, som du måtte have i dit applikations-workflow.

    Mere info her!

    Konklusion

    Ydeevneoptimering er mere kunst end videnskab – det er vigtigt at vide, hvordan og hvor meget man skal gøre, end hvad man skal gøre. Når det er sagt, er der ingen ende på, hvor meget og hvad du kan optimere i en Laravel-applikation.

    Men uanset hvad du gør, vil jeg gerne give dig nogle afskedsråd – optimering bør udføres, når der er en solid grund, og ikke fordi det lyder godt, eller fordi du er paranoid omkring appens ydeevne for 100.000+ brugere, mens den er i virkeligheden der er kun 10.

    Hvis du ikke er sikker på, om du har brug for at optimere din app eller ej, behøver du ikke sparke den ordsprogede hornets rede. En fungerende app, der føles kedelig, men gør præcis, hvad den skal, er ti gange mere ønskværdig end en app, der er blevet optimeret til en mutant hybrid supermaskine, men falder flad nu og da.

    Og for nybegynder at blive en Laravel-mester, tjek dette online kursus.

    Må dine apps køre meget, meget hurtigere! 🙂