• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to Debug and Fix Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Modern Laravel Applications

How to Debug and Fix Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Modern Laravel Applications

Identifying Slow Database Queries Impacting LCP

The Largest Contentful Paint (LCP) metric is a crucial indicator of perceived loading speed. When database queries become bottlenecks, especially those executed during the initial page render to fetch content for the LCP element, they can significantly degrade this metric. In modern Laravel applications, identifying these slow queries requires a multi-pronged approach, combining application-level profiling with database-level introspection.

The first line of defense is Laravel’s built-in query log. While useful for development, it can be too verbose for production. A more targeted approach involves enabling query logging conditionally or using a dedicated profiling tool.

Leveraging Laravel Debugbar for Development

For development environments, the barryvdh/laravel-debugbar package is invaluable. It provides an in-page overlay that details executed queries, their execution time, and bindings. Pay close attention to queries listed under the “Queries” tab that have high execution times, especially those that appear early in the request lifecycle.

To install and configure Debugbar:

  • Run the composer command: composer require barryvdh/laravel-debugbar --dev
  • Add the service provider to your config/app.php (or let auto-discovery handle it):
    'providers' => [
        // ...
        Barryvdh\Debugbar\ServiceProvider::class,
    ],
    
  • Publish the configuration: php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"

Once enabled, navigate to a page experiencing slow LCP. The Debugbar will show a list of queries. Sort by duration and identify the slowest ones. Look for queries that fetch data for the primary content block that constitutes your LCP element.

Production-Level Query Logging and Analysis

In production, enabling full query logging can impact performance. Instead, we can use Laravel’s event dispatcher to log slow queries selectively. We’ll listen for the Illuminate\Database\Events\QueryExecuted event and log queries exceeding a defined threshold.

Create a new listener:

  • php artisan make:listener LogSlowQueries --event="Illuminate\Database\Events\QueryExecuted"

Then, register this listener in your app/Providers/EventServiceProvider.php:

  • protected $listen = [ Illuminate\Database\Events\QueryExecuted::class => [ App\Listeners\LogSlowQueries::class, ], ];

Implement the listener logic:

  • app/Listeners/LogSlowQueries.php

Here’s a sample implementation that logs queries taking longer than 100ms to storage/logs/laravel.log:

  • app/Listeners/LogSlowQueries.php

Modify the handle method:

  • app/Listeners/LogSlowQueries.php
namespace App\Listeners;

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class LogSlowQueries
{
    /**
     * The threshold in milliseconds for logging slow queries.
     *
     * @var int
     */
    protected $threshold = 100; // Log queries slower than 100ms

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        // You can optionally get the threshold from config
        // $this->threshold = config('logging.slow_query_threshold', 100);
    }

    /**
     * Handle the event.
     *
     * @param  QueryExecuted  $event
     * @return void
     */
    public function handle(QueryExecuted $event)
    {
        // Avoid logging queries from migrations or seeders if not desired
        if (app()->runningInConsole() && !config('app.debug')) {
            return;
        }

        // Calculate execution time in milliseconds
        $executionTime = round($event->time, 2);

        if ($executionTime > $this->threshold) {
            $sql = $event->sql;
            $bindings = $event->bindings;

            // Format bindings for better readability
            $formattedBindings = [];
            foreach ($bindings as $key => $value) {
                // Handle different data types for bindings
                if (is_string($value)) {
                    $formattedBindings[$key] = "'{$value}'";
                } elseif (is_numeric($value)) {
                    $formattedBindings[$key] = $value;
                } elseif (is_null($value)) {
                    $formattedBindings[$key] = 'NULL';
                } else {
                    $formattedBindings[$key] = json_encode($value);
                }
            }

            // Replace placeholders with actual bindings
            $fullSql = Str::replaceArray('?', $formattedBindings, $sql);

            Log::channel('sql')->warning(sprintf(
                'Slow query detected: %s ms. SQL: %s',
                $executionTime,
                $fullSql
            ));
        }
    }
}

You’ll also need to configure a dedicated log channel for SQL queries in config/logging.php:

  • config/logging.php
// ... other channels
'channels' => [
    // ...
    'sql' => [
        'driver' => 'single',
        'path' => storage_path('logs/laravel-sql.log'),
        'level' => env('LOG_LEVEL', 'debug'),
    ],
    // ...
],
// ...

After implementing this, monitor storage/logs/laravel-sql.log for slow queries during peak traffic or when LCP issues are reported. This provides production-safe insights into query performance.

Database-Level Performance Analysis

Once slow queries are identified, the next step is to analyze them at the database level. This involves using database-specific tools to understand execution plans and identify inefficiencies.

Using EXPLAIN for Query Optimization

The EXPLAIN command (or EXPLAIN ANALYZE in PostgreSQL) is fundamental. It shows how the database intends to execute a query, including table access methods, join types, and index usage.

To use it, prepend EXPLAIN to your slow SQL query. For example, if you identified a slow query fetching user profiles:

EXPLAIN SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 10;

Analyze the output. Key indicators of poor performance include:

  • type: ALL (Full table scan)
  • rows: A very large number of rows examined.
  • Extra: Using filesort or Using temporary: Indicates sorting or temporary table creation, which can be slow.
  • Lack of appropriate indexes in the key column.

For PostgreSQL, EXPLAIN ANALYZE provides actual execution times, which is even more insightful.

EXPLAIN ANALYZE SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC LIMIT 10;

Index Optimization Strategies

The most common cause of slow `SELECT` queries is missing or inefficient indexes. Based on the EXPLAIN output, you can create or modify indexes.

For the example query above, if status and created_at are not indexed, or if the index isn’t optimal for the `WHERE` and `ORDER BY` clauses, creating a composite index can dramatically improve performance.

Consider the query’s filtering and sorting criteria. A composite index on (status, created_at) would be beneficial for this specific query.

-- For MySQL/MariaDB
CREATE INDEX idx_users_status_created_at ON users (status, created_at DESC);

-- For PostgreSQL
CREATE INDEX idx_users_status_created_at ON users USING btree (status, created_at DESC);

Important Considerations for Indexes:

  • Cardinality: Index columns with high cardinality (many distinct values) are generally more effective.
  • Index Selectivity: An index is more selective if it filters out a larger percentage of rows.
  • Composite Indexes: Order matters. Place columns used in `WHERE` clauses first, followed by columns used in `ORDER BY` or `GROUP BY`.
  • Covering Indexes: If an index includes all columns needed by the query (in the `SELECT` list, `WHERE`, `ORDER BY`, etc.), the database might be able to satisfy the query using only the index, avoiding table lookups.
  • Index Maintenance: Too many indexes can slow down `INSERT`, `UPDATE`, and `DELETE` operations. Regularly review and prune unused or redundant indexes.

Refactoring Laravel Code for Performance

Beyond database-level tuning, the way Laravel interacts with the database can also be optimized. Inefficient Eloquent usage or N+1 query problems are common culprits.

Eager Loading with Eloquent

The N+1 query problem occurs when you retrieve a list of parent models and then, for each parent, execute a separate query to fetch its related children. This is particularly detrimental to LCP if the related data is part of the LCP element.

Problematic Code (N+1):

// Fetching 10 posts
$posts = Post::take(10)->get();

// For each post, a separate query to fetch its author
foreach ($posts as $post) {
    echo $post->author->name; // This triggers a new query for each post
}

Optimized Code (Eager Loading):

// Eager load the 'author' relationship
$posts = Post::with('author')->take(10)->get();

// No new queries are triggered here
foreach ($posts as $post) {
    echo $post->author->name;
}

Ensure that any relationships used to render the LCP element are eagerly loaded. Use Laravel Debugbar or your production logging to identify unexpected `SELECT` queries within loops.

Optimizing Complex Queries and Joins

Sometimes, complex `JOIN` operations or subqueries in Eloquent can lead to inefficient SQL. It’s often beneficial to analyze the generated SQL and, if necessary, write raw SQL queries or use query builders more judiciously.

Consider a scenario where you need to fetch a list of products and their most recent review, but only if the product has reviews.

Eloquent Approach (Potentially Inefficient):

$products = Product::with(['reviews' => function ($query) {
    $query->orderBy('created_at', 'desc')->limit(1);
}])
->has('reviews') // This adds a WHERE EXISTS subquery
->get();

This can generate complex SQL. An alternative is to use a more direct query, potentially with a subquery in the `SELECT` list or a `LEFT JOIN` combined with conditional aggregation or window functions (if your database supports them well).

Optimized Raw SQL (Example for PostgreSQL):

SELECT
    p.*,
    r.id AS latest_review_id,
    r.comment AS latest_review_comment,
    r.created_at AS latest_review_created_at
FROM
    products p
LEFT JOIN LATERAL (
    SELECT *
    FROM reviews rev
    WHERE rev.product_id = p.id
    ORDER BY rev.created_at DESC
    LIMIT 1
) r ON true
WHERE
    p.id IN (SELECT DISTINCT product_id FROM reviews) -- Ensures product has reviews
LIMIT 10; -- Assuming you want a limited set for LCP

You can execute this raw SQL in Laravel using DB::select(). Analyze the `EXPLAIN` output of the generated Eloquent query versus the raw SQL to determine the most performant approach for your specific database and data distribution.

Caching Strategies for Database-Intensive LCP Elements

For elements that are frequently requested and computationally expensive to fetch from the database, caching is a powerful technique to improve LCP. This is especially relevant for dynamic content that doesn’t change on every request.

Application-Level Caching

Laravel’s caching facade provides a simple API for various cache backends (Redis, Memcached, file, etc.).

If your LCP element is, for instance, a list of featured products that are updated periodically:

use Illuminate\Support\Facades\Cache;
use App\Models\Product;

// ... in your controller or service

$featuredProducts = Cache::remember('featured_products', now()->addMinutes(30), function () {
    // This closure runs only if the cache key is not found
    return Product::where('is_featured', true)->orderBy('name')->get();
});

// Use $featuredProducts to render the LCP element

The cache duration (addMinutes(30)) should be tuned based on how frequently the data actually changes and how critical it is to have the absolute latest data versus performance gains.

Database Query Caching (with caution)

Some database systems offer query caching mechanisms. However, these are often less flexible and can be harder to manage than application-level caching, especially in distributed environments. Laravel’s query builder does not have a built-in, robust query cache that persists across requests without explicit application logic.

If you are using a database like MySQL, you might consider the query cache, but be aware of its limitations:

  • It’s invalidated on any write operation to the involved tables.
  • It can consume significant memory.
  • It’s deprecated in MySQL 5.7 and removed in MySQL 8.0.

For most modern Laravel applications, relying on Redis or Memcached via Laravel’s cache facade is the recommended and more manageable approach for caching database query results.

Monitoring and Iteration

Performance optimization is an ongoing process. Regularly monitor your LCP metric using tools like Google PageSpeed Insights, WebPageTest, and browser developer tools. Set up performance budgets and alerts to catch regressions.

When LCP issues arise, systematically apply the debugging steps outlined above: identify slow queries, analyze their execution plans, optimize indexes, refactor Eloquent code, and implement caching where appropriate. Iterate on these steps, measuring the impact of each change on your LCP metric.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala