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 "
QuotaExceededError
"DOMException
, abort the script without an exception, prompt the user, or throttle script execution.
App-Level Work-Around
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
QuotaExceededError
? - A warning should be added to
Browser.Navigation.pushUrl
and.replaceUrl
warning 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 elm/browser
.