Re-implement the browser

· Allanderek's blog


Single page applications are a good way to write responsive websites. Non-single page applications have to do a lot of re-getting of the same information, so they are often quite slow to move around. A single page application on the other hand tends to get most of what it needs up front, and then when you move around it only has to get the new data it needs for the new route that you're on. This can make it seem very fast, in particular things like searching and sorting can be near instantaneous whilst on a non-single-page application they can take a lot more time. Even when you do need to go get more data, your app can feel more responsive because you only show a 'working' indicator in the part of the app waiting to display the new data. There are other advantages, such as lessening the load on your server since all the rendering is done on the client rather than the server.

There are however, some disadvantages, it's pretty difficult to get a great lighthouse score using Elm or some other similar framework for single page applications, because you necessarily have to download your app first and then load the page. But the one I want to talk about is a niggling feeling you get whilst developing a single page application. That niggling feeling is of re-implementing the browser.

This is most notable in places such as forms and other requests such as a data request upon moving between different routes. You end up having to show the user that something is happening whilst the request is in-flight. This is something that a non-single page application doesn't have to worry about because the browser already takes care of indicating that something is happening for you.

Then, you start moving around routes in your new SPA, and you do something like download a list of posts which you display to the user, let's say on a 'posts' page. The user then scrolls down this list and clicks on one, at which point you update your view. But the browser doesn't know what is happening so it doesn't update your scroll position. Argh, okay so you think "whenever I change route, I'll scroll the user back to the top". So you implement this and the user scrolls down the list of posts, clicks one and is happy that you scrolled them to the top to start reading the post. Then they finish the post and click back. What happens? You go back to the list of posts, but either you're scrolled back to the top, or you're left at whatever the scroll position was when reading the post. Neither of these are what you want, you want to go back to the scroll position you were in when last reading the list of posts. Okay, so now when someone moves off the posts you record what the current post position was and when they return to the posts page you scroll back to that. All of this happens automatically, done by the browser when you're moving around a non-single-page application. So it is in this sense that you feel like you're re-implementing the browser.

Now something else happens, you realise that you're starting to accumulate quite a lot of state. Scroll positions, tabs etc. This can add up as a fair amount of memory being used. Particularly if your pages are non-finite, that is you have routes likes /posts/view/<post-id> so that when one is viewing a post page you have a bit of UI to store on the model, so you have to store it in some kind of dictionary, perhaps something like Dict Post.Id Post.UiState. The problem is, when should I remove this state? If I do not do that explicitly it will not get removed and hence will remain indefinitely. Because you're writing a single page application, if it is a long-running app that you expect the user to generally have open (eg. An admin app for their e-commerce store), then it could be a while until the next refresh. All the while this stale UI state is being collected and never disposed of.

Sometimes when someone returns to a page, you actually want to reset the UI, this is great because that means when they leave the page you can get rid of their UI state. But you do have to remember to do this. Again, you're re-implenting stuff that is just automatically done by the browser for non-single page applications.

There are probably a fair few other features of the browser that you either forego or have to re-implement yourself if you develop a single page application but I'll end with just one more. Multiple tab browsing. Suppose you go to an e-commerce store such as Amazon, you have a cart, to which you add items. Suppose you have multiple items that you need to buy so you browse for each in separate tabs, or maybe you open up a bunch of tabs from the search screen. Anyway, the point is your cart is stored on the session, and each time you move between pages, because you're doing a full page re-load, if your cart was updated in another tab, you get your new cart. But with single page applications you're never doing a full page reload, so it still thinks your cart is whatever it was when the tab was loaded. So your tabs can get out of sync easily. Now, there are ways around this, you can store the cart in local storage and have all the tabs subscribe to updates of that local storage and take appropriate action. This is actually a better solution than the server-side approach, because your cart in each tab will be updated immediately. However, it still feels like you're re-implementing the browser a bit.

Finally, of course I haven't said anything about the reverse. I'm sure there are some features of single-page applications that are just automatic that you have to re-implement if you're implementing a non-single page page app. So I'm not saying any of this invalidates the idea of SPAs, it's just a niggling thought I constantly have.