This is a drop-in replacement for
history.replaceState
and
history.pushState
,
with appropriate throttling applied to avoid browser errors.
If you call history.replaceState
too often, you may get one of the following errors:
- Safari: "SecurityError: Attempt to use history.replaceState() more than 100 times per 30 seconds"
- 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"
- Firefox: "Too many calls to Location or History APIs within a short timeframe."
You could catch and ignore these errors, but once browsers hit the rate limit,
they disable all calls to replaceState
for a while.
- Tiny: 0.4 KB min-gzipped with no dependencies
- Smart: prioritizes
pushState
overreplaceState
when rate limited - Browser-aware: applies different throttling to Safari (310 ms) than other browsers (52 ms)
- Compatible: works in any modern browser, and can be imported from Node
npm install --save history-throttled
Replace all your calls to history.pushState
and history.replaceState
and all
assignments to location.hash
as follows:
import { pushState, replaceState } from "history-throttled";
pushState("", "", "/foo"); // instead of history.pushState("", "", "/foo")
replaceState("", "", "/bar"); // instead of history.replaceState("", "", "/bar")
replaceState("", "", "#baz"); // instead of location.hash = "baz"
Even if you only care about replaceState
throttling, you should still replace
all calls to history.pushState
with the throttled version:
- Browsers put both functions on the same timer, so
history.pushState
can fail if you callreplaceState
a lot. - The throttled
pushState
version prevents delayedreplacedState
calls from being executed out-of-order afterpushState
, which would result in a wrong URL state.
When you call replaceState
or pushState
more often than every 310
milliseconds (or 52 milliseconds on non-Safari browsers), calls will be
automatically throttled.
pushState
calls will get priority over replaceState
calls. Say you're making
the following calls in quick succession:
pushState("", "", "/a");
pushState("", "", "/b");
pushState("", "", "/c");
replaceState("", "", "/c/1");
replaceState("", "", "/c/2");
This will result in the following behavior:
Immediately:
// We're not yet rate limited when the first pushState call occurs, so
// it will be executed synchronously.
history.pushState("", "", "/a");
After 310 milliseconds:
// We run the most recent pushState call, because it takes priority over
// the subsequent replaceState calls. Note that the intermediate pushState
// call to "/b" is dropped, because it exceeds the allowed rate.
history.pushState("", "", "/c");
After 620 milliseconds:
// Finally, the most recent of any remaining replaceState calls is flushed.
history.replaceState("", "", "/c/2");
The package can safely be imported from Node, for example for server-side
rendering, as long you don't call pushState
or replaceState
.
To disable all throttling and synchronously pass all calls through to the
history
object, run the following before any calls to pushState
or
replaceState
:
import { setDelay } from "history-throttled";
setDelay(0);
Copyright 2023 Jo Liss, licensed under the Apache License, Version 2.0.
Written for use in the calcu.net calculator.