Closed17

Remix+CloudflareでWebサイトを作る 40(v2.13でheaders機能せず、TTFBが3.8sかかってる問題、計測する、Waterfallを理解する)

saneatsusaneatsu

【2024-10-29】Remix v2.13を使ってからheadersが使えなくなっているせいかBasic認証が機能していない

背景

https://zenn.dev/link/comments/5cca3749bb46d0

ここのような実装で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",
});

公式ドキュメントによると?

https://remix.run/docs/en/main/route/headers

上記ブログでは「直接変更」という記述があったが公式ドキュメントを見る限りheaders使ってそうだけど。

結論

色々試してみたけど解決できず。
絶対にないとダメな機能でもないし気が向いたらまたやろ。

その他

zett-8zett-8

自分も同じ問題で詰まってこの記事にたどり着きました。
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,
  })
}
saneatsusaneatsu

いえいえ、ありがとうございます!

全部のドキュメント要求に対してBasic認証のヘッダーつけてしまおうっていうお話ですよね。
同じように enety.server.tsx に書いてみたんですけどそれでもrequest.headers.get("Authorization") がnullになってしまったんですよね。
それと、1ページだけBasic認証を実行したいので全ページのヘッダーにセットしたくないな...という😔

わざわざコメントくださりありがとうございました!
解決策探ってみます🙇🏻‍♂️

saneatsusaneatsu

【2024-10-29】D1→Tursoにしたしトランザクション処理を書こう

背景

https://zenn.dev/link/comments/0a03cf798206f1

前にScrapにD1ではトランザクション処理を書くとエラーになるという話を書いたけど、D1→Tursoにしたからトランザクション処理のコード書けるようになってるんじゃないか??

Tursoはできそう?

https://docs.turso.tech/data-and-connections#transactional-consistency

バッチトランザクションと対話型トランザクションの両方を含む libSQL のトランザクションは、SQLite のトランザクションセマンティクスに準拠しています。

libSQL は読み込み操作に対してスナップショット分離を提供し、同じプロセス内での書き込みの即時可視性を保証します。 これにより、直列化と他のトランザクションからの分離が保証されます。

できそう

追記:できた

saneatsusaneatsu

TursoにしたしD1のError in performIO: Error: D1_ERROR: too many SQL variables at offset 503: SQLITE_ERROR も解決された

そういや、D1のときに100件以上データを取得しようとしたら Error in performIO: Error: D1_ERROR: too many SQL variables at offset 503: SQLITE_ERROR というエラーが発生してたから最大取得件数を80件くらいに制限してた。

Tursoに移行したので適当に200件くらいまで取得できるようにしたけど問題なく動いている。

saneatsusaneatsu

【2024-10-30】ルートドキュメントの読み込みに3800msかかっている

背景

https://zenn.dev/link/comments/b2da4dffea4104

ここらへんで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でキャッシュは効いてて初期描画だけが遅い、みたいな話ならめっちゃシンプルにオリジンサーバーが遠い的なこと?

https://www.cloudflare.com/ja-jp/network/

ほぼすべてのサービスプロバイダーとクラウドプロバイダーに直接接続するCloudflareネットワークは、世界のインターネット人口のおよそ95%から約50ミリ秒圏内にあります。

これはCDNエッジサーバーに対する言及であって、オリジンサーバーのことじゃないよな。

https://zenn.dev/moutend/articles/97c98a277f4bae#workerは世界中にデプロイされる

デプロイしたコードはどこに設置されたサーバーで実行されるのでしょうか。日本でしょうか、あるいは米国でしょうか。
答えは世界中です。

これも「世界中の50msでアクセスできるCDNエッジサーバーがある」ということなのかな。

オリジンサーバー

https://developers.cloudflare.com/fundamentals/basic-tasks/protect-your-origin-server/#:~:text=Your origin server ↗ is,be bad for your origin.

オリジンサーバーはCloudflareが所有しない物理的または仮想的なマシンで、アプリケーションのコンテンツ(データ、ウェブページなど)をホストしています

saneatsusaneatsu

https://www.s3lab.co.jp/blog/remix/1946/

サーバーサイドレンダリング(SSR)を行い、初期ページロード時にHTMLを生成してクライアントに送信します。これにより、SEOが向上し、初期表示が高速化されます。

完全に良さを殺している。悲しいな...。

saneatsusaneatsu

Cloudflare Pages?

https://zenn.dev/link/comments/4512f7653643f4

初めに見た記事でCloudflare Pagesを使っていたから使い続けているけどここが原因だったりしないのかな。静的じゃない的な意味で。

https://yusukebe.com/posts/2024/cloudflare-workers-updates/

Cloudflare Workersの他にPagesというのがありまして、一般的にはNext.jsやAstroなどのフレームワークを用いて「フルスタック」なアプリケーションを作るプラットフォームです。 以前はWorkersとPagesは明確に分かれていたのですが、それが統合されつつあります。

Pagesはもともと静的なページをホストするのが主なユースケースだったのが、「SSR」というキーワードから分かる通り、ダイナミックな処理が増えてきました。 そのPagesのダイナミック処理はWorkersで動いているので、その境界線が曖昧になるんですね。

と思ったが違いそう。

Cloudflare PagesのFramework guidesにもRemixいるし。

Cloudflare Workers?

https://zenn.dev/link/comments/eedf10472db184

それか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の制限

そもそもCloudflare Workersがこのアプリにミスマッチでは

https://zenn.dev/catnose99/articles/dfc9c1197daec3

(仮に自分がZennのようなサービスを1から作るとしたら、Remix on Cloudflare Workersというスタックにはしないと思います)

これは2年前の記事だけど機能もりもりなこのサイトとCloudflare Workersは今回のTTFB遅い問題以外にもサイズという観点でミスマッチそう。

Service Binding?

https://zenn.dev/chimame/articles/0207636dea9c78

Service Bindingしても結局アプリサイズデカくなっていくことを考えてしまうと最善の方法ではない?

saneatsusaneatsu

コンポーネントライブラリがtreeshake効かないことが多い...

スライドの最後にあった一文。
treeshakeの意味がわかってないのでAIに聞いてみた。

「コンポーネントライブラリにtreeshakeが効かない」というのは、コンポーネントライブラリをインポートした際に、実際には使っていない不要なコードも一緒にバンドルに含まれてしまうことを指します。通常、Tree Shaking(ツリーシェイキング)は、JavaScriptバンドルを最適化するための技術で、使用されていないコード(未使用の関数やクラスなど)を自動的に削除し、ファイルサイズを小さくする役割を持っています。

しかし、コンポーネントライブラリによっては、この最適化がうまく機能しない場合があります。これは主に以下のような理由が原因です:

  1. インポート方式: ライブラリが「名前付きエクスポート」ではなく、「デフォルトエクスポート」を使用している場合、Tree Shakingが正しく効かないことが多いです。デフォルトエクスポートはモジュール全体をまとめて扱うため、未使用の部分も削除されにくくなります。
  1. ESM(ES Modules)サポートの欠如: Tree ShakingはES Modules形式(import/export)で効果を発揮しやすいですが、ライブラリがCommonJS形式(require/module.exports)で提供されていると、ビルドツールが正確に依存関係を解析できず、不要なコードが含まれてしまいます。
  1. コードの内部依存関係: ライブラリ内で他のコンポーネントや関数同士が強く依存している場合、未使用のコンポーネントだけを削除するのが難しくなります。

例として、import { Button } from 'ui-library'と書いた場合に、Buttonコンポーネントだけでなく、ui-libraryの他の全てのコンポーネントがバンドルに含まれてしまう可能性があります。これは、最終的にアプリのビルドサイズを大きくし、パフォーマンスに悪影響を与える原因となります。

saneatsusaneatsu

【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の間だけ数秒だったんだけどこのときはかなり早い。

参考

https://qiita.com/y_fujieda/items/a0a69151cf7307039f74
https://zenn.dev/koki_tech/articles/9deb70d0a9befb
https://ayudante.jp/column/2022-12-01/15-00/
https://qiita.com/shizen-shin/items/b619586a293de8cfe7da

saneatsusaneatsu

【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圧縮
  • ミニフィケーション(圧縮)
  • 画像の最適化

参考

saneatsusaneatsu

【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)

参考

このスクラップは2ヶ月前にクローズされました