Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

ComponentFactory::make() declares a new anonymous class on every call (memory leak)

Aperta
#157 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
72/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Tranquilla
Stack tecnologico
php

Direzione di ricerca

Inizia da src/ComponentFactory.php e segui entrambi i percorsi di make() attraverso requirePath() e il require del file compilato. Riproduci il problema con lo scenario VoltLeakTest, verificando le classi dichiarate durante rendering ripetuti e con entrambe le API. Il lavoro è completato quando la risoluzione ripetuta di un componente invariato non dichiara più un'altra classe anonima durante il rendering, il rendering continua a funzionare e le modifiche al componente vengono rilevate.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Volt Version

1.10.5 — src/ComponentFactory.php is byte-identical in v1.11.2, so this is unfixed on the current release

Laravel Version

13.24.0 (livewire/livewire 3.7.15, pestphp/pest 4.4.5 / PHPUnit 12.5.16)

PHP Version

8.4.16, no OPcache in CLI

Database Driver & Version

SQLite :memory: (not relevant to the issue)

Description

ComponentFactory::make() re-requires the component file on every call, so every call declares a new anonymous class. PHP never frees class entries, and each leaked class pins a CompileContext through its static $__context. Anything that resolves the same Volt component more than once in a single PHP process accumulates them.

In src/ComponentFactory.php both branches use a plain require, so both the class API and the functional API are affected:

// class API — requires the .blade.php source, which contains `new class extends Component`
$this->requirePath(CompileContext::instance()->path = $path);   // -> require $__path;

// functional API — requires the compiled file, which also contains `new class extends Component`
require $file->path();

Livewire does cache the resolution, but in Livewire\Mechanisms\ComponentRegistry::$aliases — an instance property of a container singleton. A new container means a cold cache, which means another make(), which means another class.

The most visible victim is the test suite, because Laravel builds a fresh container per test: every test leaks one class per Volt component it renders.

Measured impact

A private application with 108 Volt components and a ~4,000-test Feature suite (serial, single process):

peak memory declared classes
as-is 509 MB 6,193
with make() memoized 405 MB 4,749

1,552 leaked anonymous Volt classes, ~110 MB — about 22 % of the suite's peak memory. The suite had started dying with Allowed memory size of 536870912 bytes exhausted at seemingly random points, which is how we found this: the process sits right at the limit, so the failure point moves with test order and no single test is at fault.

For the measurement I pre-declared a Livewire\Volt\ComponentFactory identical to the shipped one plus a process-wide memo keyed on $componentName.'|'.$path. Nothing else changed, all tests stayed green.

Suggested fix

Memoize the resolved class name in ComponentFactory:

protected static array $resolved = [];

public function make(string $componentName, string $path): string
{
    $key = $componentName.'|'.$path.'|'.@filemtime($path);

    return static::$resolved[$key] ??= $this->makeFresh($componentName, $path);
}

Including filemtime($path) in the key keeps local development working: editing a component still yields a fresh class, it just stops re-declaring one for an unchanged file.

Note that swapping require for require_once is not sufficient on its own — on the second call the file would not execute, static::$latestCreatedComponentClass would stay null, and make() would fall through (class API) or return null (functional API). The class name has to be remembered explicitly.

One open question

The same reasoning should apply to any long-running process that resolves Volt components against more than one container instance — Octane sandboxes the container per request, so a ComponentRegistry resolved inside the sandbox would start cold on every request. I have only measured the test-suite case and have not confirmed that, but it seemed worth flagging.

Steps To Reproduce
  1. Create resources/views/livewire/counter.blade.php:
<?php

use Livewire\Volt\Component;

new class extends Component
{
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
}; ?>

<div>
    <span>{{ $count }}</span>
    <button wire:click="increment">+</button>
</div>
  1. Add a test that renders it repeatedly — one PHP process, a fresh container per test:
use Livewire\Volt\Volt;
use PHPUnit\Framework\Attributes\DataProvider;

final class VoltLeakTest extends \Tests\TestCase
{
    public static function iterations(): array
    {
        return array_map(fn (int $i) => [$i], range(1, 20));
    }

    #[DataProvider('iterations')]
    public function test_rendering_the_same_component_declares_a_new_class_every_time(int $i): void
    {
        $before = get_declared_classes();

        Volt::test('counter')->assertSee('0');

        $new = array_values(array_diff(get_declared_classes(), $before));

        fwrite(STDERR, sprintf("test %2d: +%d class | %s\n", $i, count($new), implode(', ', $new) ?: '-'));

        $this->assertTrue(true);
    }
}
  1. Run it. Every test after the first declares exactly one more anonymous class for the same unchanged component (paths shortened):
test  2: +1 class | Livewire\Volt\Component@anonymous .../views/counter.blade.php:5$2fe
test  3: +1 class | Livewire\Volt\Component@anonymous .../views/counter.blade.php:5$2ff
test  4: +1 class | Livewire\Volt\Component@anonymous .../views/counter.blade.php:5$300
...
test 19: +1 class | Livewire\Volt\Component@anonymous .../views/counter.blade.php:5$30f
test 20: +1 class | Livewire\Volt\Component@anonymous .../views/counter.blade.php:5$310

One component, one identical render, a fresh class every single time.

Lingua principale
PHP
Stelle
422
Fork
35
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di livewire/volt

Tutte le issue di livewire/volt

Issue simili

Altre issue su PHP

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.