The 'discarded' page lifecycle state
Tags: javascript, html
Published: 2020-11-10 21:23:43

## Background

Browsers don't like the reputation of being resource hogs, even if you have 100+ tabs open. To help reduce CPU + memory demands while still allowing you to keep those tabs around, browsers came up with the _Page Lifecycle_ - a way to reduce resource usage while still providing a good user experience. 

[This article](https://developers.google.com/web/updates/2018/07/page-lifecycle-api) does a great job of further explaining the different states and what happens in them, as well as APIs you can use to check which state your page was or is in. 

## Discarded State

Let's focus on the `discarded` state. The article above defines that state as

>A page is in the discarded state when it is unloaded by the browser in order to conserve resources. No tasks, event callbacks, or JavaScript of any kind can run in this state, as discards typically occur under resource constraints, where starting new processes is impossible.
>
>In the discarded state the tab itself (including the tab title and favicon ) is usually visible to the user even though the page is gone.

Or in other words - the page hasn't been used for awhile, and the browser wants to free up resources, so it's going to "close the page" while still showing the tab for the page. That's pretty cool and useful.

However, what isn't detailed in that article (at the time of writing) is: _what exactly happens when the page is loaded up again?_

The answer (again, for the moment) [appears to be](https://twitter.com/philwalton/status/1326231502825271296?s=20): it's up to each browser to determine that. (To be clear, there's nothing wrong with that!)

## What actually happens

From my testing, let me tell you what appears to happens in Chrome - and how it caused a bug in my application:

1. The page is loaded
1. After some time, Chrome determines that it needs to go into the `discarded` state
1. Chrome appears to cache the original HTML of the page that you're on
1. The page goes into `discarded` state and all resources (CPU, memory) are removed for that tab
1. Now you open that tab/page again, and Chrome needs it to be in the `active` state
1. It pulls the cached HTML and re-parses + re-executes it - (which means that it refetches any CSS or Javascript referenced by the HTML)
1. The page is now loaded

The problem I ran into lies in that second-to-last step: it is using a cached version of the HTML but downloading a new version of the JS. 

## Is that a problem?

But why would _that_ cause a problem?

Well, in any case where you _need_ a change to happen in both your HTML _and_ your JS in order to work, and there's some place where you don't have versioned assets.

In my case, it's because of how the infrastructure is set up: 
* We're using [import-maps with SystemJS](https://github.com/systemjs/systemjs)
* I have one import-map that is not versioned and is not cached; the JS assets it links to are though
* I have another import-map that is inlined into the HTML page; this import-map is for things like shared dependencies that don't change very often (e.g. `react`, `react-dom`, etc.)

So in this situation, I had put a new shared library into the inlined import-map, and also updated all the JS assets to look for the shared version of that library instead of the bundled one. However, _for those people that had a cached version of the HTML_ (for example, the people that had their page go into the `discarded` state), they never downloaded the new HTML but **did** download the new JS - and that broke the page!

## A solution

Fortunately, the solution for users is simple enough - just a page refresh will cause them to download the latest HTML and all is good again.

For me, it also means that I can handle the situation where the app is being started from the `discarded` state by checking `document.wasDiscarded`, and if that's true, then force a full-page refresh which should resolve the bug before it happens. 

```js
if(document.wasDiscarded) {
  window.location.reload(true);
}
```