Como otimizar o aplicativo da Web PHP Laravel para alto desempenho?

Laravel é muitas coisas. Mas rápido não é um deles. Vamos aprender alguns truques do ofício para acelerar!

Nenhum desenvolvedor PHP é intocável por laravel nos dias de hoje. Eles são desenvolvedores júnior ou intermediário que amam o desenvolvimento rápido que o Laravel oferece, ou são desenvolvedores seniores que estão sendo forçados a aprender Laravel por causa das pressões do mercado.

De qualquer forma, não há como negar que o Laravel revitalizou o ecossistema PHP (eu, com certeza, já teria deixado o mundo do PHP há muito tempo se o Laravel não existisse).

Um trecho de auto-elogio (um tanto justificado) do Laravel

No entanto, como o Laravel se esforça para facilitar as coisas para você, isso significa que, por baixo, ele está fazendo toneladas e toneladas de trabalho para garantir que você tenha uma vida confortável como desenvolvedor. Todos os recursos “mágicos” do Laravel que parecem funcionar têm camadas e mais camadas de código que precisam ser aprimoradas cada vez que um recurso é executado. Mesmo uma simples exceção rastreia a profundidade da toca do coelho (observe onde o erro começa, até o kernel principal):

Para o que parece ser um erro de compilação em uma das visualizações, há 18 chamadas de função para rastrear. Eu pessoalmente encontrei 40, e pode facilmente haver mais se você estiver usando outras bibliotecas e plugins.

A propósito, por padrão, essas camadas sobre camadas de código tornam o Laravel lento.

Quão lento é o Laravel?

Honestamente, é impossível responder a essa pergunta por vários motivos.

Primeiro, não há um padrão aceito, objetivo e sensato para medir a velocidade de aplicativos da Web. Mais rápido ou mais lento em comparação com o quê? Sob quais condições?

Em segundo lugar, um aplicativo da Web depende de tantas coisas (banco de dados, sistema de arquivos, rede, cache, etc.) que é bobagem falar sobre velocidade. Um aplicativo da Web muito rápido com um banco de dados muito lento é um aplicativo da Web muito lento. 🙂

Mas essa incerteza é precisamente o motivo pelo qual os benchmarks são populares. Mesmo que não signifiquem nada (veja isto e isto), eles fornecem algum quadro de referência e nos ajudam a não enlouquecer. Portanto, com várias pitadas de sal prontas, vamos ter uma ideia errada e grosseira da velocidade entre os frameworks PHP.

Passando por este bastante respeitável GitHub fonteveja como os frameworks PHP se alinham quando comparados:

Você pode nem notar o Laravel aqui (mesmo se você apertar os olhos com muita força), a menos que lance seu caso até o final da cauda. Sim, queridos amigos, o Laravel vem por último! Agora, com certeza, a maioria desses “frameworks” não são muito práticos ou mesmo úteis, mas nos dizem o quão lento o Laravel é quando comparado a outros mais populares.

Normalmente, essa “lentidão” não aparece nos aplicativos porque nossos aplicativos da web do dia a dia raramente atingem números altos. Mas quando o fazem (digamos, mais de 200-500 concorrentes), os servidores começam a engasgar e morrer. É o momento em que apenas jogar mais hardware no problema não resolve, e as contas de infraestrutura aumentam tão rápido que seus altos ideais de computação em nuvem desmoronam.

Mas ei, anime-se! Este artigo não é sobre o que não pode ser feito, mas sobre o que pode ser feito. 🙂

A boa notícia é que você pode fazer muito para tornar seu aplicativo Laravel mais rápido. Várias vezes rápido. Sim, sem brincadeira. Você pode fazer com que a mesma base de código fique balística e economize várias centenas de dólares em contas de infraestrutura/hospedagem todos os meses. Como? Vamos lá.

Quatro tipos de otimizações

Na minha opinião, a otimização pode ser feita em quatro níveis distintos (quando se trata de aplicações PHP):

  • Nível de linguagem: Isso significa que você usa uma versão mais rápida da linguagem e evita recursos/estilos específicos de codificação na linguagem que tornam seu código lento.
  • Nível de estrutura: essas são as coisas que abordaremos neste artigo.
  • Nível de infraestrutura: Ajustando seu gerenciador de processos PHP, servidor web, banco de dados, etc.
  • Nível de hardware: mudança para um provedor de hospedagem de hardware melhor, mais rápido e mais poderoso.

Todos esses tipos de otimizações têm seu lugar (por exemplo, a otimização PHP-fpm é bastante crítica e poderosa). Mas o foco deste artigo serão as otimizações puramente do tipo 2: aquelas relacionadas ao framework.

A propósito, não há lógica por trás da numeração e não é um padrão aceito. Acabei de inventar isso. Por favor, nunca me cite e diga: “Precisamos de otimização tipo 3 em nosso servidor”, ou o líder de sua equipe irá matá-lo, me encontrar e depois me matar também. 😀

E agora, finalmente, chegamos à terra prometida.

Esteja ciente de consultas de banco de dados n+1

O problema de consulta n+1 é comum quando ORMs são usados. O Laravel tem seu poderoso ORM chamado Eloquent, que é tão bonito, tão conveniente, que muitas vezes esquecemos de olhar o que está acontecendo.

  Como bloquear células do Excel com fórmulas para impedir a edição

Considere um cenário muito comum: exibir a lista de todos os pedidos feitos por uma determinada lista de clientes. Isso é bastante comum em sistemas de comércio eletrônico e em qualquer interface de relatório em geral, onde precisamos exibir todas as entidades relacionadas a algumas entidades.

No Laravel, podemos imaginar uma função de controlador que faz o trabalho assim:

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

Doce! E mais importante, elegante, bonito. 🤩🤩

Infelizmente, é uma maneira desastrosa de escrever código no Laravel.

Aqui está o porquê.

Quando pedimos ao ORM para procurar os clientes fornecidos, uma consulta SQL como esta é gerada:

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

O que é exatamente o esperado. Como resultado, todas as linhas retornadas são armazenadas na coleção $clientes dentro da função do controlador.

Agora, passamos por cada cliente, um por um, e recebemos seus pedidos. Isso executa a seguinte consulta. . .

SELECT * FROM orders WHERE customer_id = 22;

. . . quantas vezes houver clientes.

Em outras palavras, se precisarmos obter os dados do pedido de 1000 clientes, o número total de consultas ao banco de dados executadas será 1 (para buscar todos os dados dos clientes) + 1000 (para buscar os dados do pedido de cada cliente) = 1001. Isso é de onde vem o nome n+1.

Podemos fazer melhor? Certamente! Usando o que é conhecido como carregamento antecipado, podemos forçar o ORM a executar um JOIN e retornar todos os dados necessários em uma única consulta! Assim:

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

A estrutura de dados resultante é aninhada, com certeza, mas os dados do pedido podem ser facilmente extraídos. A única consulta resultante, neste caso, é algo como isto:

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

Uma única consulta é, obviamente, melhor do que mil consultas extras. Imagine o que aconteceria se houvesse 10.000 clientes para processar! Ou Deus me livre se também quiséssemos exibir os itens contidos em cada pedido! Lembre-se, o nome da técnica é carregamento antecipado e quase sempre é uma boa ideia.

Armazene a configuração!

Uma das razões para a flexibilidade do Laravel são os muitos arquivos de configuração que fazem parte do framework. Deseja alterar como/onde as imagens são armazenadas?

Bem, apenas altere o arquivo config/filesystems.php (pelo menos até o momento em que escrevo). Deseja trabalhar com vários drivers de fila? Sinta-se à vontade para descrevê-los em config/queue.php. Acabei de contar e descobri que existem 13 arquivos de configuração para diferentes aspectos da estrutura, garantindo que você não ficará desapontado, independentemente do que deseja alterar.

Dada a natureza do PHP, toda vez que uma nova solicitação da Web chega, o Laravel acorda, inicializa tudo e analisa todos esses arquivos de configuração para descobrir como fazer as coisas de maneira diferente desta vez. Só que é estúpido se nada mudou nos últimos dias! Reconstruir a configuração a cada requisição é um desperdício que pode (na verdade, deve ser) evitado, e a saída é um simples comando que o Laravel oferece:

php artisan config:cache

O que isso faz é combinar todos os arquivos de configuração disponíveis em um único e o cache fica em algum lugar para recuperação rápida. Na próxima vez que houver uma solicitação da Web, o Laravel simplesmente lerá esse único arquivo e seguirá em frente.

Dito isso, o cache de configuração é uma operação extremamente delicada que pode explodir na sua cara. A maior pegadinha é que, uma vez que você emitiu este comando, as chamadas de função env () de todos os lugares, exceto os arquivos de configuração, retornarão null!

Faz sentido quando você pensa sobre isso. Se você usar o cache de configuração, estará dizendo ao framework: “Sabe de uma coisa, acho que configurei bem as coisas e tenho 100% de certeza de que não quero que elas mudem”. Em outras palavras, você espera que o ambiente permaneça estático, para o que servem os arquivos .env.

Com isso dito, aqui estão algumas regras rígidas, sagradas e inquebráveis ​​de cache de configuração:

  • Faça isso apenas em um sistema de produção.
  • Faça isso apenas se tiver certeza de que deseja congelar a configuração.
  • Caso algo dê errado, desfaça a configuração com php crafter cache:clear
  • Ore para que o dano causado ao negócio não seja significativo!
  • Reduzir serviços carregados automaticamente

    Para ser útil, o Laravel carrega uma tonelada de serviços quando acorda. Eles estão disponíveis no arquivo config/app.php como parte da chave do array ‘providers’. Vamos dar uma olhada no que tenho no meu caso:

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

    Mais uma vez contei e são 27 serviços listados! Agora, você pode precisar de todos eles, mas é improvável.

      Como copiar e colar camadas entre arquivos PSD via atalhos de teclado

    Por exemplo, estou construindo uma API REST no momento, o que significa que não preciso do Session Service Provider, View Service Provider, etc. E como estou fazendo algumas coisas do meu jeito e não seguindo os padrões da estrutura , também posso desabilitar o provedor de serviços de autenticação, o provedor de serviços de paginação, o provedor de serviços de tradução e assim por diante. Ao todo, quase metade deles são desnecessários para o meu caso de uso.

    Dê uma boa olhada em seu aplicativo. Ela precisa de todos esses provedores de serviços? Mas, pelo amor de Deus, por favor, não comente cegamente sobre esses serviços e empurre para a produção! Execute todos os testes, verifique as coisas manualmente nas máquinas de desenvolvimento e preparação e seja muito, muito paranóico antes de puxar o gatilho. 🙂

    Seja sábio com pilhas de middleware

    Quando você precisa de algum processamento personalizado da solicitação da Web recebida, criar um novo middleware é a resposta. Agora, é tentador abrir app/Http/Kernel.php e inserir o middleware na pilha da web ou da API; dessa forma, ele fica disponível em todo o aplicativo e se não estiver fazendo algo intrusivo (como registrar ou notificar, por exemplo).

    No entanto, à medida que o aplicativo cresce, essa coleção de middleware global pode se tornar um fardo silencioso para o aplicativo se todos (ou a maioria) deles estiverem presentes em todas as solicitações, mesmo que não haja motivo comercial para isso.

    Em outras palavras, tenha cuidado onde você adiciona/aplica um novo middleware. Pode ser mais conveniente adicionar algo globalmente, mas a penalidade de desempenho é muito alta a longo prazo. Eu sei a dor que você teria que passar se aplicasse seletivamente o middleware toda vez que houvesse uma nova mudança, mas é uma dor que eu aceitaria e recomendaria de bom grado!

    Evite o ORM (às vezes)

    Embora o Eloquent torne agradáveis ​​muitos aspectos da interação com o banco de dados, isso ocorre com o custo da velocidade. Sendo um mapeador, o ORM não só precisa buscar registros do banco de dados, mas também instanciar os objetos do modelo e hidratá-los (preenchê-los) com dados de coluna.

    Portanto, se você fizer um simples $users = User::all() e houver, digamos, 10.000 usuários, a estrutura buscará 10.000 linhas do banco de dados e internamente fará 10.000 new User() e preencherá suas propriedades com os dados relevantes . Isso representa uma grande quantidade de trabalho sendo feito nos bastidores e, se o banco de dados é onde seu aplicativo está se tornando um gargalo, ignorar o ORM às vezes é uma boa ideia.

    Isso é especialmente verdadeiro para consultas SQL complexas, nas quais você teria que pular muitos obstáculos e escrever fechamentos após fechamentos e ainda assim terminar com uma consulta eficiente. Nesses casos, é preferível fazer um DB::raw() e escrever a consulta manualmente.

    passando por isto estudo de desempenho, mesmo para inserções simples Eloquent é muito mais lento à medida que o número de registros aumenta:

    Use o cache o máximo possível

    Um dos segredos mais bem guardados da otimização de aplicativos da Web é o armazenamento em cache.

    Para os não iniciados, o cache significa pré-computar e armazenar resultados caros (caros em termos de uso de CPU e memória) e simplesmente devolvê-los quando a mesma consulta for repetida.

    Por exemplo, em uma loja de comércio eletrônico, pode se deparar com 2 milhões de produtos, na maioria das vezes as pessoas estão interessadas naqueles que estão no estoque, dentro de uma determinada faixa de preço e para uma determinada faixa etária. Consultar o banco de dados para obter essas informações é um desperdício — como a consulta não muda com frequência, é melhor armazenar esses resultados em algum lugar que possamos acessar rapidamente.

    Laravel tem suporte embutido para vários tipos de cache. Além de usar um driver de cache e construir o sistema de cache desde o início, você pode querer usar alguns pacotes Laravel que facilitam cache de modelos, cache de consultaetc

    Mas observe que, além de um determinado caso de uso simplificado, os pacotes de cache pré-construídos podem causar mais problemas do que resolvê-los.

    Prefira o cache na memória

    Quando você armazena algo em cache no Laravel, você tem várias opções de onde armazenar a computação resultante que precisa ser armazenada em cache. Essas opções também são conhecidas como drivers de cache. Portanto, embora seja possível e perfeitamente razoável usar o sistema de arquivos para armazenar os resultados do cache, não é realmente o que o cache deve ser.

    Idealmente, você deseja usar um cache na memória (vivendo inteiramente na RAM) como Redis, Memcached, MongoDB etc.

    Agora, você pode pensar que ter um disco SSD é quase o mesmo que usar um stick de RAM, mas não chega nem perto. Mesmo informal benchmarks mostram que a RAM supera o SSD em 10 a 20 vezes quando se trata de velocidade.

    Meu sistema favorito quando se trata de cache é o Redis. Isso é ridiculamente rápido (100.000 operações de leitura por segundo são comuns), e para sistemas de cache muito grandes, pode evoluir para um conjunto facilmente.

    Armazenar as rotas

    Assim como a configuração do aplicativo, as rotas não mudam muito ao longo do tempo e são um candidato ideal para armazenamento em cache. Isso é especialmente verdadeiro se você não suporta arquivos grandes como eu e acaba dividindo seu web.php e api.php em vários arquivos. Um único comando do Laravel agrupa todas as rotas disponíveis e as mantém à mão para acesso futuro:

    php artisan route:cache

    E quando você acabar adicionando ou alterando rotas, simplesmente faça:

    php artisan route:clear

    Otimização de imagens e CDN

    As imagens são o coração e a alma da maioria dos aplicativos da Web. Coincidentemente, eles também são os maiores consumidores de largura de banda e um dos maiores motivos de aplicativos/sites lentos. Se você simplesmente armazenar as imagens carregadas ingenuamente no servidor e enviá-las de volta em respostas HTTP, estará deixando escapar uma enorme oportunidade de otimização.

      15 melhores upscaler de imagem baseado em IA para melhorar a qualidade da foto etechpt.com

    Minha primeira recomendação é não armazenar imagens localmente — há o problema de perda de dados a ser resolvido e, dependendo da região geográfica em que seu cliente está, a transferência de dados pode ser dolorosamente lenta.

    Em vez disso, escolha uma solução como Cloudinário que redimensiona automaticamente e otimiza as imagens em tempo real.

    Se isso não for possível, use algo como Cloudflare para armazenar em cache e servir imagens enquanto elas estão armazenadas em seu servidor.

    E mesmo que isso não seja possível, ajustar um pouco o software do servidor da Web para compactar ativos e direcionar o navegador do visitante para armazenar em cache as coisas faz muita diferença. Veja como seria um trecho de configuração do Nginx:

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

    Estou ciente de que a otimização de imagem não tem nada a ver com o Laravel, mas é um truque tão simples e poderoso (e é frequentemente negligenciado) que não pude evitar.

    Otimização do carregador automático

    O carregamento automático é um recurso legal e não tão antigo no PHP que, sem dúvida, salvou a linguagem da destruição. Dito isso, o processo de localizar e carregar a classe relevante decifrando uma determinada string de namespace leva tempo e pode ser evitado em implantações de produção em que o alto desempenho é desejável. Mais uma vez, o Laravel tem uma solução de comando único para isso:

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

    Faça amizade com as filas

    Filas é como você processa as coisas quando há muitas delas, e cada uma delas leva alguns milissegundos para ser concluída. Um bom exemplo é o envio de e-mails — um caso de uso generalizado em aplicativos da web é disparar alguns e-mails de notificação quando um usuário executa algumas ações.

    Por exemplo, em um produto recém-lançado, você pode querer que a liderança da empresa (alguns 6-7 endereços de e-mail) seja notificada sempre que alguém fizer um pedido acima de um determinado valor. Supondo que seu gateway de e-mail possa responder à sua solicitação SMTP em 500 ms, estamos falando de uma boa espera de 3 a 4 segundos para o usuário antes que a confirmação do pedido seja iniciada. aceita.

    A solução é armazenar os trabalhos à medida que chegam, informar ao usuário que tudo correu bem e processá-los (alguns segundos) depois. Se houver um erro, as tarefas enfileiradas podem ser repetidas algumas vezes antes de serem declaradas como falhadas.

    Créditos: Microsoft.com

    Embora um sistema de filas complique um pouco a configuração (e adicione alguma sobrecarga de monitoramento), ele é indispensável em um aplicativo da Web moderno.

    Otimização de ativos (Laravel Mix)

    Para quaisquer recursos de front-end em seu aplicativo Laravel, verifique se há um pipeline que compila e reduz todos os arquivos de recursos. Aqueles que se sentem confortáveis ​​com um sistema empacotador como Webpack, Gulp, Parcel, etc., não precisam se preocupar, mas se você ainda não está fazendo isso, Laravel MixName é uma recomendação sólida.

    O Mix é um wrapper leve (e delicioso, com toda a honestidade!) do Webpack, que cuida de todos os seus arquivos CSS, SASS, JS, etc., para produção. Um arquivo .mix.js típico pode ser tão pequeno quanto este e ainda fazer maravilhas:

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

    Isso cuida automaticamente de importações, minificação, otimização e todo o shebang quando você estiver pronto para produção e executar a produção de execução npm. O Mix cuida não apenas dos arquivos JS e CSS tradicionais, mas também dos componentes Vue e React que você pode ter no fluxo de trabalho do seu aplicativo.

    Mais informações aqui!

    Conclusão

    A otimização de desempenho é mais arte do que ciência — saber como e quanto fazer é mais importante do que o que fazer. Dito isso, não há limite para quanto e o que você pode otimizar em um aplicativo Laravel.

    Mas, faça o que fizer, gostaria de deixar alguns conselhos de despedida – a otimização deve ser feita quando houver um motivo sólido, e não porque soe bem ou porque você está paranóico com o desempenho do aplicativo para mais de 100.000 usuários enquanto na realidade são apenas 10.

    Se você não tem certeza se precisa otimizar seu aplicativo ou não, não precisa chutar o proverbial ninho de vespas. Um aplicativo de trabalho que parece chato, mas faz exatamente o que deve ser dez vezes mais desejável do que um aplicativo que foi otimizado em uma supermáquina híbrida mutante, mas falha de vez em quando.

    E, para newbiew se tornar um mestre Laravel, confira este curso online.

    Que seus aplicativos rodem muito, muito mais rápido! 🙂