Otimize seu Laravel: 15+ dicas para alta performance!

O Laravel é multifacetado, mas a velocidade não é seu ponto forte. Vamos explorar algumas técnicas para otimizar a performance!

Atualmente, poucos desenvolvedores PHP escapam do Laravel. Sejam eles juniores ou intermediários, atraídos pela agilidade de desenvolvimento que o framework oferece, ou seniores que se veem compelidos a aprender devido às demandas do mercado, o Laravel se tornou uma referência.

É inegável que o Laravel revitalizou o ecossistema PHP (sem ele, talvez eu já tivesse abandonado o universo PHP há muito tempo).

Um breve elogio (justificado) ao Laravel:

A facilidade de uso proporcionada pelo Laravel implica que, nos bastidores, ele realiza uma quantidade massiva de trabalho para garantir a sua comodidade como desenvolvedor. Todos os recursos “mágicos” do Laravel envolvem várias camadas de código que precisam ser processadas a cada execução. Até mesmo um simples rastreamento de exceção demonstra a profundidade dessa estrutura (observe o ponto de partida do erro até o núcleo do sistema):

Um erro de compilação em uma das visualizações pode gerar 18 chamadas de função para rastrear. Já observei 40, e esse número pode aumentar dependendo das bibliotecas e plugins utilizados.

Essa complexidade, com suas camadas de código, é o principal fator que torna o Laravel, por padrão, relativamente lento.

Qual é a lentidão do Laravel?

Responder a essa questão é difícil por vários motivos.

Primeiramente, não existe um padrão objetivo e consensual para medir a velocidade de aplicativos web. Comparado a quê? Sob quais condições?

Em segundo lugar, um aplicativo web depende de inúmeros fatores (banco de dados, sistema de arquivos, rede, cache, etc.) que tornam inadequado falar sobre velocidade de forma isolada. Um aplicativo web rápido com um banco de dados lento, resulta em um aplicativo lento. 🙂

Essa incerteza é o que impulsiona a popularidade dos benchmarks. Mesmo que não sejam perfeitos (veja este artigo e este outro), eles fornecem um ponto de referência para evitar conclusões precipitadas. Portanto, com uma dose de ceticismo, vamos analisar uma ideia geral da velocidade dos frameworks PHP.

Com base neste respeitável repositório do GitHub, observe como os frameworks PHP se comparam:

Perceba que o Laravel aparece no final da lista. Sim, o Laravel está em último lugar! É verdade que muitos desses “frameworks” não são práticos ou sequer úteis, mas eles ilustram a lentidão relativa do Laravel em comparação com outros mais populares.

Essa “lentidão” geralmente não é perceptível em aplicativos do dia a dia, pois eles raramente atingem números elevados de tráfego. No entanto, quando isso acontece (acima de 200-500 acessos simultâneos), os servidores começam a apresentar problemas e falhas. Nesse ponto, aumentar o hardware não resolve o problema, e os custos de infraestrutura crescem rapidamente, desfazendo seus ideais de computação em nuvem.

Mas não se desanime! Este artigo não se concentra no que não pode ser feito, mas sim no que pode ser aprimorado. 🙂

A boa notícia é que há muito que você pode fazer para acelerar seu aplicativo Laravel. E não estamos falando de pequenas melhorias, mas de aumentos significativos de performance. É possível tornar seu código mais eficiente e economizar consideravelmente em custos de infraestrutura. Como? Vamos descobrir.

Quatro tipos de otimização

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

  • Nível de linguagem: Utilizar versões mais rápidas da linguagem e evitar práticas de codificação que tornam o código lento.
  • Nível de framework: Foco principal deste artigo, abordando otimizações específicas do Laravel.
  • Nível de infraestrutura: Ajustar o gerenciador de processos PHP, servidor web, banco de dados, etc.
  • Nível de hardware: Migrar para um provedor de hospedagem mais robusto e rápido.

Todos esses tipos de otimização são importantes (por exemplo, a otimização do PHP-fpm é crucial). No entanto, o foco aqui será nas otimizações do tipo 2, diretamente relacionadas ao framework.

A numeração utilizada não segue um padrão, é apenas uma forma de organização. Não a utilize como referência em discussões técnicas, pois isso pode gerar confusão. 😀

E agora, finalmente, chegamos ao ponto principal.

Atente para consultas de banco de dados N+1

O problema de consultas N+1 é comum ao usar ORMs. O Laravel utiliza o Eloquent, um ORM poderoso e conveniente, que nos faz, muitas vezes, esquecer o que está acontecendo por trás dos panos.

Considere um cenário comum: exibir todos os pedidos feitos por uma lista de clientes. Isso é frequente em sistemas de e-commerce e em interfaces de relatórios, onde precisamos mostrar entidades relacionadas a outras.

Em Laravel, podemos imaginar uma função de controller que faz esse trabalho da seguinte forma:

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

Simples e elegante! 🤩🤩

Infelizmente, essa é uma forma desastrosa de escrever código em Laravel.

A razão:

Quando solicitamos que o ORM procure os clientes especificados, uma consulta SQL como esta é gerada:

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

O que é esperado. Todas as linhas retornadas são armazenadas na coleção $customers dentro da função do controller.

Em seguida, percorremos cada cliente e obtemos seus pedidos, executando a seguinte consulta…

SELECT * FROM orders WHERE customer_id = 22;

…e assim por diante, para cada cliente.

Ou seja, para obter os dados dos pedidos de 1000 clientes, o número total de consultas ao banco de dados seria 1 (para buscar todos os dados dos clientes) + 1000 (para buscar os dados dos pedidos de cada cliente) = 1001. É daí que surge o nome N+1.

Podemos fazer melhor? Claro! Usando o conceito conhecido como carregamento ansioso (eager loading), podemos forçar o ORM a executar um JOIN e obter todos os dados necessários em uma única consulta! Assim:

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

A estrutura de dados resultante é aninhada, mas os dados dos pedidos podem ser extraídos facilmente. A única consulta gerada, neste caso, é algo assim:

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

Uma única consulta é, obviamente, superior a mil consultas adicionais. Imagine o impacto com 10.000 clientes para processar! Ou pior, se também precisássemos exibir os itens contidos em cada pedido! Lembre-se, o nome da técnica é carregamento ansioso e ela quase sempre é uma boa ideia.

Armazene a configuração em cache!

A flexibilidade do Laravel reside nos inúmeros arquivos de configuração que fazem parte do framework. Precisa alterar onde as imagens são armazenadas? Basta modificar o arquivo config/filesystems.php (pelo menos, até o momento em que este artigo foi escrito). Quer trabalhar com diferentes drivers de fila? Configure-os em config/queue.php. Existem 13 arquivos de configuração que atendem a diferentes aspectos do framework, garantindo que você possa personalizar o sistema como desejar.

Devido à natureza do PHP, a cada nova requisição web, o Laravel inicializa tudo e analisa todos esses arquivos de configuração para entender como agir dessa vez. Isso é ineficiente se nada mudou recentemente! Reconstruir a configuração a cada requisição é um desperdício que pode (e deve) ser evitado com um comando simples do Laravel:

php artisan config:cache

Este comando combina todos os arquivos de configuração em um único arquivo, armazenado em cache para acesso rápido. Nas requisições seguintes, o Laravel lê este único arquivo, evitando a leitura de cada arquivo individualmente.

No entanto, o cache de configuração é uma operação delicada. Uma pegadinha é que, após executar este comando, as chamadas da função env(), exceto nos arquivos de configuração, retornarão null!

Isso faz sentido se você pensar que, ao usar o cache de configuração, você está informando ao framework: “Tenho certeza de que a configuração está correta e não quero que ela mude.” Ou seja, você espera que o ambiente permaneça estático, que é o propósito dos arquivos .env.

Com isso em mente, aqui estão algumas regras importantes para o cache de configuração:

  • Execute apenas em um sistema de produção.
  • Execute apenas se você tiver certeza de que deseja congelar a configuração.
  • Se algo der errado, desfaça a configuração com php artisan config:clear
  • Torça para que os problemas causados não sejam significativos!

Reduza os serviços carregados automaticamente

Para funcionar corretamente, o Laravel carrega diversos serviços ao ser inicializado. Eles estão listados no arquivo config/app.php, na chave ‘providers’. Veja os serviços carregados por padrão:

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

    ],

São 27 serviços listados! Embora você possa precisar de todos, é improvável.

Por exemplo, se você estiver construindo uma API REST, não precisará do Session Service Provider ou do View Service Provider. Se você estiver utilizando métodos que fogem dos padrões do framework, também pode desativar o provedor de serviços de autenticação, o provedor de serviços de paginação e o provedor de serviços de tradução. No meu caso, quase metade deles são desnecessários.

Analise seu aplicativo com atenção. Ele realmente precisa de todos esses provedores de serviços? Mas, por favor, não desabilite cegamente esses serviços e envie para produção! Execute testes, verifique as alterações em ambientes de desenvolvimento e preparação e seja muito cauteloso antes de realizar a mudança. 🙂

Utilize middlewares de forma consciente

Quando você precisa de algum processamento personalizado das requisições web, criar um novo middleware é a solução. É tentador abrir o arquivo app/Http/Kernel.php e adicionar o middleware à pilha web ou API, tornando-o disponível em todo o aplicativo. Isso pode ser prático se você precisar de um middleware que não seja intrusivo, como um logger ou um sistema de notificação.

No entanto, à medida que o aplicativo cresce, essa coleção global de middlewares pode se tornar um fardo silencioso se a maioria deles estiver presente em todas as requisições, mesmo sem necessidade. Em outras palavras, tenha cuidado ao adicionar um novo middleware. Adicionar algo globalmente pode ser conveniente, mas a longo prazo pode impactar a performance. Sei que aplicar middlewares seletivamente pode ser mais trabalhoso, mas é um esforço que eu recomendo.

Evite o ORM (em alguns casos)

Embora o Eloquent facilite a interação com o banco de dados, isso tem um custo em termos de velocidade. O ORM não só precisa buscar registros do banco de dados, mas também instanciar os objetos modelo e preenchê-los com os dados das colunas.

Por exemplo, se você executar um simples $users = User::all() e houver 10.000 usuários, o framework buscará 10.000 linhas do banco de dados e internamente executará 10.000 instâncias de new User() e preencherá suas propriedades com os dados. Isso representa um grande volume de trabalho nos bastidores. Se o banco de dados for um gargalo no seu aplicativo, ignorar o ORM pode ser uma boa alternativa.

Isso é especialmente verdadeiro em consultas SQL complexas. Nesses casos, utilizar DB::raw() e escrever a consulta manualmente é preferível.

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

Utilize o cache ao máximo

Um dos segredos mais importantes para otimizar aplicações web é o cache.

Para quem não está familiarizado, o cache significa pré-calcular e armazenar resultados que exigem muitos recursos computacionais (CPU e memória) e simplesmente retorná-los quando a mesma requisição é repetida.

Em uma loja de comércio eletrônico com 2 milhões de produtos, por exemplo, as pessoas geralmente estão interessadas naqueles que estão em 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, pois a consulta não muda com frequência. É melhor armazenar esses resultados em um local de acesso rápido.

O Laravel oferece suporte nativo para vários tipos de cache. Além de utilizar um driver de cache e criar um sistema de cache personalizado, você também pode usar pacotes Laravel que simplificam o cache de modelos, cache de consultas, etc.

No entanto, em cenários mais complexos, os pacotes de cache podem causar mais problemas do que soluções.

Prefira cache em memória

Ao usar o cache no Laravel, você tem diversas opções de onde armazenar os dados. Essas opções são conhecidas como drivers de cache. Embora seja possível e razoável utilizar o sistema de arquivos para armazenar o cache, não é a opção ideal.

O ideal é utilizar um cache em memória (na RAM), como Redis, Memcached ou MongoDB.

Embora um disco SSD possa parecer semelhante à RAM, a diferença é grande. Mesmo benchmarks informais mostram que a RAM supera o SSD em velocidade por uma margem de 10 a 20 vezes.

Meu sistema preferido de cache é o Redis. Ele é incrivelmente rápido (100.000 operações de leitura por segundo são comuns), e para sistemas de cache grandes, ele pode ser expandido para um cluster facilmente.

Armazene as rotas em cache

Assim como a configuração do aplicativo, as rotas não mudam com frequência e são candidatas ideais para serem armazenadas em cache. Isso é especialmente verdadeiro se você tem diversos arquivos de rotas, dividindo seu web.php e api.php em múltiplos arquivos. Um único comando do Laravel agrupa todas as rotas disponíveis e as mantém prontas para acesso futuro:

php artisan route:cache

Se você adicionar ou alterar rotas, execute:

php artisan route:clear

Otimização de imagens e CDN

As imagens são cruciais para a maioria dos aplicativos web. E também são as maiores consumidoras de largura de banda e um dos maiores motivos de lentidão. Se você simplesmente armazena as imagens carregadas diretamente no servidor e as envia nas respostas HTTP, você está perdendo uma oportunidade de otimização.

Minha primeira recomendação é não armazenar imagens localmente. Isso envolve problemas de perda de dados e, dependendo da localização do cliente, a transferência de dados pode ser muito lenta.

Utilize uma solução como o Cloudinary, que redimensiona e otimiza imagens automaticamente em tempo real.

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

Se nenhuma das opções acima for viável, configure seu servidor web para comprimir os arquivos e direcione o navegador do visitante a armazená-los em cache. Veja um exemplo 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";
    }
}

A otimização de imagens não está diretamente relacionada ao Laravel, mas é um truque tão simples e poderoso (e frequentemente ignorado) que eu não poderia deixar de mencionar.

Otimização do autoloader

O autoloader é um recurso do PHP que, sem dúvida, salvou a linguagem da destruição. No entanto, o processo de localizar e carregar a classe correta, decifrando o namespace, leva tempo e pode ser otimizado em ambientes de produção onde o desempenho é crucial. O Laravel oferece um comando simples para isso:

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

Use filas

Filas são importantes para processar tarefas que demoram alguns milissegundos para serem concluídas. Um exemplo comum é o envio de e-mails de notificação quando um usuário realiza alguma ação.

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

A solução é armazenar os trabalhos em uma fila, informar ao usuário que tudo ocorreu bem e processá-los (alguns segundos) depois. Se houver um erro, os trabalhos enfileirados podem ser repetidos algumas vezes antes de serem considerados falhos.

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 web moderno.

Otimização de assets (Laravel Mix)

Para os recursos de front-end do seu aplicativo Laravel, garanta que exista um processo que compila e reduz todos os arquivos de recursos. Quem está familiarizado com ferramentas como Webpack, Gulp ou Parcel não precisa se preocupar. Caso contrário, o Laravel Mix é uma excelente opção.

O Mix é um wrapper (leve e excelente) do Webpack que cuida de seus arquivos CSS, SASS e JS. Um arquivo .mix.js típico pode ser tão simples quanto este:

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 e otimizações, quando você executa npm run production. O Mix também é compatível com componentes Vue e React.

Mais informações aqui!

Conclusão

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

Mas, faça o que fizer, gostaria de deixar um conselho final: a otimização deve ser feita quando há um motivo sólido e não por precaução. Um aplicativo funcional, que cumpre suas tarefas, mesmo que não seja otimizado, é mais desejável do que um aplicativo que foi excessivamente otimizado, mas que falha ocasionalmente.

Se você não tem certeza se precisa otimizar seu aplicativo, não mexa naquilo que está funcionando. Um aplicativo que funciona e cumpre seu propósito é melhor do que um aplicativo otimizado que não entrega o resultado esperado.

Para quem deseja se tornar um mestre em Laravel, confira este curso online.

Que seus aplicativos sejam muito, muito mais rápidos! 🙂