Islas en Laravel 13 Livewire 4: Guía Completa y Ejemplos de uso

Video thumbnail

Seguramente me has escuchado decir que Livewire puede ser algo abstracto, y el concepto de Islands (Islas) es el ejemplo perfecto. Es una característica nueva y poderosa, pero su utilidad depende totalmente de cómo enfoques tu desarrollo.

Para explicarlo, he preparado un ejemplo práctico apoyado en la IA, ya que simular una carga "pesada" de datos requiere un escenario específico que veremos a continuación.

¿Qué es una Isla?

En términos sencillos, una isla es una sección de tu componente que se carga de manera aislada o paralela.

  • El problema: Normalmente, si un componente tiene que hacer una consulta pesada, toda la página (o todo el componente) se queda esperando a que esa consulta termine.
  • La solución: Al envolver esa parte en una "isla", permitimos que el resto del componente cargue instantáneamente, mientras que la parte pesada se procesa de forma independiente.

Las islas son, básicamente, una forma de cargar partes pesadas de un componente de manera aislada o paralela.

Si tienes un componente muy grande, puedes dividirlo en pequeñas partes (como subcomponentes dentro de un componente), y hacer que esas partes ejecuten tareas de forma independiente.

Diferencia entre Isla y Subcomponente

Aquí entra la parte filosófica. Podrías preguntarte: "¿Por qué no crear simplemente un componente hijo?".
La diferencia es que, aunque uses componentes hijos, si estos se renderizan dentro de la solicitud inicial, pueden ralentizar el hilo principal. Las islas permiten que esas "tareas pesadas" (como propiedades computadas que consultan miles de registros) no bloqueen la carga inicial del HTML.

Estrategias de Carga (Lazy, Defer y más)

Se han simulado procesos lentos usando la función sleep(). Esto nos permite ver cómo funcionan las diferentes directivas de las islas:

  • lazy: true: La isla no se carga hasta que es visible en el navegador (ideal para elementos que requieren scroll).
  • defer: true: Carga la isla inmediatamente después de que el componente principal esté listo, sin bloquear el renderizado inicial.
  • Comportamiento por defecto: Si no usas ninguna opción, la isla se comporta como parte del componente.
  • wire:island (con nombre): Permite identificar una isla específica para refrescarla individualmente desde cualquier parte, incluso con botones fuera de la propia isla.

resources\views\pages\dashboard\island\⚡index.blade.php

<flux:card>
    <flux:heading level="2">1. Island Básico</flux:heading>
    <flux:text>Se actualiza independientemente sin re-renderizar toda la página {{ time() }}.</flux:text>
    {{-- si comentas es island se recarga TODO el componente junt con el time --}}
    @island
        <div class="p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg">
            <flux:heading level="3">Total de Posts: {{ $this->totalPosts }}</flux:heading>
            <flux:text class="mt-2">Este island usa un Computed property que se recalcula solo cuando el island se
                actualiza.</flux:text>
            <flux:button size="sm" wire:click="$refresh" class="mt-2">
                Actualizar
            </flux:button>

            <div class="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
                <p class="text-red">El tiempo {{ time() }} sera mayor y solo se calcula al momento de que LA ISLA
                    ES VISIBLE</p>
                <flux:heading level="3">Posts Recientes</flux:heading>
                <ul class="mt-2 space-y-2">
                    @foreach ($this->recentPosts as $post)
                        <li class="flex items-center gap-2">
                            <flux:badge>{{ $post->id }}</flux:badge>
                            {{ $post->title }}
                        </li>
                    @endforeach
                </ul>
            </div>

        </div>
    @endisland
</flux:card>

<div class="h-96"></div>
<div class="h-96"></div>
<div class="h-96"></div>
<flux:card>
    <flux:heading level="2">2. Island con Lista (Lazy - carga al hacer scroll)</flux:heading>
    <flux:text>Este island carga solo cuando se hace scroll y es visible en el viewport.</flux:text>


    <p class="text-red">El tiempo {{ time() }} sera el mismo que el de <strong>1. Island Básico</strong> al
        cargar la página</p>

    @island(lazy: true)
        <div class="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
            <p class="text-red">El tiempo {{ time() }} sera mayor y solo se calcula al momento de que LA ISLA ES
                VISIBLE</p>
            <flux:heading level="3">Posts Recientes (SOLO se GENERA EL LISTADO AL CARGAR LA PAGINA) </flux:heading>
            <ul class="mt-2 space-y-2">
                @foreach ($this->recentPosts as $post)
                    <li class="flex items-center gap-2">
                        <flux:badge>{{ $post->id }}</flux:badge>
                        {{ $post->title }}
                    </li>
                @endforeach
            </ul>
        </div>
    @endisland

</flux:card>

Propiedades computadas y carga diferida

Antes de entrar en el ejemplo, recuerda que las propiedades computadas suelen usarse para operaciones pesadas o repetitivas.

Se accede a ellas con this, y permiten evitar cálculos innecesarios. En este caso, las usamos para simular cargas pesadas junto con sleep.

La idea es que el componente principal cargue rápido, y luego ciertas partes se actualicen bajo demanda.

Este es la parte lógica:

resources\views\pages\dashboard\island\⚡index.blade.php

<flux:card>
    <flux:heading level="2">6. Island Hija Custom Placeholder</flux:heading>
    <flux:text>Personaliza el estado de carga con @placeholder.</flux:text>
    {{-- @livewire('dashboard.island-lazy') --}}
    @island(lazy: true)
        @placeholder
            
            {{-- @livewire('dashboard.island-lazy')  --}}
            {{-- NO SIRVE, no puedes cargar componentes en islas, carga pero bloquea 3s --}}

            <div class="animate-pulse h-24 flex justify-center items-center bg-gray-500 border rounded-2xl">Cargando comentarios...</div>
        @endplaceholder

        
            @foreach ($this->heavyPosts() as $post)
                <li class="flex items-center gap-2">
                    <flux:badge>{{ $post->id }}</flux:badge>
                    {{ $post->title }}
                </li>
            @endforeach
            <flux:button wire:click="$refresh">
                Refresh with placeholder
            </flux:button>
    @endisland

    <div class="h-64"></div>
</flux:card>

1. Ejemplo con scroll (lazy)

En este caso:

  • El componente principal carga primero
  • La isla no se carga hasta que haces scroll

resources\views\pages\dashboard\island\⚡index.blade.php

<flux:card>
    <flux:heading level="2">1. Island Básico</flux:heading>
    <flux:text>Se actualiza independientemente sin re-renderizar toda la página {{ time() }}.</flux:text>
    {{-- si comentas es island se recarga TODO el componente junt con el time --}}
    @island
        <div class="p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg">
            <flux:heading level="3">Total de Posts: {{ $this->totalPosts }}</flux:heading>
            <flux:text class="mt-2">Este island usa un Computed property que se recalcula solo cuando el island se
                actualiza.</flux:text>
            <flux:button size="sm" wire:click="$refresh" class="mt-2">
                Actualizar
            </flux:button>

            <div class="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
                <p class="text-red">El tiempo {{ time() }} sera mayor y solo se calcula al momento de que LA ISLA
                    ES VISIBLE</p>
                <flux:heading level="3">Posts Recientes</flux:heading>
                <ul class="mt-2 space-y-2">
                    @foreach ($this->recentPosts as $post)
                        <li class="flex items-center gap-2">
                            <flux:badge>{{ $post->id }}</flux:badge>
                            {{ $post->title }}
                        </li>
                    @endforeach
                </ul>
            </div>

        </div>
    @endisland
</flux:card>

Esto se puede notar con el tiempo: si tardas 5 segundos en hacer scroll, la isla mostrará un tiempo mayor.

2. Islas con nombre

También puedes asignar nombres a las islas.

Esto permite hacer cosas como:

  • Recargar una isla específica desde fuera
  • Controlar exactamente qué parte del componente se actualiza

Por ejemplo:

Un botón externo puede recargar solo una isla concreta
Las demás permanecen intactas

resources\views\pages\dashboard\island\⚡index.blade.php

<flux:card>
    <flux:heading level="2">4. Named Island con wire:island</flux:heading>
    <flux:text>Los named islands pueden ser actualizados desde cualquier lugar con wire:island. {{ time() }}</flux:text>

    @island()
        <div class="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
            <flux:heading level="3">Estadísticas (Isla sin nombre :/)</flux:heading>
            <flux:text>Posts publicados: {{ $this->totalPosts }}</flux:text>
            <flux:button wire:click="$refresh">
                U
            </flux:button>
        </div>
    @endisland

    @island(name: 'stats')
        <div class="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg my-4">
            <flux:heading level="3">Estadísticas (Isla stats)</flux:heading>
            <flux:text>Posts publicados: {{ $this->totalPosts }}</flux:text>
            <flux:button wire:click="$refresh">
                U
            </flux:button>
        </div>
    @endisland

    @island(name: 'stats2')
        <div class="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
            <flux:heading level="3">Estadísticas (Isla stats2)</flux:heading>
            <flux:text>Posts publicados: {{ $this->totalPosts }}</flux:text>
            <flux:button wire:click="$refresh">
                U
            </flux:button>
        </div>
    @endisland

    <div class="mt-4 flex flex-col gap-2">
        <flux:button wire:click="$refresh" wire:island="stats">
            Actualizar Isla con nombre stats (Estoy fuera de la isla)
        </flux:button>
        <flux:button wire:click="$refresh">
            Actualizar Stats (NO actualiza a nadie, NO esta dentro de ningula isla)
        </flux:button>
    </div>
</flux:card>

3. Ejemplo con Placeholder: La experiencia de usuario

Podemos definir un Placeholder (un "cargando...") mientras la isla procesa la información. Esto es vital para que el usuario no vea un hueco en blanco. En el ejemplo verás un mensaje de "Cargando comentarios..." que desaparece una vez los datos están listos.

resources\views\pages\dashboard\island\⚡index.blade.php

<?php

use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {

    public $loadHeavy = false;


    #[Computed]
    public function totalPosts()
    {
        // return Post::count();
        return rand(4, 100);
    }

    #[Computed]
    public function recentPosts()
    {
        // if($this->loadHeavy){
        //     sleep(3);
        // }
        return Post::inRandomOrder()->take(rand(4, 20))->get();
    }
    
    public function heavyPosts()
    {
        sleep(3);
        return Post::inRandomOrder()->take(rand(4, 20))->get();
    }

    // En tu componente Livewire/Volt
    // public function with()
    // {
    //     sleep(3); // Esto mantendrá el placeholder visible por 3 segundos
    //     return [];
    // }
};
?>

Limitaciones actuales

Es importante mencionar que, de momento, las islas tienen restricciones:

  • No aceptan componentes de Livewire dentro de ellas (solo HTML/Blade).
  • No funcionan dentro de bucles @foreach o condicionales @if de manera directa.
  • No pueden referenciar variables declaradas en el componente principal de forma dinámica.

Importante: contexto y limitaciones

Hay varios puntos importantes a tener en cuenta:

  • No puedes usar islas dentro de foreach
  • No funcionan bien con condicionales dinámicos
  • Todo debe estar contenido dentro de la isla

Esto refuerza la idea de que son una unidad aislada de renderizado.

Para mí, las islas son una herramienta para casos muy puntuales donde el rendimiento es crítico. Aunque la definición de "componente" suele ser algo pequeño y modular, las islas parecen invitarnos a crear componentes más grandes con secciones asíncronas internas. Es un enfoque inverso, pero muy interesante para optimizar el hilo principal de nuestra aplicación.

¡Te invito a descargar el código del repositorio y jugar con los tiempos de carga para que veas la diferencia en vivo!

Explora el concepto de islas en Livewire. Optimiza componentes de Laravel mediante carga diferida (defer) y paralela. Apps interactivas de alto rendimiento.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english
<script> window.addEventListener('scroll', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); window.addEventListener('mousemove', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); window.addEventListener('touchstart', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); // Fallback if no interaction window.addEventListener('load', function() { setTimeout(function() { if (!window.scriptsLoaded) loadThirdPartyScripts(); }, 8000); }); function loadThirdPartyScripts() { if (window.scriptsLoaded) return; window.scriptsLoaded = true; console.log('Loading third party scripts...'); // Google Analytics var gtagScript = document.createElement('script'); gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-F22688T9RL'; gtagScript.async = true; document.head.appendChild(gtagScript); gtagScript.onload = function() { window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-F22688T9RL'); }; // Google ADS const adScript = document.createElement('script'); adScript.src = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"; adScript.setAttribute('data-ad-client', 'ca-pub-5280469223132298'); adScript.async = true; document.head.appendChild(adScript); // Facebook Pixel (function(f, b, e, v, n, t, s) { if (f.fbq) return; n = f.fbq = function() { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments) }; if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []; t = b.createElement(e); t.async = !0; t.src = v; s = b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t, s); })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '1643487712945352'); fbq('track', 'PageView'); } </script> <noscript> <img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1643487712945352&ev=PageView&noscript=1" /> </noscript>