◉CLAUDE LABEN
●2.1.311 — セルフホステッドランナーの待機保持オプションを追加し、起動時のフリーズや割り込みの停止を修正しました(10/26)●2.1.307 — スリープやキーチェーンロックのあとに MCP が再接続されない問題を修正しました(10/21)。2.1.306 では claude -p の誤表示も解消●SONNET 4.5 — Claude API での提供終了は 11/30、残り33日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.311 — セルフホステッドランナーの待機保持オプションを追加し、起動時のフリーズや割り込みの停止を修正しました(10/26)●2.1.307 — スリープやキーチェーンロックのあとに MCP が再接続されない問題を修正しました(10/21)。2.1.306 では claude -p の誤表示も解消●SONNET 4.5 — Claude API での提供終了は 11/30、残り33日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●API — 新しい API バージョン 2026-10-21 では temperature・top_p・top_k を渡すと 400 になります●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-10-28上級

決済ページから「戻る」と処理中のまま固まるボタンを、pageshow で直すと決めた理由。監査したら 3 ファイルに残っていました

Stripe などの外部ページから戻ると、決済ボタンが「処理中」のまま動かなくなる bfcache の症状を実ブラウザで再現し、pageshow と no-store の比較、リポジトリ監査スクリプト、CLAUDE.md の一行までをまとめます。

Claude Code277bfcacheStripe16Playwright3CLAUDE.md6

✦ プレミアム記事

決済ページを開いて、ブラウザの「戻る」を押します。戻った先のボタンは「処理中…」のまま灰色になり、何度押しても反応しません。

個人開発のメンバーシップページで、実際に出会った症状です。再読み込みすれば直るので、見過ごされやすいところが厄介でした。購入に迷って一度戻った人は、そのまま離れてしまうかもしれません。

しかも、API の呼び出しは失敗していません。ログを見ても、手がかりが残らない症状です。

起きていたのは「ページが凍結されて、そのまま戻ってきた」ことでした

ブラウザは、遷移して離れたページを bfcache(Back-Forward Cache)という場所に、メモリごと凍結して保管することがあります。戻るときは、読み込み直さずに凍結を解きます。

React のコンポーネントは再マウントされません。useState の値も、loading が true のまま残ります。ボタンに disabled={loading !== null} と書いてあれば、そのまま押せなくなります。

外部サービスへ遷移するボタンは、「戻ってきた自分」を想定して書かないと、状態だけが過去に取り残されます。

見分ける手がかりは、pageshow イベントの persisted です。bfcache から復元されたときだけ true になります。

まず、実ブラウザで再現しました

手元の感覚だけでは、直った確認が弱くなります。そこで、React を使わず、loading 変数と文言だけで同じ構造を作り、Playwright で「押す → 外部へ遷移 → 戻る」を自動で通しました。

// bfcache-check.js — 決済ボタンが「戻る」で固まらないかを実ブラウザで確かめる
// 使い方: node bfcache-check.js   (playwright が入っていること)
const http = require('http');
const { chromium } = require('playwright');
 
// 検証用のページ。React の代わりに、loading 変数と文言だけで同じ構造を再現する
const page_html = (guard, payUrl) => `<!doctype html><meta charset="utf-8"><title>plans</title>
<button id="join">Join</button><p id="state">idle</p><p id="evt">none</p>
<script>
  let loading = false;
  const $ = (id) => document.getElementById(id);
  $('join').onclick = async () => {
    loading = true; $('state').textContent = 'loading';
    await new Promise((r) => setTimeout(r, 50));      // /api/checkout の待ち時間の代わり
    location.href = '${payUrl}';                        // Stripe へ遷移
  };
  addEventListener('pageshow', (e) => {
    $('evt').textContent = 'persisted=' + e.persisted;
    ${guard ? "if (e.persisted) { loading = false; $('state').textContent = 'idle'; }" : ''}
  });
</script>`;
 
const serve = (port, handler) =>
  new Promise((ok) => { const s = http.createServer(handler).listen(port, () => ok(s)); });
 
const CASES = [
  { name: 'A 対策なし',                guard: false, noStore: false },
  { name: 'B pageshow で解除',         guard: true,  noStore: false },
  { name: 'C 対策なし + no-store',     guard: false, noStore: true  },
];
 
(async () => {
  let failed = 0;
  for (const c of CASES) {
    const pay  = await serve(4102, (_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end('<title>pay</title>pay'); });
    const html = page_html(c.guard, 'http://localhost:4102/pay');
    const site = await serve(4101, (_, r) => {
      const h = { 'content-type': 'text/html' };
      if (c.noStore) h['cache-control'] = 'no-store';
      r.writeHead(200, h); r.end(html);
    });
    // Playwright は既定で bfcache を切っているので、その引数を外して起動する
    const browser = await chromium.launch({ channel: 'chromium', ignoreDefaultArgs: ['--disable-back-forward-cache'] });
    const page = await browser.newPage();
    await page.goto('http://localhost:4101/');
    await page.click('#join');
    await page.waitForURL(/\/pay/);
    // page.goBack() は bfcache 復元時に load を待って固まるため、history.back() を使う
    await page.evaluate(() => history.back());
    await page.waitForTimeout(1200);
    const state = await page.textContent('#state');
    const evt = await page.textContent('#evt');
    const ok = state === 'idle';
    if (!ok) failed++;
    console.log(`${ok ? 'PASS' : 'FAIL'}  ${c.name.padEnd(22)} state=${state.padEnd(8)} ${evt}  (Chromium ${browser.version()})`);
    await browser.close(); site.close(); pay.close();
  }
  process.exit(failed ? 1 : 0);
})();

Playwright 1.63.0・Chromium 153 で実行した結果です。

FAIL  A 対策なし                 state=loading  persisted=true  (Chromium 153.0.8010.12)
PASS  B pageshow で解除         state=idle     persisted=true  (Chromium 153.0.8010.12)
PASS  C 対策なし + no-store     state=idle     persisted=false  (Chromium 153.0.8010.12)

A が本番で起きていた症状です。戻った直後に persisted=true なのに、文言は loading のままでした。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦決済ボタンが「戻る」で処理中のまま固まる症状を、実ブラウザで再現して、直ったかどうかまで自分で確かめられるようになる
✦pageshow で直すか、Cache-Control: no-store で bfcache ごと止めるかを、戻る操作の速さと再発のしやすさで判断できるようになる
✦自分のコードのどこに同じ穴が残っているかを、障害の報告が来る前に監査スクリプトで洗い出して塞げるようになる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⟐ Claude Code2026-10-20
席を離れて戻ると会話に要約が差し込まれていた — アイドル圧縮の見分け方と、消したくない細部の残し方
v2.1.299 で入ったアイドル圧縮により、約54分放置した長い会話は自動で要約されます。何が起きたかの見分け方、止め方、そして要約に消されたくない前提を CLAUDE.md へ退避させる手順をまとめます。
⟐ Claude Code2026-10-19
375px で横に 90px はみ出した朝、犯人は querySelectorAll に映らない疑似要素でした
スマホ幅でページが横に広がる原因が ::after だったとき、要素の走査では見つかりません。疑似要素まで差分で確定する観測スクリプトと、sticky を壊さない直し方を実測つきでまとめました。
⟐ Claude Code2026-10-27
Claude Code の新版が出る間隔を、npm の公開履歴から数えてみました
Claude Code は週に何本、何時間おきに公開されているのか。npm の公開履歴だけを使い、週ごとの本数と公開間隔の中央値を数える 37 行の Python を組みます。0 本の週が抜け落ちる落とし穴と、火曜の朝に数字が少なく見える理由も添えました。