8

Steps to reproduce the issue:

  1. The user visits the webpage, see the code below.
  2. The user closes Chrome.
  3. The device goes completely offline (turn all networking off manually).
  4. The user re-opens the browser while completely offline.
  5. Chrome automatically serves the last visited page, a saved copy of the webpage which says Online? true, even after hitting refresh several times.

The only thing that tells the user that she/he is looking at some stale, completely unusable copy of the web page is this in the address bar:

enter image description here

Non-technical users can easily miss that, and are left wondering why the page is unusable... This is bad user experience.

Browser & device: Chrome 81 on Android 6 on an Acer Iconia Tab 10 A3-40 tablet.

The webpage is served over HTTPS (secure connection).

Code:

const setMsg = (flag) => {
  const p = document.getElementById('msg')
  p.innerHTML = '<b>Online?</b>  ' + flag
}

setMsg(navigator.onLine)

window.addEventListener("online", () => {
  setMsg(true)
})
window.addEventListener("offline", () => {
  setMsg(false)
})
<p id='msg'> </p>

As far as I can tell:

  • Chrome does not re-run any JavaScript in Step 5, even after hitting refresh.
  • Chrome does not respect the Cache-Control: private, no-store either; double-checked.

So far, the only way I could prevent this from happening is to register a service worker. When I have a service worker registered, the JavaScript is re-run and I can properly and clearly inform the user that she/he is offline.

Without a service worker, how can I prevent Chrome from loading a stale, unusable webpage when offline?

The usual "No internet" page with the dinosaur is appropriate, and that's what I was expecting with Cache-Control: no-store.

28
  • I dont have time to research fully right now, but have you tried adding another listener for 'pageshow' that manually runs the check? That's how you rerun JS for a back button navigation, for instance, which seems like a similar problem. Commented Apr 20, 2020 at 15:24
  • @tmdesigned No, pageshow doesn't help either, double-checked. As far as I can tell, Chrome does not rerun any JavaScript, even if you hit reload. In other words, pageshow fails for the same reason why my code fails: It is not re-run by the browser. Commented Apr 20, 2020 at 15:49
  • On an existing site of mine, I have cache-control: no-cache, no-store, must-revalidate and it doesn't restore the page. Still, I would have thought no-store alone would do that. Commented Apr 20, 2020 at 17:07
  • @tmdesigned I have bad news for you: This version of Chrome does restore the page. I changed then verified that the page is served with cache-control: no-cache, no-store, must-revalidate as you suggested. Then, I cleared the browser cache completely, and repeated the experiment. Chrome happily restores the page with those cache control headers. Please test your own page as I describe it in the post, with the current version of Chrome. I am pretty sure your page will be restored too. If not: Are you serving your page over HTTPS (secure connection)? Commented Apr 20, 2020 at 20:48
  • 1
    Terrible, awful, no-good CSS based workaround. Have an 'online' element with an animation that hides it after 3 seconds. Use JS to perpetually reset it (remove/add?). When JS disappears, online goes away. Again, bad. But I don't know how to do anything else with JavaScript not even running. Commented Apr 23, 2020 at 18:05

1 Answer 1

7
+50

A very inelegant workaround is to use a CSS animation that makes an "offline" element appear all at once after a set amount of time. With JavaScript, you can continually reset the animation, preventing it from ever showing unless/until JavaScript is no longer running to reset it.

The animation in the example below is set to 3 seconds, and uses keyframes to stay at opacity:0 until 99% of the animation duration, at which point it changes to opacity:1.

In JavaScript we can reset the animation every 1-2 seconds with JavaScript by removing the class that is associated with it, forcing a reflow (by fetching the offsetWidth, for instance), and then adding the animation class back.

Ideally we could tell Chrome on these mobile devices to not cache the page at all. The various headers we tried in the comments on the question were not respected by a mobile device running with no Internet access.

Disabling the caching would be a more appropriate solution in both concept and execution. Having a running JavaScript interval that is perpetually forcing a reflow isn't great. But, it does give the user an indication that the JavaScript engine has stopped running, which is what the situation is here.

In the example below, the "Simulate stop JS" button just clears the interval that is continuing to reset the loading animation. This is a simulation only, but has the same effect as JavaScript not running (tested on an isolated server).

const overlay = document.getElementById('offline-overlay');

const stopJS =  document.getElementById('stopJS');

const heartbeat = () => {
  overlay.classList.remove("animate-overlay");
  void overlay.offsetWidth; 
  overlay.classList.add("animate-overlay");
}

const heartbeatInterval = setInterval( heartbeat, 1000);

stopJS.addEventListener("click", function(){
  clearInterval(heartbeatInterval);
});
@keyframes offline {
  0%   {opacity:0;}
  99%  {opacity:0;}
  100% {opacity:1;}
}

.animate-overlay{
  animation-name: offline;
  animation-duration: 3s;
}

#offline-overlay{
  background-color: rgba(255,255,255,.9);
  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
  display:flex;
  justify-content: center;
  align-items:center;
  pointer-events:none;
}

#offline-overlay span{
  font-size:300%;
  font-family: sans-serif;
  letter-spacing:5px;
  color: #888;
}
  <div id="offline-overlay" class="animate-overlay">
    <span>OFFLINE</span>
  </div>

<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam varius quam sed nulla feugiat varius. Praesent vitae mi et libero porttitor maximus. Suspendisse eu pulvinar quam. Phasellus id ante a elit faucibus cursus. Curabitur porttitor vehicula ornare. Suspendisse nec risus ex. Aenean bibendum auctor ex eget aliquet. Donec laoreet sem ut tortor viverra aliquam.</p>

<button id="stopJS">Simulate Stop JS</button>

Sign up to request clarification or add additional context in comments.

7 Comments

+1 and thanks! On the real webpage I already have another hack running in the background. That hack triggers a reflow every second, because Safari on iOS is buggy, and that hack was the only way to fix it. Adding your workaround to this already existing hack won't make my code uglier. However, there is one tweak I would like to ask. It seems to me that overlay totally covers the page, even if not visible. If that is the case, it is not acceptable: There are interactive elements on the page and the overlay would make those elements unusable.
Does the pointer-events:none not address this? I am able to still interact.
My apologizes, I wasn't aware of this: "[pointer-events] the value none instructs the pointer event to go "through" the element and target whatever is "underneath" that element instead" (from MDN). Sorry, I did not know that. OK, so we have a fix to this problem, and due to my other hack that is already running in the background (Safari bug workaround), it is not even making my code uglier. I am leaving the question open for a while so that your answer gets more upvotes.
As a side note: I am still surprised that none of the JS is re-run when the page is restored, but the CSS animations are running. It is definitely not what I expected to happen.
The CSS still working isn't too surprising to me because it's still allowing other CSS rules to apply. JavaScript not running in the first place is super suprising to me though. I'd love to see any other approaches people come up with.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.