2025/04/16 シラバスビューアWebアプリの実装
アプリケーションの実装
画面は2つだけ
科目リスト
科目詳細
科目詳細を開くときは、裏で科目リストの状態は保持するようにする。
こういうのをネイティブアプリだとやりやすいだろうか
ただ、公開するのに手間と金銭的負荷があるので、ネイティブアプリではなくWebアプリケーションとして実装する
金銭負荷に関してはサードパーティ制のアプリストアで配布すれば一部解決できそう
F-DroidではオープンソースなAndroidアプリケーションを配布可能 iOSでは非Apple制アプリストアでの配布は現状不可能だが、
アプリ配布の公正競争を求める動きはある
UIライブラリを使って、全画面Modalか、Drawerとして実装するのが良いだろう
フレームワークについて
現時点ではNext.jsのApp Routerを使用する。
Data Fetching機能を使用して、JSONデータを元にページを静的生成する。
Propsを渡す方法
Pages Router: getStaticProps()を使用していた。
App Router: Server Component内でfetchし、子コンポーネントに渡すという方法を取れるようになった。
静的生成するPathの指定
Pages Router: getStaticPaths()を使用していた。
App Router: generateStaticParams()から動的セグメント(パスの[...]の部分)の値の配列を返す。
デプロイについて
静的生成を利用する場合、今回のアプリケーションにはサーバーが必要ない。
GitHub Actionsを使って、next buildし、GitHub Pagesにデプロイする。
Next.jsのStatic Export機能が内部で使われていそう。静的なHTMLにビルドする
code:error.jsx
Uncaught Error: Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
...
<Stack>
<UIComponent ref={null} className="ui-stack" __css={{...}}>
<Styled(div) ref={null} data-mode={undefined} className="ui-stack" __css={{...}}>
<Insertion>
+ <div data-mode={undefined} className="ui-stack css-1vw0258" ref={null}>
- <style data-emotion="css-global 1dokfuo" data-s="">
カスタムThemeが反映されない問題も解決できなかった。
code:bad.jsx
"use client";
const Provider = ({ children }) => {
const custom = {
tokens: { fonts: { ... } }
};
const extendedTheme = extendBaseTheme(custom);
return (
<UIProvider theme={extendedTheme}>
</UIProvider>
);
}
ui-...などのCSSクラスは割り当たったが、スタイルは一部を除いて付与されていなかった
たとえばContainer要素やその中のButton要素の幅は問題なく設定されていた
Hydration関連の問題?
CLIの指示にしたがってnpm installしたが、依存しているNext.jsのバージョンが古そう
code:error
$ npm audit
# npm audit report
next 15.0.0 - 15.2.2
Severity: critical
手動でバージョンを上げた
'node' の型定義ファイルが見つかりません。ファイルがプログラム内に存在します。理由: 暗黙的なタイプ ライブラリ 'node' のエントリ ポイント
よく分からず解決
モジュール '@heroui/link' またはそれに対応する型宣言が見つかりません。
Visual Studio CodeでコマンドパレットからTypeScript: Reload Projectを選んだら解決した
これでHeroUIのテンプレートが動くようになった
/icons/hr.icon
どのように作業するか?
先に画面のイメージを決めたい
JSONの理想的な形式が分からないから。
現状の形式で問題なく目的を達成できるのかを検証したい
とくに、検索まわりと、詳細ページの生成が上手くいくかを確かめておきたい
→ アプリケーション側では、仮のSubjectオブジェクトを定義し、基本的な要件を達成できることを確認する
Next.jsのInstallationをHeroUIのCLI上で行ってしまったので、ブランチはHeroUIの検証と同じものを使って行う
https://scrapbox.io/files/6817855c4456a821418e5057.png
ひとまず、科目名を出力することはできた
https://scrapbox.io/files/68179219e37acdfcf5e8b0f3.png
各科目の情報をより詳細にした
学年や分類、必修・選択は、将来絞り込みに使う予定なので、Chipを使用した画面にしている
Buttonのほうが適切か?
Evaluation Eventを一覧ページに記載するかは微妙
選択科目の選択肢は毎学期そこまで多くない(高々10個程度)ので、Evaluation Eventを一覧できても何かの体験の向上に繋がるイメージが湧かない
一覧ページと詳細の行き来を高速化することに努めたほうが良さそう
問題点: 画面の描画に4秒程度かかる。
LightHouseでパフォーマンスを確認する
Webサーバからの応答に3秒程度要している。
Dev Server用のWebSocketや、cache-control: no-cacheが原因でbfcacheが効いていない。 next startなら適切に動作するのか?
→ bfcacheが効いて、一覧ページとページ詳細が適切に行き来できるようになった。
next dev時のLightHouse測定値は参考にならないと思われるので、next start時の値を使って考える。
Codespace内で、localhostにアクセスした場合
code:header_inner
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 4294967294
X-Powered-By: Next.js
Cache-Control: s-maxage=31536000
ETag: "unkp6wcu7s17a4u"
Content-Type: text/html; charset=utf-8
Content-Length: 2388510
Vary: Accept-Encoding
Date: Mon, 05 May 2025 06:19:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5
インターネットを介してブラウザから、Forwardされたページにアクセスした場合
code:header_outer
cache-control: no-cache,no-store
content-type: text/html; charset=utf-8
date: Mon, 05 May 2025 06:05:53 GMT
etag: "unkp6wcu7s17a4u"
expires: Thu, 01 Jan 1970 00:00:00 GMT
pragma: no-cache
ratelimit-limit: HttpRequestRatePerPort:1500/m
ratelimit-remaining: HttpRequestRatePerPort:1499
ratelimit-reset: HttpRequestRatePerPort:9s
referrer-policy:same-origin
strict-transport-security: max-age=31536000; includeSubDomains
vary: Accept-Encoding
vssaas-request-id: 71781a24-7935-4025-ab05-7d7f962f2e21
x-content-encoding-over-network: gzip
x-content-type-options: nosniff
x-ms-ratelimit-limit: 1500
x-ms-ratelimit-remaining: 1497
x-ms-ratelimit-reset: 0
x-ms-ratelimit-used: 3
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 4294967294
x-powered-by: Next.js
x-robots-tag: noindex, nofollow
x-served-by: tunnels-prod-rel-asse-v3-cluster
実際の挙動を確認したいときは、GitHub Pagesなどで計測したほうが良さそう
いまのところは、コンテンツのレンダリングを高速化する方向で作業する
現時点では5023 Elements
react-windowはCompile TimeではなくRuntimeで動作するので、Client component ただ、Subjectの情報はServer componentで取得する必要がある。
高さのハードコーディングが必要ないのが良さそう。
なぜ必要ないのか?
リストデータを一覧するとき、react-windowではindexから対応するデータを取得する処理を書かなければならない。 しかし、virtuosoでは、data引数を利用することで、要素に対応するデータの取得をユーザ側で書かなくて良い。
パフォーマンス改善の前提条件
LightHouseなどで計測できるLCPやFCPは、Dev ServerとProduction Serverで大きく変わる。
HTTPレスポンスに含まれる cache-control: no-cache が原因で、bfcacheが効かなくなることがある。
GitHub CodespacesのPort Forwarding機能からアプリケーションにアクセスする場合はこの点に注意が必要。
Visual Studio CodeからOpen Codespaceした場合のPort Forwardingでは、この問題を回避できる。
画面上にはつねに5個程度の科目だけが表示されるようになった。
結果
LightHouseスコアは30前後から77まで改善した。
理由はわからないが、bfcacheが効くようになった。
これにより、一覧ページと詳細ページをスムーズに移動できるようになった。
問題点
bfcacheで復元した画面では、移動前のスクロール位置が維持されない
読み込み時に画面がちらつく
https://scrapbox.io/files/68187a9c10071e196c165420.gif
Hydrationが完了するまでの間、SubjectCardがRenderされないのが原因なのではないか?
要素のRenderについて
157msのあたりで、SubjectList(Virtuosoを子にもつコンポーネント)がRenderされている
https://scrapbox.io/files/68188132f5771b858ba53670.png
これは画面上へのDOM要素の描画とは関係がない。React要素としてのRenderのこと
300msのあたりで、SubjectCard(リストの要素)がRenderされている
https://scrapbox.io/files/68188246577ddf1e719f128c.png
Lane名から判断すると、Hydrationは10msから始まり、280msに終わっていそう
TransitionHydration Laneのイベントは、下図のように280ms以降記録されていない
https://scrapbox.io/files/6818831a10071e196c1689ee.png
したがって、Hydrationが完了したあとにSubjectCard(リストの要素)がはじめてRenderされていると考えられる
改善するためには何が必要か?
根本的な問題は、Hydration完了後にはじめてリストの要素がRenderされること
SSG時点で、リストの要素がHTML要素として存在していれば良いのではないか。
従来のVirtualized Renderingは、DOM要素を動的に生成するので、この問題を根本的に回避できないのでは?
この方法なら、Hydration時に全要素を存在させることと、不要な要素の描画を回避することを両立することができるかもしれない
根本的回避はできないが、どちらの問題も緩和できるはず
1. スクロール位置が復元できない問題に対して
Virtuaなどの他のライブラリを試してみる
スクロールや選択状態を履歴やURLに保存して、復元できるようにする
2. 画面がちらつく問題に対して
生成するHTMLには、HeroUIのSkeltonのみを含めるようにする
そのために、Reactのドキュメントにある useState のテクニックを使う
サーバで実行するときにはHeroUIのSkeltonがRenderされるように
ブラウザで実行するときは、Client Componentが表示されるように
分岐を実装する
現在可能なアプローチ
全科目を一画面に描画する場合
特に工夫しない場合
Scroll Restoration
適切にScroll restorationされる
ブラウザバックにかかる時間
戻るまでの時間は0.5秒程度(要計測)
content-visibilityを利用する場合
Scroll restoration
適切に行われない場合がある
ブラウザバックにかかる時間
戻るまでの時間は変化しない?(要計測)
共通するメリット
DOM構造がつねに維持されるので、ブラウザに同梱された検索機能などが動作する
JavaScriptが有効でない環境でも適切に動作する
ただ、
共通するデメリット
Virtual Scrollを利用する場合
Scroll Restoration
デフォルトでは行われない
Scroll positionを保存するような実装が必要
ブラウザバックにかかる時間
非常に軽快(ただし、Scroll Restorationを行った場合にどうなるかは未検証)
設計の焦点
詳細表示はClient Sideで行うのか?別ページにするのか?
別タブで開くケースも考えられるが、URL Persistenceを使えば同一視できるので考えなくて良い
別ページにする場合
bfcacheに期待する場合
現状では速度に不満があるので、パフォーマンスを改善する
Virtual Scrollに期待する場合
上記で検討した問題への対策が必要
Client Sideで表示する場合
とくに工夫しなくても良い気がする
content-visibilityでの最適化が上手く効きそう