TECH PLAY

macOS

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

はじめに こんにちは。リテールハブ開発部小売アプリチームの池です。 業務で Laravel Octane のメモリが残る挙動について調査する機会がありました。 Laravel Octane は、長時間稼働するプロセス上で Laravel アプリケーションを動かして高速化するツールです。便利な一方で、プロセスが長く生きるためメモリが残り続け、書き方次第ではリクエスト間で状態が引き継がれてしまうという、従来の Nginx + PHP-FPM 構成の Laravel では発生しにくい特性を持っています。この特性を理解せずに使うと予期しない事故につながる可能性があると感じました。 そこで本記事では、Octane + Swoole の仕組みを整理した上で、サンプルプログラムで挙動を検証し、Worker プロセスが常駐することに起因して気をつけるべきポイントについて整理したいと思います。 Laravel および Octane について多少の知識がある方を前提に書いており、Laravel 本体の解説等には触れません。 なお、本記事の内容は一次情報から確認するように努めていますが、私の理解違いや Octane / Swoole のバージョン差による挙動の違いが含まれている可能性があります。誤って実装すると事故につながり得る領域でもあるため、最終的にはご自身でソースコードや公式ドキュメントをご確認の上で適用ください。 仕組みの整理 この章では、Octane + Swoole で前のリクエストの情報が次のリクエストに残る仕組みを、次の 4 つの観点から整理します。 Octane + Swoole では Worker プロセスが常駐するため、PHP プロセス内のメモリがリクエストごとに初期化されない Octane はリクエストごとに $this->app を clone して $sandbox 上で処理することで、ベースのアプリインスタンスを直接書き換えないようにしている ただし PHP の clone はシャローコピーなので、共有されたオブジェクトの内部状態はリクエスト間で残り得る Octane は RequestReceived イベントに紐づくリスナー群でフレームワーク側の状態をリセットしている 本記事に登場するアーキテクチャ図やライフサイクル図は必要な要素に絞って簡略化しています。 1. Worker プロセスが常駐するため、メモリは初期化されない Octane + Swoole は、長時間生きる PHP プロセスを立ち上げる仕組みです。 Swoole のプロセスアーキテクチャ Worker は OS プロセスです。Manager から複数生成され、一定数のリクエストを処理するか停止シグナルを受信するまで走り続けます。 Worker のライフサイクル 長時間生き続ける Worker プロセスは次のように動きます。 注目したいのは、1 つの Worker プロセスが生き続けたまま複数のリクエストを順に処理し続ける構造です。Worker 起動時に 1 回だけ Laravel アプリケーションを組み立てて $this->app に保持し、その後はリクエストのたびにこの $this->app を clone して $sandbox として使い回します。この「同じ $this->app が複数リクエストにまたがって使われる」点が、後で見る「前のリクエストの情報が次のリクエストに残ってしまう」仕組みの一部になっています。 Worker 起動時に Laravel アプリケーションを組み立てている処理 Worker::boot() の実装は以下の通りです。 <?php // vendor/laravel/octane/src/Worker.php public function boot ( array $ initialInstances = []) : void { // ベースとなる Laravel アプリインスタンスを1つ生成 // 以降、リクエストのたびにここから clone する $ this -> app = $ app = $ this -> appFactory -> createApplication ( ... ) ; $ this -> dispatchEvent ( $ app , new WorkerStarting ( $ app )) ; } なぜメモリが残るのか Swoole + Octane では以下の仕組みでメモリが残ります。 Worker プロセスが生き続けるため、リクエスト終了時に変数・オブジェクトに割り当てられたメモリが解放されない 結果として、 $this->app を含む変数・オブジェクトがリクエストをまたいでメモリに残り続ける ここから先は、この「メモリに残り続ける $this->app をリクエストごとにどう扱うか」ということに焦点を当てます。 2. Octane はリクエストごとに $this->app を clone する Worker が常駐すれば $this->app も残ります。ただ、リクエスト処理の中で $this->app を直接書き換えてしまうと、その変更が次のリクエストにそのまま残ります。Octane はこれを避けるために、リクエストのたびに $this->app を clone して $sandbox を作り、その上でリクエスト処理を回す設計になっています。 <?php // vendor/laravel/octane/src/Worker.php public function handle ( Request $ request , RequestContext $ context ) : void { // アプリインスタンスを clone してリクエスト用の sandbox を作る CurrentApplication :: set ( $ sandbox = clone $ this -> app ) ; $ gateway = new ApplicationGateway ( $ this -> app, $ sandbox ) ; try { $ response = $ gateway -> handle ( $ request ) ; // ... レスポンス返却 ... } finally { $ sandbox -> flush () ; // sandbox 側の bindings クリア unset ( $ gateway , $ sandbox , ... ) ; CurrentApplication :: set ( $ this -> app ) ; // 元のアプリインスタンスに戻す } } ここでの clone の役割は、リクエスト処理を $sandbox 側に隔離して、ベースの $this->app を直接書き換えないようにすることです。 $this->app 自体は Worker 寿命までずっと生き続けますが、毎リクエストの処理が $this->app を直接書き換えなければ、結果としてベースを変更せずに使い回せる、という設計になっています。 3. clone はシャローコピーなので、内部状態はリクエスト間で残り得る ただし、PHP の clone はシャローコピーであるため、前節の「ベースを書き換えない」が成り立つ範囲には限界があります。 Application オブジェクト自体は新規(リクエストごとの器) 配列プロパティ( bindings / instances など)はコピーされる(sandbox 側で書き換えても元には反映されない) 配列の中身(実際のオブジェクト)は元のアプリ $this->app と $sandbox で共有される これにより、以下のような挙動になります。 例えばリクエスト中に app()->instance('request', $req) のように差し替えても、 $sandbox 側にのみ反映され、ベースの $this->app には反映されない 一方で、ベースの $this->app に登録された解決済み singleton インスタンスは両者で共有されたまま 「singleton にリクエスト固有データを入れると次のリクエストにも残る」という現象は、この「clone してもオブジェクト自体は共有される」ことが要因と考えられます。 clone はコンテナの配列レベルでの隔離は提供するものの、配列の中に入っているオブジェクトの内部状態までは守ってくれない、というのがポイントです。 4. Octane の RequestReceived リスナーが一部の状態をリセットする clone だけでは配列の中に入っているオブジェクトのプロパティ書き換えを防げないため、Octane はそこを、リクエストごとに発火するイベントとそれに紐づくリスナーで、明示的に状態をリセットすることで補っています。 Worker のメインループ Worker のメインループの中では RequestReceived イベントが発火し、デフォルトで 8 個のリスナーを順に実行してから HTTP Kernel に処理を渡します。 Worker 起動 ↓ WorkerStarting イベント ↓ ┌── メインループ ─────────────────────────────────────────┐ │ RequestReceived イベント ─→ [8 listeners] │ │ ├─ FlushLocaleState │ │ ├─ FlushQueuedCookies │ │ ├─ FlushSessionState │ │ ├─ FlushAuthenticationState │ │ ├─ EnforceRequestScheme │ │ ├─ EnsureRequestServerPortMatchesScheme │ │ ├─ GiveNewRequestInstanceToApplication │ │ └─ GiveNewRequestInstanceToPaginator │ │ ↓ │ │ HTTP Kernel (Middleware → Controller) │ │ ↓ │ │ リクエスト終了 │ └─────────────────────────────────────────────────────────┘ ↓ max_requests に達したら Worker 再起動 これら 8 個のリスナーは、Locale / Cookie / Session / Auth といったフレームワーク状態のリセットや、 app('request') の差し替え、HTTPS スキームやポートの整合性チェックなどを担います。 実装の中でも、特に挙動を把握しておきたい 2 つを確認します。 まず、認証ガードを毎リクエスト破棄する FlushAuthenticationState を見てみます。 Laravel の AuthManager は内部で Guard インスタンスを $guards 配列にキャッシュし、各 Guard はさらに認証済みユーザーをプロパティに保持します。Octane でこのインスタンスをクリアしないと、 singleton で起きる現象と同じ構造で、前のリクエストの認証ユーザーが次のリクエストに引き継がれてしまいます。 FlushAuthenticationState は、このキャッシュを毎リクエスト破棄することで、前のリクエストの情報が次のリクエストに残らないようにしています。 実装は以下の通りです。 <?php // vendor/laravel/octane/src/Listeners/FlushAuthenticationState.php class FlushAuthenticationState { public function handle ( $ event ) : void { if ( $ event -> sandbox -> resolved ( 'auth.driver' )) { $ event -> sandbox -> forgetInstance ( 'auth.driver' ) ; } if ( $ event -> sandbox -> resolved ( 'auth' )) { with ( $ event -> sandbox -> make ( 'auth' ) , function ( $ auth ) use ( $ event ) { $ auth -> setApplication ( $ event -> sandbox ) ; $ auth -> forgetGuards () ; }) ; } } } auth がコンテナで解決済みの場合に forgetGuards() を呼びだし、Guards のキャッシュをクリアしていることがわかります。 次に、Request インスタンスを差し替える GiveNewRequestInstanceToApplication の実装は以下の通りです。 <?php // vendor/laravel/octane/src/Listeners/GiveNewRequestInstanceToApplication.php class GiveNewRequestInstanceToApplication { public function handle ( $ event ) : void { $ event -> app -> instance ( 'request' , $ event -> request ) ; $ event -> sandbox -> instance ( 'request' , $ event -> request ) ; } } app('request') を新しい Request インスタンスに差し替えます。 app('request') を呼ぶコードが常に最新の Request を見られるのは、このリスナーの働きによるものと理解できます。 何が残って、何が消えるのか 2 つのレイヤーに分けて整理します レイヤー 何が常駐するか リセット手段 Worker プロセス全体 Worker プロセス自体 + 関数テーブル / クラステーブル / static 変数 / グローバル変数 octane:reload / max_request 到達による Worker 再起動 Laravel Application ( $this->app ) bindings、singleton インスタンス、boot 済み ServiceProvider RequestReceived リスナー(部分的) なお、各 Worker は独立した OS プロセスであるため、Worker 間のメモリは分離されています。コルーチン無効時には同じ Worker 内のリクエストも順次処理されるため、気にすべきは「同じ Worker の中で、前のリクエストのデータが次のリクエストに引き継がれてしまわないか」という点に絞られると考えられます。 そのうえで、Laravel アプリコード上でよく使う状態保持の方法ごとに、同じ Worker・別リクエストでどう振る舞うかを整理すると次のようになります。 状態保持の方法 同 Worker・別リクエスト static 変数 残る グローバル変数 / $GLOBALS 残る Worker boot 時点などで解決済みの singleton インスタンスのプロパティ 残る 通常 bind (毎回新規) 毎回新規 scoped バインディング 次のライフサイクル開始時に flush $request->attributes Request 自体が新規生成 「残る」となっている static / グローバル変数 / singleton プロパティは、リクエスト固有のデータや、リクエストごとに増え続けるデータを置くと事故につながり得る点に注意が必要です。アプリ起動時に 1 度だけ初期化されるような不変なデータや、Worker 内で意図的に共有したいデータを置く分には問題ないと考えています。 ここまでは仕様上こうなっているはず、という整理でした。次は簡易的なプログラムで動作を検証します。 検証 仕組みの整理で示した観点を、実機で順に確認していきます。 static 変数 / グローバル変数がリクエスト間で残ること $this->app の内部状態(singleton インスタンスのプロパティ)がリクエスト間で残ること RequestReceived リスナーが一部の状態を実際にリセットすること 加えて、これらの検証が成立する前提として「同 Worker 内ではリクエストが順次処理される」ことを最初に確認します。 検証環境 検証は macOS 上のローカル Docker コンテナで、以下のバージョン構成で行います。 ソフトウェア バージョン Laravel 12.59.0 Octane 2.17.3 Swoole 6.2.1 Octane は次のコマンドで起動します。 php artisan octane:start --server=swoole --workers=10 --max-requests=500 このとき内部で適用される主な Swoole オプションは次の通りです。 設定 値 由来 enable_coroutine false Octane デフォルト(Laravel 本体がコルーチンセーフでないため意図的に無効化) worker_num 10 --workers=10 で指定 max_request 500 --max-requests=500 で指定(メモリリーク対策) 特別な設定はしておらず、Octane / Swoole のデフォルトのままです。 検証用ルートは routes/web.php に用意し、curl で叩いて結果を観察します。 前提の確認: 同 Worker 内でリクエストが順次処理されること まず大前提として、1 Worker 内では複数リクエストが並行処理されず、1 つずつ順番に処理されることを確認します。これ以降の検証はすべて「同じ Worker に来た複数リクエストが順番に処理される」という前提で議論を組み立てるため、確認します。 確認用コード リクエストを受け取ったら 2 秒スリープして PID とコルーチン ID を返すだけの単純なルートを用意します。 <?php Route :: get ( '/test-coroutine' , function () { $ pid = getmypid () ; $ cid = \Swoole\Coroutine :: getCid () ; sleep ( 2 ) ; return [ 'pid' => $ pid , 'cid' => $ cid , 'is_coroutine' => $ cid !== - 1 , ] ; }) ; 実行 Worker 数を 1 に絞った状態( --workers=1 )で、3 リクエストを並列で投げます。もし並行処理されるなら合計 2 秒前後で完了するはずです。 start= $( date +%s ) curl -s http://localhost:8001/test-coroutine > /tmp/r1 & curl -s http://localhost:8001/test-coroutine > /tmp/r2 & curl -s http://localhost:8001/test-coroutine > /tmp/r3 & wait echo " Total: $(( $ ( date +%s ) - start )) s " 結果 { " pid ": 14 ," cid ": -1 ," is_coroutine ": false } { " pid ": 14 ," cid ": -1 ," is_coroutine ": false } { " pid ": 14 ," cid ": -1 ," is_coroutine ": false } Total : 6s 3 リクエストの PID が一致(=14)し、 cid = -1 でコルーチン外、合計時間が 2 秒×3 = 6 秒となっていることから、1 Worker 内ではリクエストが並行ではなく順次処理されることが確認できました。 検証 1: static 変数 / グローバル変数がリクエスト間で残ること 仕組みの整理で「static 変数は同 Worker 内では持続する」と書きました。これを実際に確かめます。 確認用コード <?php Route :: get ( '/test-static' , function () { static $ counter = 0 ; $ counter ++ ; $ GLOBALS [ 'global_counter' ] = ( $ GLOBALS [ 'global_counter' ] ?? 0 ) + 1 ; return [ 'pid' => getmypid () , 'static_counter' => $ counter , 'global_counter' => $ GLOBALS [ 'global_counter' ] , ] ; }) ; static 変数とグローバル変数の両方をインクリメントするシンプルなコードです。 実行 まず、同 Worker 内挙動を見るため Worker 数 1( --workers=1 )で順次 10 リクエストを投げます。 for i in { 1 .. 10 } ; do curl -s http://localhost:8001/test-static echo done 次に、Worker 数 10( --workers=10 )で並列 20 リクエストを投げ、Worker 間の独立性を確認します。 for i in { 1 .. 20 } ; do curl -s http://localhost:8001/test-static > /tmp/s_ $i & done wait for i in { 1 .. 20 } ; do cat /tmp/s_ $i ; echo; done | sort 結果 順次 10 リクエスト( --workers=1 ): { " pid ": 14 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 14 ," static_counter ": 2 ," global_counter ": 2 } { " pid ": 14 ," static_counter ": 3 ," global_counter ": 3 } ... { " pid ": 14 ," static_counter ": 10 ," global_counter ": 10 } 並列 20 リクエスト( --workers=10 ): { " pid ": 22 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 22 ," static_counter ": 2 ," global_counter ": 2 } { " pid ": 23 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 23 ," static_counter ": 2 ," global_counter ": 2 } { " pid ": 24 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 24 ," static_counter ": 2 ," global_counter ": 2 } ... { " pid ": 31 ," static_counter ": 1 ," global_counter ": 1 } { " pid ": 31 ," static_counter ": 2 ," global_counter ": 2 } 順次 10 リクエストでは全て同じ PID(=14)で、 static_counter と global_counter が 1 から 10 まで連続して増加しています。同 Worker 内では static / グローバル変数が持続していることが確認できます 並列 20 リクエストでは 10 個の異なる PID(22 〜 31)にリクエストが分散し、それぞれの Worker でカウンタが独立に 1 から始まっています。Worker 間でメモリが分離されていることが分かります 以上から、 static 変数とグローバル変数は同 Worker 内のリクエスト間で持続し、Worker 間では分離されることが確認できました。 これは、1 リクエスト目の値が 2 リクエスト目に意図せず見えてしまう可能性を意味します。前のリクエストの情報が次のリクエストに残るパターンと考えられるため、利用には注意が必要そうです。 検証 2: $this->app の内部状態がリクエスト間で残ること singleton の中にリクエスト固有のデータを保持して、リクエスト間で値が残ることを確認します。 確認用コード <?php // app/Services/UserContextSingletonService.php class UserContextSingletonService { private ? string $ currentUserName = null ; public function setCurrentUser ( string $ name ) : void { $ this -> currentUserName = $ name ; } public function getCurrentUser () : ? string { return $ this -> currentUserName; } } <?php // app/Providers/AppServiceProvider.php public function register () : void { $ this -> app -> singleton ( UserContextSingletonService :: class ) ; } public function boot () : void { // ベースの $this->app->instances にインスタンスを格納するため、boot 時点で resolve する $ this -> app -> make ( UserContextSingletonService :: class ) ; } 通常の singleton() だけでは、初回 app(...) 解決時にインスタンスが sandbox 側に入り、リクエスト終了時の $sandbox->flush() で破棄されます。リクエスト間で持続する状態を再現するため、サンプルでは boot() で make() を呼んでベースの $this->app->instances にインスタンスを積んでいます。 <?php use App\Services\UserContextSingletonService; Route :: get ( '/test-singleton-set/{name}' , function ( string $ name ) { app ( UserContextSingletonService :: class ) -> setCurrentUser ( $ name ) ; return [ 'pid' => getmypid () , 'action' => 'SET' , 'value' => app ( UserContextSingletonService :: class ) -> getCurrentUser () , ] ; }) ; Route :: get ( '/test-singleton-get' , function () { return [ 'pid' => getmypid () , 'action' => 'GET' , 'value' => app ( UserContextSingletonService :: class ) -> getCurrentUser () , ] ; }) ; 実行 curl http://localhost:8001/test-singleton-set/Alice curl http://localhost:8001/test-singleton-get curl http://localhost:8001/test-singleton-get 結果 { " pid ": 14 ," action ":" SET "," value ":" Alice " } { " pid ": 14 ," action ":" GET "," value ":" Alice " } ← 別リクエストなのに残っている { " pid ": 14 ," action ":" GET "," value ":" Alice " } ← まだ残っている 別のリクエストにもかかわらず、Alice という値がそのまま見えています。 singleton バインディングのインスタンスプロパティに格納した値が残ることが確認できました。 検証 3: RequestReceived リスナーが一部の状態をリセットすること FlushAuthenticationState リスナーが Guard キャッシュを破棄していることを確かめます。 確認用コード <?php use App\Models\User; use Illuminate\Support\Facades\Auth; Route :: get ( '/test-auth-set/{name}' , function ( string $ name ) { $ user = new User ([ 'name' => $ name ]) ; Auth :: setUser ( $ user ) ; return [ 'pid' => getmypid () , 'action' => 'SET' , 'user_name' => Auth :: user () ?-> name , ] ; }) ; Route :: get ( '/test-auth-get' , function () { return [ 'pid' => getmypid () , 'action' => 'GET' , 'user_name' => Auth :: user () ?-> name , ] ; }) ; Auth::setUser() を利用してデフォルトの Guardの $user プロパティに直接 User インスタンスを入れ、Guard キャッシュの状態を作って検証します。 実行 (1) デフォルト構成( FlushAuthenticationState 有効) curl http://localhost:8001/test-auth-set/Alice curl http://localhost:8001/test-auth-get 結果は以下の通りです。 { " pid ": 14 ," action ":" SET "," user_name ":" Alice " } { " pid ": 14 ," action ":" GET "," user_name ": null } ← flush で消えている 実行 (2) FlushAuthenticationState を外した場合 検証のため、 RequestReceived リスナーから FlushAuthenticationState を除外します。 <?php // config/octane.php (listeners 部分のみ抜粋) use Laravel\Octane\Events\RequestReceived; use Laravel\Octane\Listeners\FlushAuthenticationState; use Laravel\Octane\Octane; return [ // ... 'listeners' => [ // ... RequestReceived :: class => array_values ( array_filter ( Octane :: prepareApplicationForNextRequest () , fn ( $ listener ) => $ listener !== FlushAuthenticationState :: class , )) , // ... ] , ] ; 同じ curl を実行します。 { " pid ": 15 ," action ":" SET "," user_name ":" Alice " } { " pid ": 15 ," action ":" GET "," user_name ":" Alice " } ← 前のリクエストの情報が残っている リクエストをまたいで Alice という値が残っています。 検証結果から、 FlushAuthenticationState を外すと認証状態がリクエスト間で残り、有効な場合は Guard キャッシュが毎リクエスト破棄されることが確認できました。フレームワーク標準の Auth が Octane でも安全に使えるのは、このリスナーが裏で動いているからこそだと言えます。 おわりに 本記事では、Octane + Swoole で前のリクエストの情報が次のリクエストに残る仕組みを整理し、サンプルコードで動作を検証しました。その結果、 clone とリスナーによって Auth などのフレームワーク側の状態はリクエストごとにクリアされる一方、 static 変数 / グローバル変数や、Worker boot 時に解決済みの singleton インスタンスのプロパティは自動ではクリアされないことが確認できました。 持続させてはいけない場所に状態を置かないこと、また、もし独自のグローバル状態を持たせる場合には RequestReceived などのイベントに自前のリスナーを追加してクリアすることを意識できればと思います。 最後まで読んでいただきありがとうございました。 参考 Deep Dive into Laravel Octane(Albert Chen)
みなさん、こんにちは☀️ プロダクト開発部の ねぎちゃん です🌱 スタメンは、先日開催された フロントエンドカンファレンス名古屋 2026 にプラチナスポンサーとして、出展・協賛させていただきました! フロントエンドカンファレンス名古屋2026 日時:2026年5月9日(土) 場所:ウインクあいち(〒450-0002 愛知県名古屋市中村区名駅4丁目4-38 ) fec-nagoya-org.github.io プラチナスポンサーとしてブース出展いたしました💪 今回のカンファレンスには、専門領域がそれぞれ異なるスタメンのエンジニアたちが参加しました。 バックエンドメインのメンバーから、AI活用に注目するメンバーまで、多角的な視点でセッションを聴講した結果、「一見地味な罠」への対策から「5年におよぶ刷新劇」の舞台裏、さらには「AIインストールのリアルな地獄」まで、濃厚で痺れる知見がたくさん集まりました✨ このブログでは、参加メンバーが「いちばん印象に残った!」と語る、厳選セッションの感想レポートをそれぞれの視点でお届けします🔥 🔥 スタメンエンジニアが痺れたセッション5選 おしん 💭印象に残ったセッション: 「見た目は同じなのに検索でヒットしない」 ( @wabi_1318 ) wabiさんの「見た目は同じなのに検索でヒットしない」という発表が私にとって特に印象に残りました。 MacOS(NFD)からのアップロードと手入力(NFC)の差分でファイル名検索がヒットしなくなるというバグは、原因究明が難しそうで沼にハマりそうだと感じました。ブラウザやライブラリ側で自動解決されない問題を、Issueの議論などを交えてリアルに語ってくださり、面白かったです。 こうした「一見地味だけど踏むと痛い落とし穴」は中々表に出てこないと思うので、貴重な知見でした。 システムの入力境界でしっかり検証・正規化して、型を活用して素の文字列と区別するという設計思想の重要さを学べたので、今後のプロダクト開発に活かしていきたいと思います。 鈴木 雄一郎 💭印象に残ったセッション:「デザインとコードの境界を溶かす」( @kskwtnk ) 綿貫さんの基調講演「デザインとコードの境界を溶かす」という発表がとても印象に残りました。 私は、普段バックエンドメインで書いており、キャッチアップとしてTypeScript, Reactの学習はしているのですが、デザイン周りの語句もキャッチアップしておくべきという気づきを得ました。 また、デザインシステムを現場に導入したがチームが慣れておらず、うまくワークできなかったという話も、実体験をもとにした話で現場感がありました。こういう失敗談みたいなものはなかなか話しづらいと思うので、共有してくれてとてもありがたかったです。 「プロジェクトの初期はデザイントークンのグローバルトークンを導入するだけでもデザインの統一感が出て、コスパ良く導入できて良い」という話も明日から仕事で即使えそうなナレッジとして参考になりました。 ちぇる 💭印象に残ったセッション: 「いつか誰かが、と思っていた フロントエンド刷新5年間の実践知」 ( @kiichi_sugihara ) 長期的なリアーキテクチャに取り組みたいけど、目の前の改善業務に追われて時間が取れないという、現場でよくある課題への向き合い方が非常に面白かったです。 kiiさんの現場では、当初「業務時間の10%を自由に使えるシステム健全化枠」を設けていたそうですが、機能的な改善の方がチームや社内からも喜ばれやすく、「改善 → 喜ばれる → やめられない」というループに入り、枠を使い切ってしまっていたという失敗談には非常に共感しました。そこから学び、枠ではなく「丸1日システム改善しかやらない『システム健全化デー』」を設けることで集中の分断を防いだ点や、あえて残業時間を減らしてその余白を勉強に充てることが、長期的に組織やシステムへの良い投資になるというお話が印象的でした。 また、技術的負債という目に見えづらい課題を解決するためには、機能開発以上に周囲への説明と合意形成が必要となります。リアーキテクチャに伴うバックエンド側のAPI分離や、デザイントークン作成の必要性など、各ステークホルダーを巻き込む壁があった中で、「理想を描いて、事業プロジェクトのついでに段階的に仕込んでいく」という立ち回りは、どんな仕事においても通じる突破口だと感じました。 必要性の吟味に始まり、周囲の課題と自分の理想をマッチさせながら、最終的に覚悟を持ってプロジェクトを完遂させる姿勢は、まさに「Get things done」の体現だなと思いました。 伊賀本 衛 💭印象に残ったセッション: 「AIと乗り切った1500ページ超のヘルプサイト基盤刷新」 ( @mugi_uno ) 「AIと乗り切った1500ページ超のヘルプサイト基盤刷新」のセッションを聞いて、AIへの過信の危うさを改めて実感しました。 AIがあれば何でもできると思いがちですが、実際に掘り進めると「地獄への始まり」とも言える複雑さが待っています。コードを変えるだけでなく、プロダクトのフェーズに応じた判断が求められ、刷新前後の差分を完全に排除することの難しさも痛感しました。TDDの徹底やドキュメントの積み重ねによってナレッジを蓄積し、誰でも編集できる状態を作るというアプローチはとても参考になりました。 また、AIを使うことで自分の観測範囲に閉じた局所最適に陥りやすいという指摘も刺さりました。自社でも個人スキルの向上は進む一方、プロジェクト横断的なナレッジ共有の仕組みはまだ整備途上です。この課題に向き合い、チーム全体のスキルを底上げする仕組みを作ることが急務だと感じています。自社プロダクトをより多くの人に使いやすく届けるためにも、AIを活用しながらスピード感を持って動いていきたいと思います。 taro 💭印象に残ったセッション: 「いつか誰かが、と思っていた フロントエンド刷新5年間の実践知 」 ( @kiichi_sugihara ) 最も印象に残ったのはこちらのセッションです。 「いつか経験豊富なエンジニアが来て、一緒に刷新を進めてくれるだろう」と思いながら目の前の開発で精一杯だった、というところから始まる 5 年間の物語でした。 何より心を動かされたのは、登壇者の 仕事に対する熱量 です。5年かけて少しずつ信頼と専門性を積み上げ、最終的にフロントエンド刷新をやり切るまでの軌跡は、聞いている自分まで熱くなるものでした。 セッションを聞いて自分も取り入れたいと思ったのは、次の2つです。 1日の時間の使い方 責任者・関係者の巻き込み方 1日の時間の使い方についてです。毎日の業務を同じようにこなすのではなく、「もっと良いやり方があるのではないか」と問い続け、 時間の使い方そのものを設計する 。その積み重ねが複利のように効いてきて、未来を決めるのだという考え方は強く印象に残りました。 責任者・関係者の巻き込み方については、検証が進まない時期に、技術顧問との定例ミーティングを先に設定し、「 検証の進捗を共有する場を作る → 否応なく前進せざるを得ない構造」を作ってしまうという話が象徴的でした。また、1人だと曖昧になりやすい部分が、説明する過程で言語化されてクリアになる、という副次効果も語られていました。 さらに、刷新を「自分の余白」だけでやろうとせず、 事業ロードマップを見据えて事業 PJ に段階的に仕込む という発想にも痺れました。「場を作ってから埋める」アプローチを社内のあらゆる関係者に対して実践している姿勢は、今の自分にとても必要だと感じました。 このような規模のカンファレンスに参加するのは初めての経験でしたが、ブース運営もセッション聴講も、どちらもモチベーションを大きく引き上げてくれる時間でした。次回も機会があればぜひ参加したいですし、いつかは聴く側ではなく発表する側として登壇できるよう、日々の業務と学びを積み重ねていきたいと思います。 おわりに 以上、フロントエンドカンファレンス名古屋 2026の参加レポートでした! 専門領域が異なるメンバーがそれぞれの視点でセッションを聴講したことで、技術的なナレッジはもちろん、チームビルディングやプロジェクト推進のヒントまで、本当に多くの刺激と学びを得ることができました🔥 スタメンでは、こうして得た最先端の知見や現場の実践知を日々のプロダクト開発に還元し、ユーザーの皆さまにより良い価値を届けていけるよう、これからもチーム一丸となって突き進んでいきます💨 改めまして、素晴らしいカンファレンスを企画・運営してくださったスタッフの皆様、そして当日ブースにお立ち寄りいただいた皆さま、本当にありがとうございました💐 それでは、次回のテックブログもお楽しみに🌷 herp.careers
こんにちは。プロダクト開発部の髙木です。 今回はAIコーディングエージェント向けのコードエディタである Superset の紹介になります。公式では『The Code Editor for AI Agents』と紹介されており、Claude CodeなどのコーディングエージェントをGUIから扱えるデスクトップアプリです。現時点ではmacOSのみの提供ですが、公式ドキュメントではWindows・Linuxも近日対応予定とされています。 僕はマウス操作によるGUIに慣れているため、自分にあっているツールを探そうとしたときにフィットするツールが見つけられませんでした。同じような悩みを持つ方の参考になればと思います。 これまでの自分の開発 これまでの開発では主にVisual Studio Code(VSCode)を使用しておりました。開発する言語にもよりますが、VSCodeは開発に必要なベースが整っており、足りないものについてはプラグインによって拡張できる優れた統合環境です。基本的にはVSCodeで完結できるため、VSCodeで開発できるように環境を整えていました。 現在の自分の開発 もう自分でコードを書く頻度は激減しClaude Codeと対話してコードを出力する毎日です。個人的にはこの開発方法は肌にあっています。コードの設計方針などを対話をしつつ進められるため、具体的な実装をClaude Codeに移譲できることで、その方向性が正しいかを常に考えながら成果物を見ることができます。自分で考えながら作っていると検討漏れが起きやすかったのですが、ドライな目で実装を眺められるので成果物の質は上がったように思います。 つまり現在は、もともと開発環境に求められていたエディタに入力するための思考や入力のしやすさではなく、出力されたものを確認したり修正を依頼するレビューベースの開発を行っている状況です。さらに複数のリポジトリを並行で作業できるような作業的な余裕も生まれ、加えて同一リポジトリ内で並行して作業するケースも増えてきました。そうなってくると1ブランチ1画面のVSCodeでは、作業したいリポジトリに対して行き来する画面が多すぎて本来の作業以外に画面の整理のような雑務が発生する状況になっていました。もしかしたらVSCodeにそういった拡張機能があるかもしれませんが、ちゃんと調査までは行えていません。 こういうときにキーボードベースの開発に慣れていればtmuxで画面分割をしつつタブで画面を変えるなどの作業ができたのかもしれません。そういう意味で cmux はそういう人には向いているツールだと思います。 cmux自体は『AIコーディングエージェント向けの縦タブと通知機能を備えたGhosttyベースのmacOSターミナル』と説明されており、CUIベースでの開発が好きな人には合うと思います。 AIによる開発で求めていた機能 GUIで操作できる 複数のリポジトリを行き来できる 同一リポジトリの複数ブランチで並行作業ができる コードのdiffが確認しやすい コードを確認できる これらができるツールを探しており、たどり着いたのがSupersetでした。 GUIで操作できる 基本的な操作はGUIをベースで実行できます。ここはあまり掘り下げるポイントがないので割愛します。 複数のリポジトリを行き来できる リポジトリを追加することができ、左側のタスクバーに追加したリポジトリが表示されます。クリックすることで簡単に切り替えることができます。これも複雑な機能ではないため割愛します。 同一リポジトリの複数ブランチで並行作業ができる 各ブランチをワークスペースという概念で扱っています。公式では、 Each workspace = one git branch. と表現しています。左側にタスクバーが表示されており、そこからマウスクリックでリポジトリやワークスペースを切り替えることができます。 公式トップの画像を引用 良きポイントとして各ワークスペースが独立しているため、AIエージェントのコンテキストが混ざらずに使用することができます。Workspace自体はGit Worktreeで管理されていますが、利用者がそれを意識する必要はなく+ボタンを押すことで追加することができます。追加時にブランチ名やプロンプトを入力することでそのまま作業を開始してくれます。 コードのdiffが確認しやすい VSCodeを使用していたこともあり、ツール内でdiffを確認したいと思っていました。 difit を使うことも考えていましたが、スキルを使ったりアプリケーションを立ち上げることに手間を感じていたのでできれば内蔵が良いと思っていました。リッチなものではありませんが、必要十分な機能だと思いました。 コードを確認できる エージェントが作ったコードを見る際に、簡素なコードをシンタックスハイライトつきで見られれば良いと思っていました。Claude Codeが表示したファイルパスをCmd+クリックで表示することができるので、簡単な確認であれば十分です。このあたりの機能はVSCodeと同様でした。 使ってみて気づいた利点 ソフトウェアのアップデートでも作業が中断しない ソフトウェアアップデートをすると大体のツールが再起動となり、これまでのコンテキストが失われます。もちろん --continue などのパラメータをつければコンテキストを復活できますが、バックグラウンドで起動していたアプリケーションなどの接続は失われるため、作業していたものを復旧することができません。しかしこのツールはアップデートをしたとしても作業を自動で復旧してくれます。具体的にどのような仕組みなのかまでは追っていませんが、アップデートが億劫にならないというのは他のツールではあまり感じたことがありませんでした。 作業のスピードが上がった 1つのリポジトリにおいて並列で調査をしたりすることが増えたことで、こなせる作業量が増えたように感じます。これまではブランチの切り替えにおいて自分で管理していたこともあり、簡単にGUIで作業できるようになったことは別ブランチでの作業の億劫さを軽減してくれました。これまではstashしたものがどこにいったかわからなかったり、作業に戻ろうと思ったときに戻れない事が多かったので、ズボラな自分にあった作業のしやすさだと思います。 Supersetだけで完結しない部分とその割り切り コードをちゃんと読もうとしたとき、僕としてはコードジャンプが必須です。VSCodeでできていたコードジャンプが、Supersetでは(おそらく)できませんでした。ちゃんと調査はしていないのでもしかしたらできるかもしれません。 困っていない理由としては、コードジャンプでコードを確認するためだけにVSCodeを開くからです。SupersetにはIDEなどで開くためのリンクが設置されているため、機能不足は普段使っているツールで解消することができます。足りない機能をいつも使っているツールに流せるため、思っていたよりもストレスも低く移行できました。最初は全てのツールをこなせるものを考えていましたが、得意なことを得意なツールにやらせたほうがストレスもたまらないと思いました。 まとめ 簡単にですがSupersetについて書きました。これまで開発に求められていた機能とは異なり、並列性や切り替えのしやすさなどが求められる時代になったと思います。作業の仕方を変える必要はありませんが、自分にあったツールを使っていくことで作業のやりやすさも変わると思いました。 個人的にはSupersetで得られた体験が非常に心地よかったので、皆さんにも一度試してもらいたいです。

動画

該当するコンテンツが見つかりませんでした

書籍