Skip to content

Generated HTML too large #2370

@mcfriend99

Description

@mcfriend99

Flux version

v2.11.1

Livewire version

v4.0

Tailwind version

v4.0.7

Browser and Operating System

Chrome and Firefox on Linux and MacOS

What is the problem?

Let me start by saying that this is a completely different issue from #740, #1048 and #1075.

After upgrading to v2.11.0 (and subsequently v2.11.1), we noticed a significant increase in the size of generated html.

We have a simple page following the default laravel livewire backend which consists of a flux:select of type listbox that has just one option, a flux:input acting as a search box, and 5 column table that consists of 6 rows with the first column containing a flux:checkbox.

This page is generating raw html output of over 5.2mb in size (confirmed via browser network logs and simple curl requests). We suspected something was fishy and decided to downgrade back to v2.9.0 which we were using before, the generated html dropped down to 591kb. While both versions are fairly large for the contents of the page, the size different is phenomenal.

The problem here is not really the load time itself as that is consequential to the size of the page being rendered, but the size of the page itself. It really alarming.

I've provided the actual code snippets without any redaction to enable as much insight as will be needed to resolve the issue (I have sufficient authority on the project to do this, but I might slightly redact the codes this issue once its closed though).

Code snippets to replicate the problem

Main component

<?php

use Livewire\WithPagination;
use Livewire\Attributes\{Computed, On, Url};
use Flux\Flux;
use App\Models\Zone;
use App\View\ToastableComponent;
use App\Concerns\SortablePage;

new class extends ToastableComponent {
    use WithPagination, SortablePage;

    public ?int $editId = null;
    public string $editName = '';
    public ?int $editStateId = null;

    #[Url(as: 'search')]
    public ?string $search = null;
    #[Url(as: 'country')]
    public ?int $filterCountryId = null;
    #[Url(as: 'state')]
    public ?int $filterStateId = null;

    public ?array $bulkSelections = [];

    /**
     * Get paginated zones with sorting
     */
    #[Computed]
    public function zones()
    {
        return Zone::query()
            ->with('state.country')
            ->withCount('districts')
            ->where('user_id', auth()->id())
            ->when(
                $this->search,
                fn($query) =>
                $query->where('name', 'like', "%{$this->search}%")
            )
            ->when(
                $this->filterCountryId,
                fn($query) =>
                $query->whereHas(
                    'state.country',
                    fn($query) =>
                    $query->where('id', $this->filterCountryId)
                )
            )
            ->when(
                $this->filterStateId,
                fn($query) =>
                $query->where('state_id', $this->filterStateId)
            )
            ->orderBy($this->sortBy, $this->sortDirection)
            ->paginate(config('site.page_size'));
    }

    /**
     * Navigate to zone view page
     */
    public function viewZone(int $id): void
    {
        $this->redirect(route('locations.district', $id), navigate: true);
    }

    /**
     * Open edit modal for zone
     */
    public function updateZone(int $id): void
    {
        $zone = Zone::select(['id', 'name', 'state_id', 'user_id'])
            ->findOrFail($id);

        $this->authorizeZoneAccess($zone);

        $this->editId = $id;
        $this->editName = $zone->name;
        $this->editStateId = $zone->state_id;

        $this->dispatch('edit-zone', id: $id);
        Flux::modal('edit-zone')->show();
    }

    /**
     * Delete a zone
     */
    public function deleteZone(int $id): void
    {
        $zone = Zone::withCount('districts')
            ->findOrFail($id);

        if ($zone->districts_count > 0) {
            $this->fail('This zone has districts and cannot be deleted without moving those districts to another zone.');
            return;
        }

        $this->authorizeZoneAccess($zone);

        $name = $zone->name;
        $zone->delete();

        $this->congrats('Zone "' . $name . '" deleted successfully.');
        $this->dispatch('zone-deleted', zone: $zone);
    }

    public function deleteSelectedZones(): void
    {
        if (empty($this->bulkSelections))
            return;

        $zones = Zone::withCount('districts')
            ->findMany($this->bulkSelections);

        $deleted = 0;
        $deletables = $zones->count();

        foreach ($zones as $zone) {
            if ($zone->districts_count > 0) {
                $this->fail('Zone "' . $zone->name . '" has districts and cannot be deleted without moving those districts to another zone');
                continue;
            }

            $this->authorizeZoneAccess($zone);
            $zone->delete();
            $deleted++;
        }

        if ($deleted > 0) {
            if ($deleted === $deletables) {
                $this->congrats('Successfully deleted ' . $deleted . ' ' . $this->plural('record', $deleted));
            } else {
                $this->congrats('Successfully deleted ' . $deleted . ' of ' . $deletables . ' ' . $this->plural('record', $deleted));
            }

            $this->dispatch('zone-deleted', zone: $zone);
        }
    }

    /**
     * Authorize zone access
     */
    protected function authorizeZoneAccess(Zone $zone): void
    {
        if ($zone->user_id !== auth()->id()) {
            $this->fail('You do not have permission to modify this zone.');
            throw new \Illuminate\Auth\Access\AuthorizationException();
        }
    }

    /**
     * Refresh zones list when created or updated
     */
    #[On('zone-created')]
    #[On('zone-updated')]
    public function refreshZones(): void
    {
        unset($this->zones);
    }
}; 
?>

<div>
    <x-pages::locations.layout :title="__('Locations - Zone')">
        <div class="w-full flex items-end">
            <flux:heading size="xl">{{ __('Zones') }}</flux:heading>
            <flux:spacer />
            <flux:modal.trigger name="add-zone">
                <flux:button icon="plus" variant="primary">{{ __('New Zone') }}</flux:button>
            </flux:modal.trigger>
        </div>

        <div class="flex py-6 gap-4 border-b border-b-2 border-zinc-200 dark:border-zinc-700">
            <div class="flex flex-row gap-2">
                <x-bulk-actions on-delete="deleteSelectedZones" />
            </div>

            <flux:spacer />

            <div class="flex flex-row gap-2">
                <livewire:filters.country wire:model.live="filterCountryId" key="country-filter" :has-members="true" />
                <livewire:filters.state :country="$filterCountryId" wire:model.live="filterStateId" key="state-filter"
                    :has-members="false" />

                <flux:input icon="magnifying-glass" wire:model.live="search" clearable class="min-w-72!"
                    placeholder="{{ __('Search zones...') }}" key="search-input" />
            </div>
        </div>

        <flux:checkbox.group wire:model="bulkSelections">
            <flux:table class="" :paginate="$this->zones">
                <flux:table.columns sticky class="bg-white dark:bg-zinc-800">
                    <flux:table.column sticky class="bg-white dark:bg-zinc-800">
                        <flux:checkbox.all />
                    </flux:table.column>

                    <flux:table.column class="font-medium" sortable :sorted="$sortBy === 'name'"
                        :direction="$sortDirection" wire:click="sort('name')">
                        {{ __('Name') }}
                    </flux:table.column>

                    <flux:table.column class="font-medium" sortable :sorted="$sortBy === 'districts_count'"
                        :direction="$sortDirection" wire:click="sort('districts_count')">
                        {{ __('Districts') }}
                    </flux:table.column>

                    <flux:table.column class="font-medium" sortable :sorted="$sortBy === 'state_id'"
                        :direction="$sortDirection" wire:click="sort('state_id')">
                        {{ __('State') }}
                    </flux:table.column>

                    <flux:table.column class="font-medium" sortable :sorted="$sortBy === 'country_id'"
                        :direction="$sortDirection" wire:click="sort('country_id')">
                        {{ __('Country') }}
                    </flux:table.column>

                    <flux:table.column class="font-medium" sortable :sorted="$sortBy === 'created_at'"
                        :direction="$sortDirection" wire:click="sort('created_at')">
                        {{ __('Created') }}
                    </flux:table.column>

                    <flux:table.column></flux:table.column>
                </flux:table.columns>

                <flux:table.rows>
                    @foreach ($this->zones as $zone)
                        <flux:table.row :key="$zone->id">
                            <flux:table.cell sticky class="bg-white dark:bg-zinc-800">
                                <flux:checkbox :value="$zone->id" />
                            </flux:table.cell>

                            {{-- TODO: Change this to go to zone overview when its implemented --}}
                            <flux:table.cell>
                                <a href="{{ route('locations.district', $zone->id) }}" wire:navigate>
                                    {{ $zone->name }}
                                </a>
                            </flux:table.cell>

                            <flux:table.cell>{{ $zone->districts_count }}</flux:table.cell>

                            <flux:table.cell>{{ $zone->state->name }}</flux:table.cell>

                            <flux:table.cell>{{ $zone->country->name }}</flux:table.cell>

                            <flux:table.cell class="whitespace-nowrap">
                                {{ $zone->created_at->diffForHumans() }}
                            </flux:table.cell>

                            <flux:table.cell>
                                <x-list-actions :id="$zone->id" on-view="viewZone" on-edit="updateZone"
                                    on-delete="deleteZone" />
                            </flux:table.cell>
                        </flux:table.row>
                    @endforeach
                </flux:table.rows>
            </flux:table>
        </flux:checkbox.group>
    </x-pages::locations.layout>
</div>

The livewire:filters.country component

<?php

use Livewire\Component;
use Livewire\Attributes\{Modelable, Computed, Reactive};
use App\Models\Country;

new class extends Component {
    #[Modelable]
    public ?int $value;

    #[Reactive]
    public ?string $placeholder = 'Filter by country...';

    #[Reactive]
    public ?bool $hasMembers = true;

    #[Computed]
    public function countries()
    {
        return Country::query()
            ->when($this->hasMembers, function ($query) {
                return $query->has('states.zones');
            })
            ->get();
    }
};
?>

@php
    $classes = Flux::classes();
@endphp

<flux:select variant="listbox" searchable clearable wire:model="value" placeholder="{{ $placeholder }}" {{ $attributes->class($classes) }}>
    @foreach ($this->countries as $country)
        <flux:select.option :value="$country->id" :key="$country->id">{{ $country->name }}</flux:select.option>
    @endforeach
</flux:select>

The livewire:filters.state component is similar to the country component. The main difference is the computes using State model instead of Country model.

The x-bulk-action component

@blaze

@props([
    'onDelete' => '',
])

@php
    $classes = Flux::classes();
@endphp

<flux:dropdown position="bottom" align="start" {{ $attributes->class($classes) }}>
    <flux:button icon:trailing="chevron-down">Bulk Actions</flux:button>

    <flux:menu>
        @if($slot->isNotEmpty())
            @if($onDelete)
                <flux:menu.separator />
            @endif

            {{ $slot }}
        @endif

        @if($onDelete)
            <flux:menu.item wire:click="{{ $onDelete }}" icon="trash" variant="danger">
                {{ __('Delete') }}
            </flux:menu.item>
        @endif
    </flux:menu>
</flux:dropdown>

The x-list-actions component

@blaze

@props([
    'onView' => '',
    'onEdit' => '',
    'onDelete' => '',
    'id' => null,
])

@php
    $classes = Flux::classes();
@endphp

<flux:dropdown position="bottom" align="end" {{ $attributes->class($classes) }}>
    <flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" inset="top bottom" />

    <flux:menu>
        @if($onView && $id)
            <flux:menu.item wire:click="{{ $onView }}({{ $id }})">
                {{ __('View') }}
            </flux:menu.item>
        @endif
        @if($onEdit && $id)
            <flux:menu.item wire:click="{{ $onEdit }}({{ $id }})">
                {{ __('Edit') }}
            </flux:menu.item>
        @endif
        @if($onDelete && $id)
            <flux:menu.submenu heading="{{ __('Delete') }}">
                <flux:menu.item disabled class="text-xs font-semibold text-zinc-500">
                    {{ __('Are you sure?') }}
                </flux:menu.item>

                <flux:menu.separator />

                <flux:menu.item variant="danger" wire:click="{{ $onDelete }}({{ $id }})">{{ __('Yes') }}</flux:menu.item>
                <flux:menu.item>{{ __('No') }}</flux:menu.item>
            </flux:menu.submenu>
        @endif

        @if($slot->isNotEmpty() && ($onView || $onEdit || $onDelete))
            <flux:menu.separator />
        @endif

        {{ $slot }}
    </flux:menu>
</flux:dropdown>

The x-pages::locations.layout is basically a wrapper that wraps the page in a div to add tailwind styles the page (and its related pages) as center aligned so it doesn't add anything important to the issue.

All in all, nothing extra-ordinary.

Screenshots/ screen recordings of the problem

Unable to provide screenshots and recordings.

How do you expect it to work?

Generate considerably sized html output.

Please confirm (incomplete submissions will not be addressed)

  • I have provided easy and step-by-step instructions to reproduce the bug.
  • I have provided code samples as text and NOT images.
  • I understand my bug report will be closed if I haven't met the criteria above.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions