I have encountered a runtime error in my Elm app and am looking for thoughts on the root of the problem. I’ll describe what I tried to do, how it failed, and how I worked around it.
Desired App Feature
In my photo album app, I wanted the user to be able to scroll down a large page of thumbnails, click on a picture to see it full size, then click back and have the scroll position restored.
I achieved this by listening for
onScroll events, putting the current scroll position in a query param in the URL via
Browser.Navigation.replaceUrl, and then on the “back” navigation I parse that query parameter and scroll to that position on the thumbnail page.
Exception Observed in Production
This worked great on Chrome & Firefox, so I released it into production. However, I soon began getting reports from users of Safari on iPad that the app would just stop responding to all taps sometimes.
When I finally got ahold of a mac I could debug on, I opened up the Safari console and found this:
[Error] SecurityError: Attempt to use history.replaceState() more than 100 times per 30.000000 seconds replaceState (elbum.js:4061) (anonymous function) (elbum.js:4061) _Scheduler_step (elbum.js:959) _Scheduler_enqueue (elbum.js:933) _Scheduler_rawSend (elbum.js:879) _Platform_dispatchEffects (elbum.js:2057) sendToApp (elbum.js:1886) callback (elbum.js:2956)
Low-Level Browser Cause
Safari rate-limits calls to
replaceState, and since I was updating my query param in an
onScroll handler, it could end up getting called quite rapidly as the user was scrolling the thumbnail page.
As far as I could tell, this basically killed the Elm event loop, and no more mouse clicks etc. were processed in the app.
There is a fair bit of discussion in webkit bug 156115 about whether this is in fact a standards-compliant thing for Safari to do. The participants in that discussion ended up agreeing that it is. In particular quoting this bit of the spec:
User agents may impose resource limitations on scripts, for example CPU quotas, memory limits, total execution time limits, or bandwidth limitations. When a script exceeds a limit, the user agent may either throw a "
DOMException, abort the script without an exception, prompt the user, or throttle script execution.
The discussion in the webkit bug points out, among other things, that it probably would’ve been a better approach for my app to intercept the thumbnails -> fullsize click event, grab the scroll position once, and call
replaceState once. I see their point … but it’s also the case that I found it made my code simpler to continuously track the scroll position.
So, my work-around was to use jinjor/elm-debounce to limit the rate at which I propagated the
onScroll updates out to my model & thus the URL. This has fixed the problem reliably as far as I can tell.
Whose Problem Is This, Really?
- Safari’s violating the spec?
- Elm should recover more gracefully from a
- A warning should be added to
.replaceUrlwarning app developers not to call them too frequently?
- Something else I’m not thinking of?
If it helps, I can probably put together a SSCCE that will demonstrate the problem, and/or file an issue against