Working within the constraints of a web browser is hard.
Did you know that around 20% of page visits (source: Google) on mobile are via the back and forward buttons? Today, hopefully, we can come to understand a bit more what happens when you hit the forward and back button in the browser. We will also think through how various constraints may affect your user’s experience depending on the type of technology you use.
We do have an ideal experience in mind when thinking about how a user should experience back or forward navigation - native applications. They do an extraordinary job of giving you content fast. However, their architectures lend themselves to a seamless experience. For example, Android maintains an “Activities” stack. When you navigate to another page, that stack item turns “off”. When navigating back, it is as simple as turning it back “on”. As a result, the state of that page is shown to the user in an extremely efficient manner.
Bfcache
Luckily, as web developers, we have various superpowers available to us as well. This is known as the bfcache (the colloquial name is said to be back forward cache). Here are some docs on Firefox’s implementation. If you haven’t heard of this, that is completely fine. Safari and Firefox have had this feature for much longer than Chrome. Only sometime in 2019 did Chrome come out with their own bfcache implementation.
Effectively, the browser will create a new frame for every navigation. Whether same-site or cross-origin, the browser will put the page on ice. This includes taking a snapshot of the page along with other metadata. Further, all work on that page, including any delayed tasks like setTimeout
will be stopped in its tracks, only to be resumed if you reach that page again through the bfcache. When you navigate back to an item in the stack with the back or forward button, the browser takes it off ice and renders it to the user without refreshing the content. This includes all input element states that may have been left in flux. In other words, stateful browsing.
Making Compromises
As stated, we are bound to have some tradeoffs. Imagine you log out of your current session. What happens when you return? Well, your last stack item is taken off ice and rendered to the user. This can be very problematic for security reasons. There are solutions to get around this. However, I won’t present them here for the sake of potential changing browser heuristics or solutions conjured up by the community.
In addition, libraries like Phoenix Live View, Hotwire , or Turbolinks avoid putting their pages into bfcache. This may cause subtle bugs for users filling out large forms.
Lastly, when thinking about static sites versus dynamic sites, a back action may be taken after the app has sat idle for days! It is likely preferable that navigation back to a page not only takes it off ice, but also requests data and ensures you have the most up to date information presented to your user. Alas, another tradeoff.
Single Page Apps (SPAs)
Sadly, this cache space is not available to a single page app built with Ember.js or other client-side frameworks. That is because navigation to a new document doesn’t really take place when a user interacts with the back button. We interact with APIs like history.pushState
and history.replaceState
on window.onpopstate
.
Benefits
We noted some of the problems that may occur above with server-side applications and the bfcache. However, with SPAs, we easily subvert these issues. If you have properly manicured your local state with an SPA, you likely gain dynamic content by default plus avoid the pitfalls of a logged-out user seeing previous session history. Moreover, a framework like Ember.js has the pieces to render your page on the back button ready to go with very little initialization cost.
Costs
The downside to using the back and forward button with SPAs is the cost to paint the DOM and retrieve resources (likely) from the browser cache. Even retrieving resources from the browser cache can be costly if your document is quite large. Moreover, maintaining your previous scroll position can be quite tricky. In the Ember.js community, ember-router-scroll has become critical in helping SPAs implement this correctly. However, it also is tricky with lazy loaded DOM elements and large, content-heavy documents. Certainly, a frozen state of your previous page would help all SPAs restore your last scrolled position. However, this would require different browser APIs to solve some of the issues presented above.
Summary
Bfcache is a default setting provided by many of the modern browsers today that will aggressively cache your pages for future backward and forward navigations. However, this does not apply to Single Page Apps. SPAs interestingly solve some problems but creates others. Achieving native application user experience on backward and forward navigation is quite difficult without a construct like bfcache or activities stack that we can just take off ice and render for the user. It may require you to pull some tricks out of the bag such as a properly configured server to instruct the browser to cache assets or UI tricks to fade in the navigation to hide the reconstructing DOM.
As with all things in software development, tradeoffs abound. However, we can close the gap and continue improving on the user experience by choosing the right tool for the project. SPAs might be right for you, but they might not be. And if you don’t know where to begin or are lacking the expertise needed to complete your next app endeavor, we can help. Reach out to us.
DockYard is a digital product consultancy specializing in user-centered web application design and development. Our collaborative team of product strategists help clients to better understand the people they serve. We use future-forward technology and design thinking to transform those insights into impactful, inclusive, and reliable web experiences. DockYard provides professional services in strategy, user experience, design, and full-stack engineering using Ember.js, React.js, Ruby, and Elixir. From ideation to delivery, we empower ambitious product teams to build for the future.