I ran into an interesting issue recently, which caused the loading of my application to deadlock on an iPad.
Browsers have a set maximum number of connections per host that they can use to load resources. For most desktop browsers it’s 6, less on mobile devices, and specifically 5 on iOS 6.0 Mobile Safari.
A smart way of making the most of these connections would be to have them all pull from a things-to-load queue, and this is what desktop browsers seem to do. So we always keep every connection busy while there are things in the queue. But Mobile Safari seems to be different – deciding up front using a round robin allocation which connection will load each resource (so if we trigger the load of 10 resources, connection 1 will get the job of loading 1 and 6). Not only does this not make the best use of available connections; it can also cause a loading deadlock.
My application uses a single SignalR with Server Sent Events (SSE) transport, and as such one connection from the budget is always in use and won’t be freed up. So any resources that Safari allocates to that connection for loading will never load. It’s nothing specific to SignalR/SSE, anything which holds up a connection will cause it.
If that resource that never loads is an image for example, then maybe it’s not such a big problem. But in my case I was using the RequireJS AMD script loader, which loads scripts using script tags with the async attribute set. After they all load, a callback is executed. As my SignalR connection was starting up early when my application loaded, it was blocking some of these scripts from loading, and hence the callback was never run. The callback in this case was a major module of the application. Result: deadlock.
In a test case I made which requested 10 scripts after the SignalR connection was initiated, 1-4 and 6-9 load (presumably on connections 2-5), but 5 and 10 will never load. They usually all load correctly if the connection is fast enough.
It only seems to happen if connection keep-alive is used for the persistent connection, but I haven’t investigated that in detail as we didn’t want to turn that off in any case.
Solutions (until the iOS bug gets fixed):
- delay starting persistent connections until your application is fully loaded
- monitor resource requests (e.g. by wrapping your AMD loader), and pause/stop/restart persistent connections when more than 4 requests are in progress
These would only need to be done if you detect an iOS Safari, to avoid degrading performance for users of other devices. The problem doesn’t happen on Chrome for iOS (it seems to have 6 available connections per host so if it did, the indices of failed resources would be different).
There’s probably a lesson here: not only do you need to test your web applications on the mobile devices that users will use to access them – you need to do that testing using the slower connections that many of them will be using.