Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン
#157 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
72/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
静か
技術スタック
php

調査の方向性

src/ComponentFactory.php から始め、requirePath() とコンパイル済みファイルの require を通る make() の両方の経路を追跡します。VoltLeakTest シナリオで問題を再現し、繰り返しレンダリングした場合と両方の API で宣言されたクラスを確認します。変更されていないコンポーネントを繰り返し解決しても、レンダリング中に別の匿名クラスが宣言されず、レンダリングが引き続き正常に動作し、コンポーネントへの変更が検出されれば完了です。

索引モデルが issue の本文から書いたものです。

説明

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.

主要言語
PHP
スター
422
フォーク
35
PR マージ指標
30日以内にマージされた PR はありません

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

livewire/volt のほかの issue

livewire/volt の issue をすべて見る

似ている issue

PHP の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。