ワダチを作る #15:記録の画面の CPU 時間を 1/5 にした。その途中で、D1 の「読む行の数」の落とし穴に気づいた

前回(#14)のレビューで、記録の画面が、Workers の無料プランの CPU 時間の上限(1 回 10ms)を、大きく超えていると分かりました。今回は、その直しの話です。

  • 記録の画面の CPU 時間を、41〜114ms から 8〜13ms にした
  • 途中で、1 分ごとの自動の読み直しが、D1 の無料の枠を超えかねないことに気づいた
  • 自動の読み直しをやめて、「読み直す」のボタンにした

まず、どこが重いかを測った

当てずっぽうで直さず、先に測りました。

手元で、習慣 10 個・7 年分(約 2 万 3 千件)の記録を持つユーザーを作ります。ビルドしたアプリを wrangler dev で動かし、開発者ツールの仕組み(inspector)につないで、CPU のプロファイルを取りました。20 回の平均です。

直す前の記録の画面は、手元で 1 回 57ms。内訳はこうでした。

  • 積み上がりを数えるところ:約 25ms。直近 7 日・30 日・通算を、1 日ずつ回して数えていた
  • データベースから 2 万 3 千行を受け取るところ:約 8ms
  • タイムゾーンの確かめ(日付の書式の部品を毎回作る):約 3ms

1 回目:数え方を、引き算にした

積み上がりの数え方を、日付を「日の番号」(1970-01-01 から何日目か)にして、引き算で数える形に変えました。

  • 期間の日数・やる曜日の数は、1 日ずつ回さず、引き算で出す
  • 週○回の習慣の、週ごとの回数は、記録を 1 回なめてまとめる

数え方を変えるとき、いちばん怖いのは、数字がこっそり変わることです。そこで、前の「1 日ずつ数える」形を、テストの中に見本として残しました。でたらめに作った習慣 300 個(毎日・平日・土日・週○回、休止あり、期間の外の記録あり、など)で、新しい数え方と見本の結果が、全部そろうことを確かめています。

数え方の決まりを変えるときは、見本も一緒に変えます。見本は遅いのですが、読めば決まりが分かります。

あわせて、記録の日付を、習慣ごとに 1 行にまとめて受け取るようにしました(SQL の group_concat)。受け取る行は、2 万 3 千行から、習慣の数の 10 行になります。

本番に出すと、41〜114ms が、30ms 前後になりました。ただ、まだ 10ms には届きません。

2 回目:共通の部分と、問い合わせの数

残りを、もう一度測りました。比べるために、使い方のページ(データベースをほとんど読まない)も測ると、約 5ms。どの画面にも共通の部分が、それだけかかっていました。

見えてきたのは、次の 3 つです。

  • Drizzle(データベースの部品)と Better Auth(ログインの部品)を、リクエストごとに作り直していた。それぞれ約 0.5ms
  • 記録の画面は、データベースに約 10 回、1 つずつ問い合わせていた。1 回ごとに、SQL を組み立てる手間と、送り受けの手間がかかる
  • 積み上がり用の日付を、文字(2026-10-23)で受け取って、1 つずつ数に直していた

そこで、次のように直しました。

  • Drizzle と Better Auth は、一度作ったものを使い回す
  • 記録の画面の問い合わせ約 10 回を、1 回の db.batch にまとめる
  • 日付は、SQL の中で日の番号にしてから受け取る(julianday を使う。アプリ側の数え方と同じ値になることは、手元のデータベースで SQL を実際に動かすテストで確かめた)

本番に出したあとの Observability です。

直す前 1 回目のあと 2 回目のあと
記録の画面の読み直し 41〜114ms 30ms 前後 8〜13ms

しばらく間が空いたあとの最初の 1 回だけは、45〜56ms でした。Worker が起動したてで、使い回す部品を一度だけ作るためです。

手元の数は、本番と同じにならない

途中、手元の測定で、おかしな数が出ました。ある直しで、手元では 17ms → 35ms と倍に増えたのに、受け取る文字の量は 1/10 になっていたのです。

調べると、手元のデータベース(D1 のまね)は、同じプロセスの中で動いているため、データベースが SQL を実行する重さまで、手元の CPU 時間に入って見えていました。本番では、D1 は別のところで動きます。手元の数は目安にして、最後は本番の数で確かめる、と決めました。

試してやめたこと:読む行が増えた

その「倍に増えた」直しは、続いている日を、SQL の中で「始めの日+日数」にまとめて受け取る形でした(ウィンドウ関数を使う)。ほぼ毎日付けている習慣なら、受け取る文字は 1/10 になります。

ところが、D1 が読む行の数(rows_read)を見ると、1 回の表示で 4 万 6 千行が、11 万 8 千行に増えていました。D1 の無料プランは、読む行の数に 1 日 500 万行の枠があります。この形は、やめました。

落とし穴:1 分ごとの読み直し

この「読む行の数」を見ていて、もっと大きな問題に気づきました。

少し前に、記録の画面に、自動の読み直しを入れていました。ホーム画面に追加したアプリを開いたままにしていると、翌日になっても、前の日の画面のままだったからです。最初は、「1 分以上離れて戻ってきたとき」と「日付が変わったとき」に読み直す作りでした。

受入のとき、「パソコンでチェックを付けても、開いたままのスマホに出ない」と分かりました。そこで、見えているあいだも、1 分ごとに読み直すようにして、本番に出していました。

ところが、記録の画面は、1 回の表示で、その人の全部の記録を読みます(積み上がりを数えるため)。

  • 7 年分の記録なら、1 回で約 4 万 6 千行
  • パソコンで記録の画面を開いたまま 8 時間 → 1 分ごとに約 480 回 → 約 2,200 万行
  • D1 の無料の枠は、1 日 500 万行。超えると、その日のうちは、全員の読み出しが失敗する

利用者が 1 人でも、開きっぱなしにするだけで、枠を超えかねない作りでした。

作りを変えた:日付が変わったときと、ボタン

直し方を相談すると、こう言われました。

この対応を入れたのは、翌日に開き直さないと、当日の日付にならないことが原因。日付が変わった場合のみ読みに行くのと、再読み込みのボタンを配置して、押した場合のみ読みに行くようにすれば、1 分ごとに読みに行く必要はなくなる。

そのとおりでした。1 分ごとの読み直しは、「見えたままの画面に、ほかの端末の変更を出す」ためだけのもので、ボタンで代わりが利きます。

  • 日付が変わったとき:見えているあいだは、1 分ごとに、ブラウザの中で日付だけを見る(通信しない)。変わったら読み直す
  • 1 分以上離れて、戻ってきたとき:今までどおり読み直す(アプリを開き直したとき・ロックを外したときなので、回数は限られる)
  • 「読み直す」のボタン:ほかの端末で付けた分は、これを押すと出る
スマホ幅の記録の画面。1 行目に日付「10月23日(金)」がまん中に。2 行目の左に「今日」のバッジ、右に、ぐるぐるのアイコンの「読み直す」のボタンと、‹ › のボタン。その下に 1 週間の帯と、習慣のカード

日付の見出しの右に「読み直す」のボタン(画面の中身は、架空のデータ)

ボタンの置き場所は、少し迷いました。スマホの幅では、日付・「今日」の札・ボタン 3 つが 1 行に収まりません。案を画面で作って見せてもらい、スマホでは見出しを 2 行(1 行目は日付だけをまん中に、2 行目に札とボタン)、パソコンでは 1 行にしました。

このことは、開発の決まり(CLAUDE.md)にも書きました。時間ごとに読みに行く作りを足すときは、D1 の読む行の数を、先に見積もる。

おわりに

  • 直す前に、本番に近いデータで、プロファイルを取って、どこが重いかを測った
  • 数え方を変えるときは、前の数え方を見本としてテストに残し、でたらめな入力で結果がそろうことを確かめた
  • 手元の数は目安。最後は本番の数で確かめる
  • CPU 時間だけでなく、D1 の読む行の数も、無料の枠に効く。時間ごとに読みに行く作りは、先に見積もる

次回は、トップページをメニューの画面にしたことと、見出しのタブ、画面の小さな直しの話です。

← トップページに戻る