三方面からのバグ修正
去年末、辛酸を舐める結果になったピクチャレ大会の期間限定ランキングが中止になった問題。
その直接的な原因は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開発もモチベが上がりました。