弱い回線での交換ページ:3Gで何が壊れるか(2026年実測)
Slow 3Gでは交換ページが1.6秒で「使えそう」に見えますが、実際に使えるのは6.8秒後。実測では、早すぎる入力は消え、遅いタイピングは4回の見積を呼び、順不同の応答が表示価格を入れ替えます。
2026年11月6日14:03 UTC、新しいモバイルChromiumプロファイルで veilswap.io/exchange を開き、ChromeのSlow 3G(往復400ms、上下400kbps)に絞って全リクエストを記録した。入力欄は1.6秒で見えるが、最初のデータ要求 /api/currencies は6.84秒まで出ず、Reactは6.84秒にようやく動き出す。その間に40リクエストで235KB。最大のJSチャンク3つ(54.7、47.0、28.2KB)だけで6.09秒、5.91秒、5.04秒かかった。見積の本体は181バイト、API自体は0.44〜1.10秒——価格はボトルネックではなかった。制限なしなら最初の要求は0.589秒。弱回線でページが遅いのは、重いからだ。
失敗1:フォームは先に見え、命は後から入る。 1.93秒で100を入力した。サーバー描画のHTMLは画面にあり、JSはまだダウンロード中。15秒後、入力欄は空で見積リクエストは0件だった。ハイドレーションがReactの初期状態(空文字)でDOM値を上書きし、入力は警告なしに消えた。Reactのバグではなく、「フォームをHTMLで、状態をJSで出す」ハイドレーションの必然だ。速い回線では最初の要求が0.589秒、Slow 3Gでは6.84秒。通貨数かレートが出るまで入力せず、消えたら打ち直す。
失敗2:500msのデバウンスは速く打つ人にしか効かない。 コードは最後のキーから500ms待って /api/quote を送る。100を一度にfillすると1リクエスト、1.49秒後に受取額が出た。1文字900msで「1000」と打つと4リクエスト——amount=1が+0.51秒、10が+1.42秒、100が+2.32秒、1000が+3.23秒——最大2本が同時に飛ぶ。最初の2本は400で返る。1と10 USDTがこのペアの下限19.219061 USDTを下回るからだ。往復は0.7〜1.1秒。見積APIはIPあたり10分60回で、検証の前に数えるため、遅い編集およそ15回で使い切る。8秒キャッシュのキーは from|to|amount。同じ100 USDT-TRXを5秒以内に繰り返すと240〜295msで同じ181バイトが返り、1セント変えるとミスする。 (XMR→USDT 出口ルート)
失敗3:応答は順不同で届き、古い方が新しい方を上書きしうる。 見積のeffectにはAbortControllerも連番もなく、どの応答もそのままsetQuoteする。14:07 UTCの実見積2つ(100 USDT→0.14082928 XMR、1,000→1.592011)で、1本目を2.5秒、2本目を0.1秒遅らせた。100→1,000と入力すると画面は0.140829のまま、入力は1,000——11.3倍の過少表示。逆に遅らせると入力100で受取行は1.592、レート行は0.00159183、正しい0.00140888より13.0%高い。注文APIは再見積しリクエスト金額で建てるので約定は古い価格にならない——が、画面は間違っている。レートが落ち着くまで待つか、注文前に再読み込みを。 (XMR→USDT 出口ルート)
失敗4:注文ページは5秒ごとにポーリングし、タイムアウトも在途ガードもない。 コードは /api/order/<id> を即座に呼び、setInterval(5000) を回す。各呼び出しは独立で、AbortControllerも「前の要求が開いていたらこの拍を飛ばす」判定もない。偽の waiting_deposit を0.5秒で返し、以後を全部保留した。リクエストは1.6、7.1、12.1、17.1、22.1、27.1、32.1秒——ちょうど5.000秒間隔——最後は6本が同時に飛んでいた。ページは「入金待ち」を出し続け、琥珀色の「状態同期が遅延」バナーは出ない。あれは上流が pollError を返したときだけ設定され、fetchのハングや非2xxでは出ない。モバイルの背景タブはさらにタイマーを絞る。注文自体はこのページに依存しない。入金アドレスはプール側で、出金はプールの時計で進む。
失敗5:再試行の値段は見た目より高い。 Exchangeボタンは creating が true の間だけ無効で、クリックハンドラに第二のガードはない。本当の壁はサーバー側:同一IP・同一の fromAsset|toAsset|fromAmount|toAddress が90秒以内なら、どのプールも呼ばず429 duplicate orderを返す。14:06 UTC、意図的に下限未満の1 USDTで測った。最初のPOSTは1.128秒で400 amount below minimum(下限19.219061、上限165,135.014302)、完全に同じ2回目は0.234秒で429 duplicate order。安くて正しい。高くつくのは、IPあたり6件/時の注文リミッタがフィールドとアドレス検証の前に走ること。失敗した試行も枠を1つ食う。返金アドレスを6回間違えれば、その時間は本物の注文が出せない。住所はフォームで直し、duplicate orderを見たら金額をいじらず注文リンクを開く。 (XMR→USDT 出口ルート)
3Gでどうするか、ページは何を直すべきか。 ユーザー側:通貨数かレートが出るまで入力しない。遅い回線なら金額を貼り付ける。編集のたびに受取行が止まるのを待ち、「1 USDT ≈」のレートが金額と合うか確認する。スピナーが10秒を超えたら「まだ」であって「失敗」ではない。注文リンクは控えるが、弱回線で注文タブを1時間開けない。ページ側:デバウンスをRTTに適応させ、前の見積をabortして順序外の応答を無視し、71KBの通貨カタログを2コンポーネントが別々に取る(圧縮13.9KB×2)のを1回にし、ハイドレーションを止める初期バンドルを小さくし、ポーリングにタイムアウト・バックオフ・visibilitychange時の即時更新を入れ、ハイドレーション完了までフォームを無効にする。 (XMR→USDT 出口ルート)
結び:交換は生き残る。弱いのは画面だ。 KYCなしの交換の本体はアドレスと入金。資金はプール側にあり、プールの時計で出金されるので、回線が悪くても作成済みの注文は取り消せない。回線ができるのは、数字を間違えて見せる、入力を飲み込む、二度押させることだ。執筆時点(2026-11-06 14:0x UTC)、100 USDT-TRXは0.14082928 XMR(1ルート、validUntil 120秒)、1,000は1.592011 XMR(2ルート)。同じペアの下限19.219061、上限165,135.014302 USDT。カタログは436資産・65ネットワーク。数字は動く。ページのライブ見積が正だ。 (XMR→USDT 出口ルート)