世界を動かす技術を、日本語で。

Next.jsはイライラする

概要

  • Next.jsの本番環境用ロギング設定に苦労した体験談
  • ミドルウェアの制限とAsyncLocalStorageの挙動の問題点
  • ヘッダー経由でしか情報を渡せないNext.jsの設計上の課題
  • カスタムサーバー導入でも根本的な改善は困難
  • SvelteKitなど他フレームワークとの比較とNext.jsへの批判

Next.jsでの本番ロギング地獄

  • Next.js サービスで障害発生、本番環境でのログ取得困難
  • デフォルトのロギングは 開発環境のみ 有効
  • pino ライブラリを選択し、ミドルウェアで導入を試みる
  • ミドルウェアは 4つのパラメータ しか渡せず、 ヘッダーのみ がルートに影響
  • 複数ミドルウェアチェーン 不可という設計上の制約
  • AsyncLocalStorage を利用し、リクエストごとのloggerインスタンス生成
  • ログ出力はできるが、 Edge Runtime のため意図した挙動と異なる
  • Node.js Runtime に切り替えると一部動作するが、プロジェクトによっては不安定

ページ・レイアウトでのロギング問題

  • ページやレイアウトでlogger利用時、 nullが返る 問題発生
  • ミドルウェアと 非同期コンテキスト が一致せず、loggerが共有されない
  • 唯一渡せる情報が ヘッダー のみのため、requestIdをヘッダー経由で伝搬
  • サーバ・ミドルウェア・クライアントで ロギング実装を分割 する必要性
  • import制限 により、サーバ/ミドルウェア間でloggerコード共有不可

カスタムサーバー導入と挫折

  • custom server で独自のロジック実装を試みる
  • AsyncLocalStorage を再利用し、サーバ・ミドルウェア・ページでlogger呼び出し
  • しかし 非同期コンテキストの継続性 が保証されず、loggerが機能しない
  • headers()cookies() はAsyncLocalStorageを使っているが、ユーザーは同じ方法が使えない
  • ミドルウェアからページへ情報を渡す手段は ヘッダーリダイレクト のみ

Next.js設計への批判と他フレームワーク比較

  • Next.js 開発者の「ビジョン」による制約の多さ
  • 日常的に遭遇する「痛み」と ユーザー体験の悪化
  • Vercel 製の SvelteKit では、event.localsで自由にオブジェクトやクラスを渡せる
  • 複数handle関数シーケンス実行 も可能
  • SvelteKit の柔軟性とNext.jsの硬直性の対比
  • Vercel 自身のフラグシップでこの差は納得できないという不満

まとめ・教訓

  • Next.js の本番ロギングは設計上非常に困難
  • ミドルウェアの貧弱さ、 AsyncLocalStorage の制約、情報伝搬手段の乏しさ
  • SvelteKit など、より柔軟なアプローチを持つフレームワークの存在
  • Vercel はNext.jsの改善に本腰を入れるべきという提言
  • この経験が、より良いフレームワーク選択や設計への気づきとなる

Hackerたちの意見

本当にそうだよね。俺はReactの熱心な支持者で、毎日使ってるけど、クラスコンポーネントからフックへの移行は、プログラミングモデルとしては良いと思ってる。でも、Nextを使うと、どこかで道を見失った気がするんだ。いろんなフレームワークを試して、エソテリックなプログラミング言語も好きなんだけど、なぜかNext.jsだけは、エラーメッセージが何を言ってるのか全然わからないことが多い。変なハイドレーションの問題にどれだけ時間を費やしたか、数えきれないよ。

俺はReactやNext.jsのユーザーじゃない。他の人は異論があるかもしれないけど、個人的には、伝統的なHTML+CSSのドキュメントに必要に応じてバニラJSをデコレーションすることで、JSへの接触を最小限に抑えたいと思ってる。シンプルなNext.jsのランディングページがFirefoxで壊れるのを見た時は、ちょっと驚いた。さらに悪いことに、失敗モードはすべてのコンテンツを黒い画面と白い文字で覆って、「アプリケーションクライアントサイドエラーが発生しました」って表示されることだった。シンプルなランディングページがレンダリングできないのは驚きだったけど、その原因がJSのフロントエンドフレームワークだと知った時は、まあそういうもんだなって感じた。支持者には意味があるのかもしれないけど、バンドワゴンに乗ってない俺たちには、本当に混乱することがある。

Nextはかなり前に自分たちをダメにしちゃった。VCサイクルを通るものは、結局そうなるんだよね。彼らとそのお金には嬉しいけど、もう使えないな。今はViteをデフォルトオプションにしてるけど、もっと軽量なものがいいな。

フィードバックを聞いて、感謝してるよ。ミドルウェアのDXの問題はよくわかってる。15.5ではNodeランタイムのサポートに大きな一歩を踏み出して、多くの人が報告してきた問題に対処したんだ。もしタイムマシンがあったら、Routing MiddlewareとかRouting Handlerって呼んでたと思う。ルーティングフェーズでインターセプトするための特定のフックがあって、特定のプロバイダー向けにCDNエッジに届けられるんだ。それに、ちょっとした高度な逃げ道でもある。OPがログについて言及してるから、計測と可観測性のためにOpenTelemetryを取り入れて、instrumentation.tsの規約もあるってことは覚えておいてほしい。

返信ありがとう。でも… > OPがログについて言及してるから、計測と可視化のためにOpenTelemetryを取り入れて、instrumentation.tsの規約を作ったってことは、ちょっと不器用なログ機能の答えが、単にもう一つの重い複雑さのレイヤーを追加することになってるみたいだね。確かに、すべてのアプリケーションがOpenTelemetryを必要とするわけじゃないよね。logger().info()が普通に動けばいいのに。そんなに難しい問題じゃないでしょ?他の言語やフレームワークはみんなやってるのに!

もし本当に適切なサーバーサイドミドルウェアをサポートすることに決めたなら、なぜ他のまともなサーバー実装が提供しているように、ミドルウェア関数を一つだけでなく、チェーンにできないの?

ここにいるから、ちょっと言わせて。この記事では、著者がドメインの違いを理解できず、どこでも同じアプローチで関数を呼び出そうとしている。そりゃうまくいかないよ。Next.jsの誤りは、本質的に異なる関数ドメインを混ぜようとすることだ。そんなことはやめれば大丈夫。ドキュメントは役に立たないし、ただ混乱を招くだけだよ。エッジとSSR、Nodeとクライアントサイドを一つに混ぜるのはめちゃくちゃで、その試みは冗長なフレームワークの複雑さを生むだけなんだ。

アプリルーターに移行した時、まるでブートキャンプを卒業した人たちが「改善」しようとしてるみたいだった。エクスプレスAPIは成熟していて、サーブレットやRack、プラグなどのコンポーザブルなロシアの人形アプローチに大体合ってるのに。ひどいミドルウェアAPIの他に、リクエストパラメータをcookies()やheaders()みたいなグローバル関数に置き換えるという疑わしい決定もあった。もしかしたら、これらの決定が意味を持つような設計上の制約があるのかもしれないけど、正直言って、彼らは苦労して学んだ教訓を全部捨てて、同じ間違いを繰り返してるように見える。

ストリーミングへの執着が新しい制約の大きな要因だと思う。それに、最低限の共通点をサポートすることも。

これらの問題の半分は、コードがどこで実行されているかの相対的な誤解から来てる。Next.jsはブラウザ、ミドルウェア、エッジ、Node、SSRの相互作用によって、層が重なり合ってるから、ものすごく複雑なんだ。これが適用されるのは、次のような状況だけだと思う。* グローバルなオーディエンスにB2C製品を販売しているので、エッジのセマンティクスがレイテンシの問題に役立つ * Vercelに高いプレミアムを払ってホスティングしてもらうことに抵抗がない * バックグラウンドタスク処理が必要ない(Vercelはマーケットプレイスやパートナーサービスを紹介するから)、だから他のプロバイダーでホスティングする必要がない。それ以外の場合は、よく知られた道を進んで、React-ViteのSPAか、Railsの普通のSSRに従った方がいい。

たとえ最初のカテゴリに当てはまっても、VercelとSSRを使うことでパフォーマンスのボトルネックが解決されるとは思えない。みんながやってる他のクレイジーなこと(数メガバイトのバンドルサイズ、DBへの何十回もの往復を伴う遅いAPIコールなど)を考えると、基本的なプロファイリングや最適化、シンプル化をする方が、もっと進展があるように思える。

そう、基本的にはそれだね。Vercelは、React Server Components、部分的なプリレンダリング、エッジサーバー、ストリーミングなどの組み合わせを使って、最適化されたパフォーマンスを解決しようとしてる。彼らの一見変わったデザインやAPIの決定は、基本的にそれに帰結するんだ。必要なら、それが存在するのはいいことだよね。でも、エッジ関数でSSRをやるだけでもかなりのところまで行けるよ。

どこかで言われてたけど、これがSSRフレームワークが何十年も持ってきたレイヤーなんだ。もしかしたら、すべてにSPAsを使わないことを学ぶべきかもね。

その状況には同意しないけど、たとえNext.jsと一致することを示していたとしても、それに伴う生産性や保守性の低下には見合わないよね。私はGleamのLustreを使ってて、もう戻る気はないよ。Elmの創設者がNext.jsの真逆についての素晴らしいケーススタディの基調講演をしてたんだ。

そうでなければ、よく知られた道を進んで、react-viteのSPAか、Railsを使った普通のSSRにこだわるべきだよ。大人なやり方でSPAを書こう。APIは、その作業に適した言語とフレームワークで書いてね(Rails、Spring、今年のMicrosoftの.NETウェブ技術の名前、何でも好きなものを選んで)。フロントエンドはTypescriptで書こう。フロントエンドとバックエンドを密結合する理由なんて全くないよ、2015年に「アイソモーフィック」という言葉を覚えたJavaScript開発者が何人いてもね。

これらの問題の半分は、コードが正確にどこで動いているのかの相対的な誤解から来ている。別の言語を見てみると、こういったマルチスレッドの問題は、通常、別のコンテキストや同期パッケージ(ミューテックスやアトミックを扱う)を標準ライブラリで提供することで表現される。Node.jsやブラウザ側のJS環境には、こういった罠に陥らないようにするための標準ライブラリが完全に欠けていると思う。それが、下流のパッケージやライブラリの質を向上させるために強制されるべきものなんだ。

これらの問題の半分は、コードがどこで実行されているかの相対的な誤解から来てる。私はかつて、どこでもJavaScriptが使えるのは利点だと思ってたけど、今はそれが悪いアイデアだと思うようになった。私の会社はInertia.jsとVueを使っていて、かなり良い体験になってる。現代的なフロントエンドレンダリングの力はそのまま得られるけど、全体のアーキテクチャはずっとシンプルなんだ。ルーティングは100%サーバーサイドで、一般的なAPIは必要ない。(注:InertiaはReactやSvelteとも動くよ)最初はNuxtを試したけど、ひどい状況だった。実際のバックエンドサーバーとフロントエンド用のサーバーの2つが必要になって、コードが実際にどこで実行されているのかを理解するのがすごく複雑だった。今は超シンプルだよ。PHPならサーバー上、JSならブラウザ内。これを疑う必要がないのは、私たちにとって大きな助けになってる。

新しいプロジェクトでは、なんとなく違うものを使う傾向があるんだ。クライアントにはexpress + react、angular、vue、next、nuxtでアプリを作ったり、サーバーにはgo、.net、node、phpなどを使ったりしてる。良いところも悪いところも見つけるし、いろんな解決策の異なる側面を評価してる。でもNextだけは別。Nextでかなり大きなアプリを作ったけど、最初から最後まで痛い思いをした。どの部分も変だったり、遅かったり、面倒だったり、完全に狂ってたりした。今でもそのアプリを維持してるけど、現時点で俺が心底嫌いな「もの」はそれだけだ。エコシステムはかなり良いし、人気があるから人々は結果に満足しているようだけど、俺の経験は救いようがないほどネガティブだった。変な感じだよ。

最初にNext.jsを見たとき、Meteor.jsを思い出したんだ。少し学んで、いくつかの個人プロジェクトもやったけど、すぐにオーバー抽象化されてて、柔軟性がないことに気づいた。プロトタイプを超えるのが本当に難しかったんだよね。でも、こういうソリューションが出てくるのは、自己完結型で「バッテリー付き」だからなんだ。ついこの間、Hacker NewsでLaravelとSymfonyのスレッドがあって、同じことが言われてた。複雑さが入ると壊れるんだよね。これらのソリューションを、NodeJSやReact SPAが急速に人気になった古いモデルと比べると、バイキングスタイルのツールやライブラリがある。要するに、余ったパーツで自分だけのスイスアーミーナイフを作る感じ。余ったパーツが自己完結型だから、すごく低い抽象レベルをターゲットにしないといけない(例えば、Reactはコンポーネントライブラリ、HTTP+Expressはバックエンドルーター、PostgresはDB)。このアプローチには多くの欠点があるけど、柔軟性を保てて、タワーオブバベルみたいな過剰設計を避けられる。いろんなレイヤーが重なってるわけじゃないから、複雑さは消えないけど、レイヤー同士が兄弟みたいに並んでるから、うまくいかないときに一つのレイヤーを別のもので置き換えるのがやりやすい。だから「バッテリー付き」が人気なのも理解できる。少し互換性のないツールやライブラリを組み合わせるのって本当に面倒だもんね。全部セットアップするには、もっと経験のある人が必要だよ。

お互いに少し互換性のないツールやライブラリを組み合わせるのは本当に面倒だよ。これが私の仕事。私たちは小さなチームで、私の仕事は物事を最新の状態に保つことなんだ。信じられないくらい時間がかかるよ。ハード依存関係のあるパッケージや、5年前にサポートが終了したパッケージがあるからね。

いや、Laravelは過剰に設計された抽象化が時にはうまくいくことを証明してるよ。Laravelは本番環境で素晴らしく動いてるし、使ったことを後悔したことはない。

彼らは根本的にとても難しいことをしようとしてるんだ。サーバーサイドとクライアントサイドのコードを統一するのは、違いが重要になるときに混乱を招くのはいつもそうだよね。個人的には、サーバーサイド、クライアントサイド、または共有ユーティリティを明確に分けたコードの方向に行きたいな。でも、そのアプローチにはもっと型安全な考え方が必要で、ランタイムエラーを好む人たちが多いから、そうすると大多数を怖がらせちゃうかも。

あなたの観察は一般的に当てはまるね。「魔法の」フレームワークのデバッグは、シンプルな基本と一貫したデザインから始めるよりも複雑なことが多い。ボーナスとして、依存関係が少なく、ビルドの問題も減って、ページやバイナリが軽くなるよ。

100%同意するよ。私も同じ問題に直面したことがあって、Next.jsは絶対に使わないし、職場のチームには他の何かを使うように勧めるつもり。一般的に、Next.jsには99.9999%のプロジェクトには必要ない抽象化の層がたくさんある。必要なプロジェクトは、むしろ下位のパーツから特注のソリューションを作った方がいいと思う。Next.jsは、私が今まで使った中で最悪の技術だね。

私にとっては二番目に悪い。Sharepointを使ったことがあるよ。

多くの抽象化やNext.jsのツールは、私のOSがもっと良く、クリーンに、予測可能にやってることをやってるよ。Windowsが環境変数を持ってない(持ってなかった?)から、過剰に複雑なENV/.envの読み込み階層が(部分的に)必要なんだろうね。inotifyやポート検出、スレッド管理も同様で、*nixはそれをうまくやってる。だけど、*nixとWindowsの両方で同じように動くインターフェースや機能が欲しいときは、結局Next.jsみたいな再発明されたホイールや抽象化の山に行き着くことになる(結局、いつも漏れちゃうんだけどね)。

代わりに何を使ったの?

Next.jsの経験から言うと、その粗い部分はバグじゃなくて、むしろ特徴だよ。すべてがVercelのホスティングを使う方向に向かってる感じ。

多くの企業がNext.jsを要件にしているのがわかるよ、求人情報にもそう書いてある。まるでReact = Next.jsみたいで、Reactのことすら触れてない。Vercelのビジネスに強く関わっている開発者もいるしね。最終的には、開発チームがこれらの決定を下す責任がある。いくつかのプロジェクトでNext.jsに基づく問題に直面したけど、最善のアプローチはそれを排除することだった。結果は良くて、開発の満足度も高かったよ。特定のプロジェクトからNext.jsを取り除かなきゃいけなかったって面接で言ったら、終わりだね!

もし私があなたをインタビューしていたら、それはあなたにとってプラスになるポイントだね。実際、その逸話を話すのは、クールエイドを飲んでる会社を見極めるいい方法かも。

今、Next.jsを使ったプロジェクトを立ち上げようとしてるんだけど、経験豊富な開発者たちから意見や潜在的な落とし穴を教えてもらえないかな?要するに、これは概念実証のデモ/探索的プロジェクトなんだ。今はNext.jsのアプリルーターを使って、(マーケティング)、(アプリ)、(ログイン)とかのマルチルートを設定してる。データベースはNeonを使って分岐させる予定で、AI機能を含めるようにVercel AI SDKも使うかもしれない。唯一の心配は、このサーバーレス層/バックエンドがBetterAuthと一緒に使ったときにユーザー認証/認可とどうやってやり取りするのかが不明なこと。今はVercelでホスティングしてるけど、Next.jsはVercelでホスティングするのにぴったりだと思う。もしかしたら、私が不慣れだからかもしれないけど、Next.jsはVercelでの概念実証を簡単にしてくれたからね。