Remix+CloudflareでWebサイトを作る 40(v2.13でheaders機能せず、TTFBが3.8sかかってる問題、計測する、Waterfallを理解する)
【2024-10-29】Remix v2.13を使ってからheadersが使えなくなっているせいかBasic認証が機能していない
背景
ここのような実装でBasic認証を行っている。
ずっと開発環境でキャッシュがある状態で動かしていて気が付かなかったがステージング環境でBasic認証が動かなくなっていた。
具体的には以下のheader
がnullになってしまいBasic認証の入力欄が出る前に認証エラーとなってしまう。
// コードは上記のサイトのものを引用
import { HeadersFunction, json, LoaderFunction, useLoaderData } from "remix";
export const headers: HeadersFunction = () => ({
"WWW-Authenticate": "Basic",
});
const isAuthorized = (request: Request) => {
const header = request.headers.get("Authorization"); // ここが常にnullになってしまう
if (!header) return false;
const base64 = header.replace("Basic ", "");
const [username, password] = Buffer.from(base64, "base64")
.toString()
.split(":");
// 環境変数などでIDとパスワードを渡す
return username === "admin" && password === "password";
};
export const loader: LoaderFunction = async ({ request }) => {
if (!isAuthorized(request)) {
return json({ authorized: false }, { status: 401 });
}
// 認証されたページで利用するデータ
return json({
authorized: true,
// 認証されたページで利用するデータを送る
});
};
v2.13から headers関数が変わった?
Remix v2.9 で導入された Single Fetch
Single Fetch では headers 関数を export していても、その値はもはや使用されません。代わりに loader/action 関数の引数で受けとる response オブジェクトを直接変更することでレスポンスヘッダーやステータスコードを設定できます。
そういやだいぶ前にSingle Fetchについてこのサイトを見たときにheadersについての言及があったな、と思ったのでheadersの仕様を調べる。
// つまり、ここの書き方がバージョンアップによって変わったのでは?ということ
export const headers: HeadersFunction = () => ({
"WWW-Authenticate": "Basic",
});
公式ドキュメントによると?
上記ブログでは「直接変更」という記述があったが公式ドキュメントを見る限りheaders使ってそうだけど。
結論
色々試してみたけど解決できず。
絶対にないとダメな機能でもないし気が向いたらまたやろ。
その他
-
「HTTP」の仕組みをおさらいしよう(その4):リトライ! 触って学ぶTCP/IP(5)(1/2 ページ) - @IT
-
WWW-Authenticate
のおさらい
-
自分も同じ問題で詰まってこの記事にたどり着きました。
entry.server.tsx
で header set することで解決しました。
残りのコードは同じです(Buffer だとエラーになったので atob を使いましたが)
余計なお世話でしたらすみません🙏
// entry.server.tsx
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
// 省略...
responseHeaders.set('WWW-Authenticate', `Basic realm="User Visible Realm", charset="UTF-8"`)
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
}
いえいえ、ありがとうございます!
全部のドキュメント要求に対してBasic認証のヘッダーつけてしまおうっていうお話ですよね。
同じように enety.server.tsx
に書いてみたんですけどそれでもrequest.headers.get("Authorization")
がnullになってしまったんですよね。
それと、1ページだけBasic認証を実行したいので全ページのヘッダーにセットしたくないな...という😔
わざわざコメントくださりありがとうございました!
解決策探ってみます🙇🏻♂️
【2024-10-29】D1→Tursoにしたしトランザクション処理を書こう
背景
前にScrapにD1ではトランザクション処理を書くとエラーになるという話を書いたけど、D1→Tursoにしたからトランザクション処理のコード書けるようになってるんじゃないか??
Tursoはできそう?
バッチトランザクションと対話型トランザクションの両方を含む libSQL のトランザクションは、SQLite のトランザクションセマンティクスに準拠しています。
libSQL は読み込み操作に対してスナップショット分離を提供し、同じプロセス内での書き込みの即時可視性を保証します。 これにより、直列化と他のトランザクションからの分離が保証されます。
できそう
追記:できた
Error in performIO: Error: D1_ERROR: too many SQL variables at offset 503: SQLITE_ERROR
も解決された
TursoにしたしD1のそういや、D1のときに100件以上データを取得しようとしたら Error in performIO: Error: D1_ERROR: too many SQL variables at offset 503: SQLITE_ERROR
というエラーが発生してたから最大取得件数を80件くらいに制限してた。
Tursoに移行したので適当に200件くらいまで取得できるようにしたけど問題なく動いている。
【2024-10-30】ルートドキュメントの読み込みに3800msかかっている
背景
ここらへんでCacheルールを設定したりJoy UIではなくshadcnを使ったりしてサーバーの応答速度を早くできたと思っていたけど最近またどうも遅い。
本番環境をLighthouseで測定してみる。
何も画像なども表示されておらず、DOMも極限まで少ないページでこの速度。
以前は600ms以内に収まっていた。
Remixのコードはめっちゃ普通なはずだからCloudflare側の設定がミスっているとは思うんだけどどうすりゃいいのかわからない。
結構致命的だな...。
結構致命的だと思うのにLighthouseのスコア94点なんだよな。もっと下げたほうが良くないか。
キャッシュは効いてる
再度図ったら20ms。キャッシュは効いてるんだよな。
同じURLでのキャッシュはうまく効いてるんだけど初期描画がめっちゃ遅いから、新しいページに遷移するたびにめっちゃ遅い。
追記:
1h後に同じページで計測を実行したら3340msかかった。
考えられる原因
1. HTMLファイルのサイズ
https://tools.pingdom.com/ で計測したところ141.1KB。
Test fromは東京。1回目は1.75s、2回目は821ms。
ちなみにサンフランシスコにしたら2.08s、2回目は1.48sかかった。
ちなみにZennのトップページは825KBだが、Load timeが全然短い。
なんとなく思っていたけどやはりファイルサイズは直接の原因じゃないのでは。
2. サーバーが遠い?
RemixはSSRでキャッシュは効いてて初期描画だけが遅い、みたいな話ならめっちゃシンプルにオリジンサーバーが遠い的なこと?
ほぼすべてのサービスプロバイダーとクラウドプロバイダーに直接接続するCloudflareネットワークは、世界のインターネット人口のおよそ95%から約50ミリ秒圏内にあります。
これはCDNエッジサーバーに対する言及であって、オリジンサーバーのことじゃないよな。
デプロイしたコードはどこに設置されたサーバーで実行されるのでしょうか。日本でしょうか、あるいは米国でしょうか。
答えは世界中です。
これも「世界中の50msでアクセスできるCDNエッジサーバーがある」ということなのかな。
オリジンサーバー
オリジンサーバーはCloudflareが所有しない物理的または仮想的なマシンで、アプリケーションのコンテンツ(データ、ウェブページなど)をホストしています
参考になったサイトメモ
サーバーサイドレンダリング(SSR)を行い、初期ページロード時にHTMLを生成してクライアントに送信します。これにより、SEOが向上し、初期表示が高速化されます。
完全に良さを殺している。悲しいな...。
mizchiさんのこういう仕事かっこいいよなぁ
Cloudflare Pages?
初めに見た記事でCloudflare Pagesを使っていたから使い続けているけどここが原因だったりしないのかな。静的じゃない的な意味で。
Cloudflare Workersの他にPagesというのがありまして、一般的にはNext.jsやAstroなどのフレームワークを用いて「フルスタック」なアプリケーションを作るプラットフォームです。 以前はWorkersとPagesは明確に分かれていたのですが、それが統合されつつあります。
Pagesはもともと静的なページをホストするのが主なユースケースだったのが、「SSR」というキーワードから分かる通り、ダイナミックな処理が増えてきました。 そのPagesのダイナミック処理はWorkersで動いているので、その境界線が曖昧になるんですね。
と思ったが違いそう。
Cloudflare PagesのFramework guidesにもRemixいるし。
Cloudflare Workers?
それかCloudflare Workersに課金するレベルでサイズが大きいことがネックだったりしないのだろうか。とはいえ、圧縮後のサイズは1.8Mなので余裕あるけども。
$ npx wrangler deploy 'functions/[[path]].ts' --outdir bundled/ --dry-run
> Total Upload: 1420.32 KiB / gzip: 346.75 KiB # Before(2024-03-02)
> Total Upload: 7797.91 KiB / gzip: 1857.75 KiB # After(2024-11-07)
> Total Upload: 4734.31 KiB / gzip: 1558.23 KiB # After(2024-11-07)--minify オプション追加
そもそもCloudflare Workersがこのアプリにミスマッチでは
(仮に自分がZennのようなサービスを1から作るとしたら、Remix on Cloudflare Workersというスタックにはしないと思います)
これは2年前の記事だけど機能もりもりなこのサイトとCloudflare Workersは今回のTTFB遅い問題以外にもサイズという観点でミスマッチそう。
Service Binding?
Service Bindingしても結局アプリサイズデカくなっていくことを考えてしまうと最善の方法ではない?
Cloudflare Workers を使う前に知っておきたい注意点 でも参照されているこのスライドを見てみる。
CDN Edgeで動かすパフォーマンスメリットのためにもやっぱり1MBをパフォーマンスバジェットとして設定したい
このアプリで長期的にCloudflare Workers使い続けるのどうなんだろうか。
コンポーネントライブラリがtreeshake効かないことが多い...
スライドの最後にあった一文。
treeshakeの意味がわかってないのでAIに聞いてみた。
「コンポーネントライブラリにtreeshakeが効かない」というのは、コンポーネントライブラリをインポートした際に、実際には使っていない不要なコードも一緒にバンドルに含まれてしまうことを指します。通常、Tree Shaking(ツリーシェイキング)は、JavaScriptバンドルを最適化するための技術で、使用されていないコード(未使用の関数やクラスなど)を自動的に削除し、ファイルサイズを小さくする役割を持っています。
しかし、コンポーネントライブラリによっては、この最適化がうまく機能しない場合があります。これは主に以下のような理由が原因です:
- インポート方式: ライブラリが「名前付きエクスポート」ではなく、「デフォルトエクスポート」を使用している場合、Tree Shakingが正しく効かないことが多いです。デフォルトエクスポートはモジュール全体をまとめて扱うため、未使用の部分も削除されにくくなります。
- ESM(ES Modules)サポートの欠如: Tree ShakingはES Modules形式(import/export)で効果を発揮しやすいですが、ライブラリがCommonJS形式(require/module.exports)で提供されていると、ビルドツールが正確に依存関係を解析できず、不要なコードが含まれてしまいます。
- コードの内部依存関係: ライブラリ内で他のコンポーネントや関数同士が強く依存している場合、未使用のコンポーネントだけを削除するのが難しくなります。
例として、import { Button } from 'ui-library'と書いた場合に、Buttonコンポーネントだけでなく、ui-libraryの他の全てのコンポーネントがバンドルに含まれてしまう可能性があります。これは、最終的にアプリのビルドサイズを大きくし、パフォーマンスに悪影響を与える原因となります。
【2024-10-30】Networkタブで計測する
ChromeのインスペクタのNetworkでTimingを見る。
また、Cloudflareの設定でキャッシュは無効化した。
見る値は以下2つ。
- Waiting for server response
- Lighthouseで3800msかかったと言われているTTFB(Time to First Byte)にあたる
- サーバーがリクエストを受け取ってからレスポンスを返すまでの(初回のレスポンスまでにかかった)時間
- Content Download
- ブラウザがサーバーからのレスポンスを受け取るまでにかかった時間
測定
1回目
2回目
2回目にアクセス時のHTMLのNetwork。
3回目
4回目は3回目と変わらなかったのでスクショ無し。
5回目
まとめ
特に遅いところを太字にしている。
回数 | TTFB | Content Download | Explanation |
---|---|---|---|
1回目 | 847ms | 2.41s | 3.78s |
2回目 | 195ms | 633ms | 837ms |
3回目 | 653ms | 216ms | 889ms |
4回目 | 636ms | 159ms | 795ms |
5回目 | 620ms | 736ms | 1.44s |
6回目 | 677ms | 553 | 1.35s |
7回目(6回目のあとすぐ実行) | 131ms | 123ms | 275ms |
8回目(7回目から2分後) | 158ms | 346ms | 509ms |
LighthouseにはTTFBが遅いと言われたけど、初期描画に関してはContent Downloadも大概遅いな。
この2つの速度が結構ばらつきあるんだけどどこで決まっているんだろう。
各計測では間に1分くらい感覚があった。
6、7の間だけ数秒だったんだけどこのときはかなり早い。
参考
Content Download が4.83sかかったときもある...。
【2024-10-30】NetworkのWaterfallの項目を理解する
Resource Scheduling
Resource Schedulingはリクエストの順番を決定するためのスケジューリングにかかった時間のこと。
Queueing
リクエストはPriorityごとにキューに登録されてから処理される(PriorityはNetworkで確認できるやつ)。
ブラウザではHTTP1のオリジン1つあたり6つの接続しか許可されない
Connection Start
キューに入った後に、リクエストを送信するまでの時間。
Stalled
ブロッキングともいう。
リクエストを送信するまでの待機状態。
Priorityが低いリクエストはQueueingとStalledが長くなる。
違いが良くわからなかったのでAIに聞いた。
DNS Lookup
DNSを用いてドメイン名からホスト名・IPアドレスを調べたり、逆をすること。
ブラウザがサーバーと通信するには、まずDNSルックアップを行う必要がある。
これに関してできることはあまりない。
また、すべてのリクエストで行われるわけではない。
1つ上のScrapでは初回は400〜500msかかってるけどそれ以降はキャッシュされているので数msで済んだ。
Initial connection
ラウザがリクエストを送信するには、TCP接続を確立しなければならない。
最初だけ行われるものだが、それ以降も行われている場合はパフォーマンスに問題がある。
例えば以下図ではオレンジ色がInitial connectionにあたるが、TCP接続の確率が必要なのは最初の数件だけなので、この例では持続的接続(Keep Alive)を行なえていないということになる。
SSL
(「SSL」しか書いていないけど、「SSL/TLSネゴシエーション」のことかな)
ページがSSL/TLSを介してリソースをセキュアに読み込んでいる場合、どのように暗号化するのかを決めてい、接続を確立している時間(SSLハンドシェイク)。
SEOの観点だとGoogleはHTTPSを検索順位を決定する要因の1つにしているらしい。
ここが長い場合はTLSの設定に問題があるということになる。
Waiting for server response
今悩んでいるTTFB。
サーバーの応答時間を示す指標で、ブラウザがサーバーからデータの最初の1バイトを受け取るまでにかかる時間を計測している。
TTFB が長いと、それに続く FCP や LCP などの指標にも影響を与える。
ここを減らすメジャーな方法の1つがCDNの利用になる。
ref: 最初のバイトまでの時間(TTFB) | Articles | web.dev
startTime 〜 responseStart までが TTFB。
補足
-
FCP(First Contentful Paint)
- 定義: ブラウザがページの最初のコンテンツ(テキストや画像など)を画面に描画し始めた時点です。
- 意味: ユーザーが初めてページのコンテンツを視覚的に認識できるまでの時間を示します。TTFBとFCPの間には、HTMLの解析、CSSのレンダリング、DOMの構築などの時間が含まれます。
-
LCP(Largest Contentful Paint)
- 定義: ページ内で最も大きい画像やテキストブロックが視覚的に安定して表示された時点です。
- 意味: ページの主要なコンテンツが表示されるまでの時間を示します。LCPは、ユーザーがページの主要なコンテンツを視覚的に認識できるまでの時間を測る指標として重要です。
Content Download
Networkタブで時間がかかっていたところ。
ブラウザがレスポンス全体をダウンロードするのにかかる時間。
長ければ、リソースの容量が大きい、またはサーバーとの間の通信が遅いということになる。
見るべき観点の例は以下。
- HTTP圧縮
- ミニフィケーション(圧縮)
- 画像の最適化
参考
- 最初のバイトまでの時間(TTFB) | Articles | web.dev
- あなたのWebページ表示を爆速にするための、HTTPリクエスト状況分析ガイド(前編) | Moz - SEOとインバウンドマーケティングの実践情報 | Web担当者Forum
-
あなたのWebページ表示を爆速にするための、HTTPリクエスト状況分析ガイド(後編) | Moz - SEOとインバウンドマーケティングの実践情報 | Web担当者Forum
- 今回のScrapの内容とは違うけど後編も面白かった
- Chromeのデベロッパーツールでリクエストの状態を把握し、webサイトの表示スピード改善に役立てる : ビジネスとIT活用に役立つ情報(株式会社アーティス)
ここも合わせて見る
【2024-10-30】ページの描画速度とユーザー体験
- TTFBは600ms以下が良い
- 92%サイトが600ms以下
- ページの読み込みが
- 1s→3sになる:直帰率が32%上昇
- 1s→5sになる:直帰率が90%上昇
- 1s→6sになる:直帰率が106%上昇
- 1s→10sになる:直帰率が123%上昇
- 2.5s以上かかると?
- 訪問した人の53%が離れる
- 直帰率が113%
- 検索ランキングが下がる
- 1s改善されるごとにCVが2%増加した(Walmart)