DEV Community

Cover image for Blade gets slow when your views keep doing the same work
Saqueib Ansari
Saqueib Ansari

Posted on • Originally published at qcode.in

Blade gets slow when your views keep doing the same work

Most Laravel Blade performance optimization work starts in the wrong place.

Teams blame Blade when pages feel slow, but Blade is usually just exposing bigger architectural habits: too much conditional rendering, too many nested components, repeated partial evaluation, and data shaping that happens far too late. The fix is rarely a clever micro-optimization. It is usually about rendering less, preparing data earlier, and being more selective about what the view layer is responsible for.

So here is the recommendation up front: treat Blade like a thin rendering layer, not a mini application runtime. The more logic, branching, and repeated work you push into templates, the more large pages will drag as your app grows.

This article takes a tutorial-style path because that is the most useful shape for this topic. Start with the simple baseline, identify where over-rendering actually comes from, then tighten the view layer step by step until Blade becomes cheap again.

Start by fixing the real cause: over-rendering is usually repeated work in disguise

When developers say a Blade page is slow, they often mean one of three things.

The first is that the page executes too much logic while deciding what to show. The second is that it repeats expensive partials or components many times in loops. The third is that the data is not shaped properly before it reaches the template, so the template keeps doing tiny bits of work across a large tree.

That is why large apps feel this problem more than small apps. A few @if branches are harmless. A few Blade components are harmless. A partial inside a loop is harmless. But once you mix all three across dashboards, admin tables, notifications, sidebars, modals, and role-based UI fragments, the view layer stops being a thin presentation concern and starts acting like a low-visibility execution engine.

A simple Blade file can quietly become the place where you:

  • branch on permissions repeatedly
  • inspect relationships repeatedly
  • render nested components hundreds of times
  • compute state labels and CSS classes repeatedly
  • include partials that include other partials that include other partials

That is not a Blade feature problem. It is a rendering-discipline problem.

A bad baseline often looks like this:

@foreach ($users as $user)
    <x-user-row :user="$user">
        @if($user->team && $user->team->is_active)
            <x-badge color="green">Active Team</x-badge>
        @endif

        @if($user->orders->count() > 10)
            <x-badge color="blue">VIP</x-badge>
        @endif

        @can('impersonate', $user)
            @include('admin.users.actions.impersonate', ['user' => $user])
        @endcan
    </x-user-row>
@endforeach
Enter fullscreen mode Exit fullscreen mode

This reads nicely. It can still perform badly in a large list because it combines relationship access, policy checks, nested components, and partial includes at per-row scale.

The first fix is not “rewrite Blade.” It is reduce repeated decisions inside the template.

Move view decisions upstream before you optimize syntax

The biggest improvement in large Blade codebases usually comes from one habit: precomputing view state before the template renders.

A Blade template should not be figuring out business meaning row by row if the controller, action class, view model, or resource transformer can do it once.

Instead of asking Blade to decide whether a user is VIP, has an active team, or can be impersonated on every pass through a loop, shape that state in advance.

For example:

$users = $users->map(function ($user) use ($currentAdmin) {
    return [
        'id' => $user->id,
        'name' => $user->name,
        'team_name' => $user->team?->name,
        'has_active_team' => (bool) $user->team?->is_active,
        'is_vip' => $user->orders_count > 10,
        'can_impersonate' => $currentAdmin->can('impersonate', $user),
    ];
});

return view('admin.users.index', compact('users'));
Enter fullscreen mode Exit fullscreen mode

Then the Blade becomes much flatter:

@foreach ($users as $user)
    <x-user-row :user="$user">
        @if($user['has_active_team'])
            <x-badge color="green">Active Team</x-badge>
        @endif

        @if($user['is_vip'])
            <x-badge color="blue">VIP</x-badge>
        @endif

        @if($user['can_impersonate'])
            @include('admin.users.actions.impersonate', ['user' => $user])
        @endif
    </x-user-row>
@endforeach
Enter fullscreen mode Exit fullscreen mode

This does not just make the page faster. It makes it easier to reason about.

That tradeoff is worth calling out. Some developers resist this pattern because it feels like “moving presentation logic out of the view.” Good. In large apps, that is usually the right move. Blade should decide how to present prepared state, not rediscover that state at render time.

If you want structure around this, view models or small presenter-style classes are often a better long-term choice than stuffing more logic into controllers.

Components help until they become a rendering tax

Blade components are great for consistency, but they are not free.

This is where large Laravel apps get into trouble. Teams rightly standardize on components, then start nesting them everywhere because the ergonomics feel good. A table row becomes a component. Each cell becomes a component. Status becomes a component. Dropdown actions become a component. Empty wrappers become components. Eventually one index page is made from hundreds or thousands of component instances.

That cost is real.

A useful rule is this: use components for design-system consistency and composability, not for every tiny fragment of markup.

A component is usually worth it when:

  • it centralizes repeated UI behavior
  • it wraps meaningful rendering logic
  • it enforces consistency across the app
  • it would otherwise create duplicated, fragile markup

A component is often not worth it when:

  • it is just one div with two classes
  • it is rendered hundreds of times per request
  • it adds another level of nesting without real reuse value
  • it mostly forwards props to another component

For example, this is usually fine:

<x-alert type="warning" :dismissible="true">
    Billing details are incomplete.
</x-alert>
Enter fullscreen mode Exit fullscreen mode

This is where things start getting silly in large loops:

<x-table.row>
    <x-table.cell>
        <x-text.muted>{{ $user->email }}</x-text.muted>
    </x-table.cell>
</x-table.row>
Enter fullscreen mode Exit fullscreen mode

If that pattern repeats across 500 rows with nested conditional content, you are paying a rendering tax for abstraction purity.

My recommendation is opinionated here: for dense, repeated UI like tables, audit whether some components should collapse back into direct Blade or a simpler partial. Design-system discipline is good. Component maximalism is not.

Laravel’s official Blade docs are worth revisiting because the framework gives you several rendering primitives, not just one style of componentization: https://laravel.com/docs/blade

Cache fragments where the page is structurally repetitive

Once you have reduced repeated logic and trimmed unnecessary component depth, the next lever is selective caching.

This is where teams often hesitate because they imagine stale UI bugs. That fear is reasonable, but it should not stop you from caching obviously stable fragments.

Not everything on a page changes at the same rate.

A large admin layout may have:

  • a mostly static sidebar
  • role-scoped navigation that changes rarely
  • summary cards that change every minute or five minutes
  • a table body that changes frequently
  • small status badges that are cheap enough not to care about

Treating the whole page as equally dynamic is wasteful.

A good pattern is to cache stable fragments aggressively and leave the volatile parts uncached.

For example:

@php
    $navCacheKey = 'admin.nav.'.auth()->id().'.'.app()->getLocale();
@endphp

@cache($navCacheKey, now()->addMinutes(10))
    @include('admin.partials.sidebar-nav')
@endcache
Enter fullscreen mode Exit fullscreen mode

Or for role-based dashboards:

@cache('dashboard.summary.'.auth()->user()->role, now()->addMinutes(2))
    @include('dashboard.partials.summary-cards', ['stats' => $stats])
@endcache
Enter fullscreen mode Exit fullscreen mode

The exact syntax depends on how you structure fragment caching in your app or package stack, but the principle is stable: cache repeated view work where staleness tolerance is acceptable.

The failure mode to avoid is caching before you understand invalidation. If the UI is permission-sensitive, tenant-sensitive, or locale-sensitive, your cache key must reflect that. Otherwise you trade render cost for correctness bugs, which is not an upgrade.

A simple decision rule helps:

  • if the fragment is expensive and changes slowly, cache it
  • if it is cheap and highly dynamic, render it directly
  • if it is expensive and highly dynamic, redesign the page shape or data flow

Nested conditionals are often a sign the page wants view states, not more Blade

One of the most common sources of Blade sprawl in large apps is conditional branching that grows organically over time.

A view starts with one @if. Then another for role checks. Then a branch for feature flags. Then a branch for tenant rules. Then an empty state. Then a loading or syncing banner. Soon the template is full of nested decisions that are technically correct and painful to maintain.

This kind of code is a warning sign:

@if($user->is_admin)
    @if(feature('advanced-billing'))
        @if($account->hasActiveSubscription())
            @include('billing.partials.admin-active')
        @else
            @include('billing.partials.admin-inactive')
        @endif
    @endif
@endif
Enter fullscreen mode Exit fullscreen mode

The problem is not just readability. Branch-heavy Blade often means the page is trying to encode a state machine informally.

A better approach is to surface explicit view states earlier.

$billingView = match (true) {
    ! $user->is_admin => 'hidden',
    ! feature('advanced-billing') => 'hidden',
    $account->hasActiveSubscription() => 'admin_active',
    default => 'admin_inactive',
};

return view('settings.billing', compact('billingView', 'account'));
Enter fullscreen mode Exit fullscreen mode

Then Blade becomes much cleaner:

@if($billingView === 'admin_active')
    @include('billing.partials.admin-active')
@elseif($billingView === 'admin_inactive')
    @include('billing.partials.admin-inactive')
@endif
Enter fullscreen mode Exit fullscreen mode

This pattern is especially useful in large settings pages, dashboards, and tenant-aware admin surfaces where conditional sprawl accumulates fast.

The key idea is simple: if the view has too many branches, the state is under-modeled.

Large loops need cheaper rendering primitives and better data contracts

The places where Blade performance hurts most are usually predictable:

  • index tables
  • activity feeds
  • audit logs
  • nested navigation trees
  • comment threads
  • permission-heavy admin grids

These are the places where tiny inefficiencies multiply. A per-item policy check, a nested component, an included partial, or a relationship access that felt harmless at 20 rows becomes expensive at 500.

This is where you need to be practical.

First, trim the data contract. Do not pass full models with a dozen relationships into a dense loop if the template only needs six fields and two booleans.

Second, prefer simpler rendering primitives in repeated UI. Not everything needs to be a component.

Third, avoid performing authorization, formatting, or business classification repeatedly inside the loop if you can precompute it once.

A good “dense list” preparation pattern often looks like this:

$rows = User::query()
    ->select(['id', 'name', 'email', 'status', 'last_login_at'])
    ->withCount('orders')
    ->get()
    ->map(fn ($user) => [
        'name' => $user->name,
        'email' => $user->email,
        'status_label' => $user->status === 'active' ? 'Active' : 'Disabled',
        'is_vip' => $user->orders_count > 10,
        'last_login_human' => optional($user->last_login_at)?->diffForHumans(),
    ]);
Enter fullscreen mode Exit fullscreen mode

Then the Blade loop can stay boring, which is exactly what you want.

There is a tradeoff here. Some teams worry this approach moves too much formatting out of Blade. That concern is valid if you go too far. But large apps benefit from data prepared for rendering rather than raw objects dumped into the template and left to fend for themselves.

What to change in a real codebase this week

If you want practical progress instead of abstract advice, audit one slow Blade-heavy page using this order.

Start by asking where the repeated work is happening. Look for nested components, deep includes, relationship access in loops, repeated policy checks, and conditionals that branch three or four layers deep.

Then make changes in this sequence:

  1. Move repeated decisions upstream into prepared view state.
  2. Flatten overly nested components in dense repeated UI.
  3. Replace branch-heavy templates with explicit state mapping.
  4. Cache stable fragments with keys that include role, tenant, locale, or other relevant scope.
  5. Reduce the amount of raw model data flowing into large loops.

You do not need a giant rewrite to see improvement. Large Blade pages often get noticeably faster from just a few disciplined changes.

The practical decision rule is simple: if a Blade file keeps making the same decision or building the same structure hundreds of times, that work probably belongs somewhere else.

Blade is at its best when it is boring. When the template becomes a maze of components, includes, conditionals, and repeated state checks, performance is usually only one of the problems. The bigger issue is that the rendering layer has taken on too much responsibility.

Fix that, and page speed usually improves along with maintainability.


Read the full post on QCode: https://qcode.in/laravel-blade-performance-hacks-avoiding-over-rendering-in-large-apps/

Top comments (0)