Chrononglyph

Next_js

#7664

レンダリングの変化点

半分以上AIにお任せでしかも短期間の突貫工事で作ったためいまのところメンテナンス性が最悪のこのブログ。
なんでそこまで可読性が低いのか、なんとなく原因が分かりました。
それは自分がNext.jsの最新版で変更された仕様の内実をまるっきり勘違いしていたことによります。


同じNext.jsで作ったピクチャレ大会はNext.js ver.13を使って作りました。
当時はプロジェクト作成時のフォルダ構造を「Pages Router」か「App Router」か選択できたのですが、
前者は従来からあるシステムであるのに対して後者は最近できたばかりの最新システム。
前者の方がネットに落ちているノウハウの量も多いだろうと思いPages Routerを選択しました。
しかしその後、公式がApp Routerを全面的に推奨するようになり、
たった1年余りでPages Routerは古いものというような風潮に。
そこで4代目本家ブログの制作にあたってはApp RouterとTypescriptを採用することにしました。
当時はルーティングのフォルダが「pages」か「app」の違いだろうというくらいにしか考えておらず、
しかもAIにほとんどお任せなのでそんな浅い知識でも動いてしまったんですね。


実際にはこれらのシステムの違いは
もはや違うフレームワークと言っても過言ではないほど変更箇所が多く、もっとも大きな違いは
App Routerはページ用のスクリプトが原則すべてサーバーサイドレンダリング(SSR)になるという点です。
SSRとは、サーバーで諸々の変数を処理してからHTMLを出力してクライアントに渡す方式です。
一方、クライアントのブラウザで諸々の処理を行う方式をクライアントサイドレンダリング(CSR)と言います。
Pages Routerでは専用の関数によってそれぞれの役割が明確に分かれていて、
まずgetServerSideProps関数でサーバーサイド処理を実行してその戻り値を取得し、
それに基づいてクライアントサイドで処理する部分を書くというやり方ができました。
これはPHPにおけるwebアプリの書き方にもよく似ていて直感的で分かりやすいです。


しかしApp Routerは全部サーバーサイドで処理するので、そういった従来の書き方が通用しないことになります。
ここで問題になるのが、ページを表示してからボタン押下などに連動して値を変更したいような場合。
そういう処理はクライアントサイドでしか処理できません。
そこで、App Routerではそういう部分はコンポーネント(部品)化して個別に処理することになります。
コンポーネントもデフォルトではSSRですが、
先頭行に"use client";とつけることで特別にクライアントで処理してくれます。
ファイル単位で処理を分けろということですね。
Pages RouterではコンポーネントはSSRできなかったので、そこは進歩した点です。
まあいずれにしろ、Pages Routerを知っているからと言ってApp Routerを使いこなせるわけではなく、
むしろフレームワークとしてイチから学習し直さなければならないレベルで変化点が多いです。
素直にPages Routerで開発するべきだったのかもしれない。


そんなことも理解せずに開発を無理やり進めた結果、
あらゆるページで動的な仕組みを導入できないサイトが完成してしまったというわけです。
そして後々useStateを利用する仕組みを導入しようとしたときに壁にぶち当たり半ば挫折していたわけですが、
その原因はApp Router移行時による仕様変更であると今回ハッキリと分かりました。
とにかく締め切りに追われて見切り発車で進めるとこうなってしまうという教訓ですね。


今回を機にApp Routerの仕様もだいぶ理解できたので、
4代目ブログのフロントエンドはいずれ根本的に作り直すかもしれません。
まあ動的な機能も必要ないテキストサイトなので支障ないと言ってしまえばそれまでなのですが、
そう言ったこととは無関係にとにかく動線も安定性も最悪なので……。
それにApp Routerの仕様を知ったことでにわかにプログラミングしたい欲の復活も感じています。
やはりこういうのは技術に触れることが一番大事なのかもしれない。


#7322

三方面からのバグ修正

去年末、辛酸を舐める結果になったピクチャレ大会の期間限定ランキングが中止になった問題。
その直接的な原因はDockerが壊れていたことだったのですが、
デバッグを進めていくと他にも深刻な2つの不具合があることが判明しました。
それは結局年末休みだけでは片付けられず年始に持ち越すことに。
ところが今回、実家という比較的作業が捗る環境で奮闘したところ解決の目処が立ったので、
その備忘録として何が原因だったのかを書き残しておくことにします。
前提として、Next.js 13系、styled-components 5系、MUI 5系を使ったフロントエンドの開発です。
またバックエンドAPIにはLaravelを利用しています。


まずひとつは「ページをリロードすると一部のスタイルが崩壊する」という問題。
詳細については以下のフォーラムが詳しいです。



Prop className did not match (NEXT 13 + styled-component - without app folder)



おおまかに言えば、通常のページ間遷移では問題なく表示されるのに、
ページをブラウザの機能でリロードした場合にCSSのクラス名が不一致になるという現象です。
CSSのクラスが不一致になるとスタイルの指定がすべて無効になるため、
必然的に骨組みのような見た目になります。
上記のフォーラムではimport記法を修正することで直ったとの声が支持されていますが、
自分の場合はそれでも直りませんでした。
そこで、もうこれはstyled-componentsのバグだろうと割り切って
人力で同じスタイルを定義したclassを作り、バグる部分のコンポーネントに割り当てることに。
そうすれば当然直るわけですが、テーマ設定によって出力が異なる部分はそれでも直らず。
最終的に、テーマによって分岐する部分はCSSの擬似クラスで処理するようにしたら直りました。


プラグイン「next-themes」はuseThemeという関数(Hook)で現在のテーマを呼び出すことができ、
該当部分はそれを利用して背景色を決定していました。
ところがこれで最初に呼び出されるテーマ変数は初期レンダリング時のものであり、
ページロード中にユーザーが設定したものに内容が変化しているようです。
この不一致が不具合を起こしていた可能性は否定できません。
さんざんstyled-componentsに原因を求めていたのですが、もしかしたらそっちは悪くないのかも……。


もうひとつは「高速で同じディレクトリのページへ次々に遷移するとサイトが壊れる」という問題。
これの解決があまりにも難しかったことが期間限定ランキングの開催を諦めるに至った理由のひとつでもあり、
これは3方面から解決に臨みました。
まず、そもそも「ロード中なのに次の操作ができる」という構造が問題なので、
クリックしてからロード完了まではリンクを触れないようにローディング画面にするというアプローチ。
とりあえずこれを実装すれば「リクエスト中に次のリクエストを送る」ということができなくなり、
バグの根幹だったリクエストエラーは無くなるはず。
その代償としてユーザーはロード完了まで何もできないので読み込み時間が長いとストレスになります。


次に、ページが読み込まれたら必要なデータを都度リクエストするのではなく、
可能な限り事前にページキャッシュを作っておいてそれを読み込むようにしました。
従来はSSR(サーバーサイドレンダリング)という技術を使っていて、
ページが読み込まれるたびにすべてのAPIを実行していました。
これを使っているかぎりは、APIの負荷を減らすことはなかなか難しいと思います。
今回はそれをISR(インクリメンタル・スタティック・リジェネレーション)に変更しました。
これは、ページがリクエストされたらStaticなページ、つまりHTMLファイルを内部でビルドします。
ビルドされたページが存在するリクエストは次回以降それが呼び出されるため
サーバー側での処理が一切必要なくなり、
阿部寛のホームページのように爆速でページが表示されるようになります。
デプロイ時にあらかじめStatic(静的)なページを生成する技術なら前々からありますが、
それはピクチャレ大会のように最新情報を常に取得したい場合に適していません。
また、このサイトはいろいろな変数の組み合わせでページを生成するような仕組みなので、
もしStaticなページをビルドする場合はデプロイ時に全通りのページを生成する必要があり、
これはこれでデプロイの時間と負荷が大幅に高まるので望ましくありません。
しかしISRの場合は初めてアクセスされた時点で生成しかつ一定時間で自動的に再生成するため、
組み合わせが膨大で情報更新が頻繁なこともあるピクチャレ大会にも適用できるようになっています。
しかもこれのすごいのは、キャッシュをビルドしたページに他ページへのリンクがあると、
その中から次にアクセスされそうなページを自動判別して先行してキャッシュを作ってくれることです。


最後に、Laravel API側のリクエスト制限を大幅に緩和しました。
Laravel APIはデフォルトで1分60回まででそれ以上はエラーを出力するようです。
とりあえずこれを1分1800回まで緩和して様子を見ることにしました。
あんまり緩和しすぎるのも望ましくないので、今後もAPIリクエストの数を減らす工夫はしていきます。
JSONで受け取るべきエラーがHTML形式になっていると、



SyntaxError: Unexpected token < in JSON at position 0



というエラーコードが返ってくるのですが、
この問題に向き合った当初はこれの正体が掴めずに非常にモヤモヤしていました。
このエラーコードはレスポンスがHTMLなのでJSONにできないというエラーです。
だったらLaravel側でログ出力すればいいじゃん、と思ってもAPI単体では全然エラーを出力しないし……。
結局エラーハンドリングの部分を書き直してエラーはプレーンテキストで出力するようにしたら、
Too many requestsというエラー本文に辿り着き、やっと解決の糸口を掴めた次第です。


というわけでAPIの仕様変更、ユーザビリティの欠陥、ページ生成の仕様変更と、
3方面からの改善を実施したことでエラーは起こらなくなり、
しかも以前と比べものにならないくらい爆速で表示されるようになったので大いに満足です。
久々に高い壁を乗り越えた感じがしてweb開発もモチベが上がりました。