ComponentFactory::make() declares a new anonymous class on every call (memory leak)
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
- Ambito
- backend, performance
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
- 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>
- 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);
}
}
- 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
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di livewire/volt
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 48/100
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 45/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 38/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
Tutte le issue di livewire/volt
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
grokability/snipe-it#19869 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
sync-en
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
sync-en
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 77/100
I maintainer di solito rispondono entro 3 giorni
-
Перевод устарел
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 74/100