すき焼き鍋のイラスト

`history.replaceState()` にはブラウザによって呼び出し回数制限がある

`history.replaceState()` は、ブラウザの履歴を変更するための API です。使用例としてユーザーのアクションによる UI の状態の変更に合わせて、URL のクエリパラメータを変更することが挙げられます。この API はブラウザにより呼び出し回数に制限が設けられており、使い方を誤ると予期せぬ挙動が発生するおそれがあります。

history.replaceState() は、ブラウザの履歴を変更するための API です。使用例としてユーザーのアクションによる UI の状態の変更に合わせて、URL のクエリパラメータを変更することが挙げられます。例えば、検索フォームに入力したキーワードをクエリパラメータに設定することで、検索結果をブックマークしたり、URL を共有したりできるようになります。

history.replaceState() にはブラウザによって制限がある

history.replaceState() は、ブラウザによって制限があることをご存知でしょうか?一定の時間内に history.replaceState() 呼び出すことができる回数に制限があるのです。試しに以下のコードを実行してみてください。

<div id="hoge"></div>
 
<script>
  window.addEventListener("load", function () {
    var query = location.search.replace("?q=", "");
    document.querySelector("#hoge").textContent = query;
  });
 
  for (var i = 0; i <= 10000; i++) {
    history.replaceState(null, null, `?q=${i}`);
  }
</script>

Google Chrome で上記のコードを実行すると、以下のような警告が発生します。

Throttling navigation to prevent the browser from hanging. See https://crbug.com/1038223. Command line switch --disable-ipc-flooding-protection can be used to bypass the protection

ブラウザのハングアップを防ぐために、ナビゲーションをスロットリングしているという警告です。実際にクエリパラメータにはループの最後の値である 10000 ではなく、199 であることが確認できます。

現実のアプリケーションにおいても、ブラウザによりスロットリングが実行されることにより、意図しない挙動を起こす可能性があります。例えば、検索フォームに高速にキーワードが入力された場合、フォームに入力された検索キーワードとクエリパラメータが一致しないおそれがあります。

ブラウザによる制限の違い

history.replaceState() の呼び出しの制限はブラウザによって異なります。正確な値ではないものの、Google Chrome、Firefox、Edge ではおおむね 50ms ごとにスロットリングして処理を行えば、ブラウザによる制限を回避できるようです。ただし、Safari ではより厳しい制限があるようです。30s に 100 回の呼び出しに制限されています。

SecurityError: Attempt to use history.replaceState() more than 100 times per 30 seconds

解決策

history.replaceState() の呼び出しの制限を回避するためには lodash.throttle などのスロットリング関数を利用することが考えられます。

<script type="module">
  import { throttle } from "https://cdn.skypack.dev/lodash";
  const throttled = throttle((url) => {
    history.pushState(null, null, url);
  }, 100); // この値は適当
 
  window.addEventListener("load", function () {
    setTimeout(() => {
      var query = location.search.replace("?q=", "");
      console.log(query);
      document.querySelector("#hoge").textContent = query;
    }, 100); // history.replaceState() の呼び出しが遅れるので、ここでも setTimeout でちょっと待つ必要がある
  });
 
  const loopCount = 10000;
  for (var i = 0; i <= loopCount; i++) {
    throttled(`?q=${i}`);
  }
</script>

まとめ

  • history.replaceState() は、ブラウザの履歴を変更するための API
  • ブラウザによって history.replaceState() の呼び出し回数に制限がある
  • lodash.throttle などのスロットリング関数を利用することで、ブラウザによる制限を回避できる

参考


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事