Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there any way to have the back button load just a cached page? #7936

Open
1 task done
martinmckenna opened this issue Nov 8, 2023 · 19 comments
Open
1 task done
Labels
bug:unverified feat:routing feat:scroll Issues related to scroll restoration

Comments

@martinmckenna
Copy link

martinmckenna commented Nov 8, 2023

What version of Remix are you using?

2.0.1

Are all your remix dependencies & dev-dependencies using the same version?

  • Yes

Steps to Reproduce

I was observing my own site and a few sites in the Who's using remix in production discussion and it seems like out-of-the-box, Remix has this issue with the back button on mobile browsers where the back button doesn't really perform like you'd expect it to. It seems like by default, the page's loader is always re-run and you end up with this really janky experience:

  1. Navigate to new page
  2. Swipe to go back (or hit back button)
  3. See flash of the latest page you were on, then the page you expect to see appears

Here's like 4 different examples I found pretty easily just by browsing through that discussion:

video of https://datagunung.com

https://imgur.com/aPe9TiN

video of https://hub.findkit.com

https://imgur.com/50NHxCH

video of https://basementcommunity.com

https://imgur.com/xruVJnD

video of https://ajourney.io

https://imgur.com/5vmVsaO

Now, from what I can tell, HTTP Cache control headers don't really solve this problem. I tried implementing them on both the document and each loader individually, and it seems like this issue still happens. I'm not sure if this is just a side-effect of using client-side hydration after the first render, or perhaps a side-effect of having markup that is generated at the time the page is requested, but I feel like getting a cached page when hitting the back button should be behavior default to Remix without the developer having to figure this out.

Expected Behavior

Page flashes old content before showing right content when hitting back button on mobile browsers

Actual Behavior

Page loads cached page with no flashing on mobile browsers

@martinmckenna
Copy link
Author

Also from what I can tell, this has been an issue with Remix from the start. I don't really have experience with Next.js, so I'm not sure if this is the norm with all universal-rendered frameworks that hydrate the client after first load, but I have a hunch that it is the norm.

Can this problem even be solved?

@brophdawg11
Copy link
Contributor

This looks like it might have something to do with <ScrollRestoration>. Looks like the new react docs site had this same issue a while back and had to "hack" in a fix for safari only :/

https://twitter.com/dan_abramov/status/1568219286744797190
reactjs/react.dev#5026
reactjs/react.dev#5029

Anyone want to take a stab at a fix for this and see if the same thing works for <ScrollRestoration> in Remix? The code itself is actually in react-router-dom so you could play around with direct edits to that in node_modules in your Remix app as a way to test it out locally.

https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/index.tsx#L1627

@brophdawg11 brophdawg11 added the feat:scroll Issues related to scroll restoration label Nov 8, 2023
@martinmckenna
Copy link
Author

martinmckenna commented Nov 8, 2023

huh interesting. i'll try to give this a shot on my site. FWIW though, a lot of those examples I hadn't scrolled yet

e: i can also replicate on iOS chrome as well

@mikkpokk
Copy link

mikkpokk commented Nov 9, 2023

From my experience, it's happening only when client side logic receives new information from loader and forces history DOM to re-render and re-calculate. This is not the case, when your route's view is persistent or it's height stays the same as it is on history. When your first render (SSR) page looks the same as it is after first CSR will be applied, you will not see the jumping effect.

This flashing and jumpy effect can be avoided. It just requires a bit to think through the archidecture and rendering logic. Popular thing is to render null or a loading spinner in component during the load time - you should avoid that wherever possible. Use better loading indicators if necessary (ex. overlay loader, loader bar on top of the page). Easiest way to understand what's going on your site, is to disable javascript temporary and navigate without javascript. Main reason, why this side-effect is so popular on production sites, is because developers are used to write CSR React.

Challenging topic is to avoid jumping effect on infinity pagination. There are loads of material about it on Google however and it isn't Remix issue, it's actually classical infinity pagination issue. That's why Facebook never destroy it's timeline from DOM when you navigate to another page.

About the ScrollRestoration of Remix - it's actually A+ grade component from my experience. About 1.5 year ago I used next.js on my projects and their ScrollRestoration was very chaotic and unpredictable back then. Not sure, if they have improved it by now.

Only case when revalidation of loaders during navigation on browser history stack causes the problem, is the case when entities sort order has changed (ie. new entity has been added or some entity has been resorted). It doesn't cause jumpy effect, it just replaces history elements with new sort order. I don't think that's expected UX behaviour. Users, who using browser navigation, usually expect to continue browsing where they left off.

So maybe this side-effect worths a discussion to add an option to opt-out from revalidation during the browser history navigation?

@hanayashiki
Copy link

Also from what I can tell, this has been an issue with Remix from the start. I don't really have experience with Next.js, so I'm not sure if this is the norm with all universal-rendered frameworks that hydrate the client after first load, but I have a hunch that it is the norm.

Can this problem even be solved?

On iOS safari, if you visit next.js's documentation site, which is built upon nextjs itself, try to scroll back, you will have similar flashing issue. I think this might be a case in which safari does not behave well. No problems on chrome.

@hanayashiki
Copy link

hanayashiki commented Nov 13, 2023

There in fact two problems during iOS swipe back action:

  1. It shows a grey background instead of a snapshot of last page.
  2. It does not go to the previous page at the same tick that popstate is fired. So the old page still shows and only after the loading is done, the previous page shows.

For problem 1, I'm not sure with the reason why a grey background shows. It seems this is the behavior if safari sees history.scrollRestoration === 'auto'? It should be a Safari problem.

For problem 2, for the current Remix which does not cache the data of useLoader, there can be hardly a perfect solution. Imagine that popstate fires at the end of your swiping gesture, and you are supposed to see the previous page right after, but Remix makes you await the loader data, how can you see the loading previous page? You need to wait for the previous page's data to load, so the old page will still show.

@martinmckenna
Copy link
Author

martinmckenna commented Nov 15, 2023

just so it's on the record (and i haven't attempted the hack/fix yet), i can absolutely reproduce this on mobile chrome as well

just want this made aware before we write this off as a safari-only bug:

trim.BB7CDD53-421C-4F02-89A7-48BC769396CF.MOV

i feel like there's 2 different issues here

  1. the flashing of the previous page's content
  2. the flashing of the blank white page

@mikkpokk
Copy link

@martinmckenna I can reproduce issue on datagunung.com, however I am not able to reproduce issue on remix.run, shopify.com and on my projects.

I digged deeper and I can confirm datagunung have standard issue with infinity pagination which causes re-renders and blinking during back/forward navigation. You can read more about that on comment above.

Delay during back/forward navigation is caused by revalidation, which is different issue and discussion. It requires storing each state of the page to local_storage or session_storage to offer option to disable revalidation during browser history navigation. Currently, Remix do not save those states and that's why revalidation is required.

@r0stiars0
Copy link

Hi @martinmckenna, I'm the maintainer of datagunung.com.
Thanks for highlighting the issue.

Have you got a chance to test this back button behavior in Android?

Here is one example that I was able to capture:
https://imgur.com/a/mLyeZrT

or
https://github.com/remix-run/remix/assets/101281822/d15acc5d-97cb-449c-8115-0dd56e44e8a3

@martinmckenna
Copy link
Author

@r0stiars0 just iOS safari and chrome so far. it's not a problem on non-mobile, so i'm not surprised that Android doesn't have this issue.

@martinmckenna
Copy link
Author

martinmckenna commented Jun 16, 2024

@brophdawg11 so I FINALLY got around to trying out Dan's code and it seems to solve the blank page issue, but doesn't solve the issue of the "flashing the previous content"

https://imgur.com/9zL7bUf

I updated this code in my react-router-dom node_modules:

  // Trigger manual scroll restoration while we're active
  React.useEffect(() => {
    window.history.scrollRestoration = "manual";
    return () => {
      window.history.scrollRestoration = "auto";
    };
  }, []);

to this:

  React.useEffect(() => {
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
    if (isSafari) {
      // This is kind of a lie.
      // We still rely on the manual Next.js scrollRestoration logic.
      // However, we *also* don't want Safari grey screen during the back swipe gesture.
      // Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time.
      history.scrollRestoration = 'auto';
    } else {
      // For other browsers, let Next.js set scrollRestoration to 'manual'.
      // It seems to work better for Chrome and Firefox which don't animate the back swipe.
      window.history.scrollRestoration = "manual";
    }
  }, []);

I also tried without the cleanup function, but the issue still remains.

So as suspected, we have 2 bugs here, not 1.

@martinmckenna
Copy link
Author

martinmckenna commented Jun 16, 2024

i was doing some googling yesterday too and found some similar conversations about the page flashing happening in Svelekit, Nuxt, and Next:

sveltejs/kit#10700
vercel/next.js#57243
nuxt/nuxt#6101

@martinmckenna
Copy link
Author

@brophdawg11 wondering if you had any thoughts on the above ^^

should the hacky fix be PRed here even if it doesn't solve the flickering issue?

@mikkpokk
Copy link

mikkpokk commented Jul 31, 2024

To prevent white background instead of previous / forward page's preview during the half a drag on Safari, the scrollRestoration auto hack seem to prevent that. However, it brings in visually more annoying feature - visual scroll jump. That's due to nature of "manual" and "auto" scrollRestoration behaviour.

I haven't seen any iOS user to use prev/forward actions to just preview the previous page on half screen. User goes back to navigate the last page where they left off. Considering that case scenario, white preview is less annoying than slower and jumpy previous page due to triple browser calcuations due to scrollPosition. White preview is something that iOS Safari developers can resolve if needed since it's not React, react-router (Remix), next.js problem - it's CSR problem. CSR happens with any SSR javascript framework which uses re-hydration / VirtualDOM (React, Angluar, Vue, ..).

All the other behaviours are logical - javascript will update your data on mount. During prev/scroll your view mounts and triggers data revalidation and all the other application specific mounting actions.

What Gemini thinks about the issue?

Understanding the Issue: iOS Safari, Scroll Restoration, and Drag Preview

Problem:
When scrollRestoration is set to manual in iOS Safari, dragging back doesn't display a preview of the previous page but instead shows a white screen during the drag gesture.

Potential Causes:

  1. Scroll Restoration Behavior:
  • Default Behavior: When scrollRestoration is automatic, the browser typically saves and restores scroll positions, allowing for smooth transitions and previews during back navigation.
  • Manual Mode: Disabling scroll restoration might interfere with the browser's ability to cache and render previous page content for quick previews.
  1. iOS Safari Specifics:
  • Performance Optimization: To optimize performance and battery life, iOS Safari might prioritize certain rendering processes, potentially affecting drag previews.
  • Platform Limitations: There could be inherent limitations in iOS Safari's rendering engine or gesture handling that prevent accurate previews in this specific scenario.
  1. Web Page Complexity:
  • Heavy JavaScript or DOM: Complex web pages might consume more resources during rendering, impacting the preview generation process.
  • Third-party Scripts: Conflicting or resource-intensive scripts could interfere with the expected behavior.

Additional Considerations:

  • User Experience: While investigating the issue, consider alternative approaches to provide a smooth user experience, such as loading indicators or progress bars during back navigation.

It's worth to report the issue to Apple via Apple Feedback Assistant rather than working out temporary hack, which will prevent Apple to resolve the main issue and putting Safari to same path where Internet Explorer was.

@martinmckenna
Copy link
Author

martinmckenna commented Oct 11, 2024

gonna post the react-router issues regarding the same bugs here too just for visibility:

remix-run/react-router#10883

remix-run/react-router#10822

@hanayashiki
Copy link

hanayashiki commented Oct 11, 2024

Since this issue has been open for 1 year, may I assume Remix hasn't considered caching since day 0 thus is broken by design?

@martinmckenna
Copy link
Author

perhaps it would be a nice idea to just be able to turn off client-side routing altogether

@mikkpokk
Copy link

mikkpokk commented Nov 1, 2024

perhaps it would be a nice idea to just be able to turn off client-side routing altogether

You can use <a> HTML-tag instead of <Link> component to achieve that.

@martinmckenna
Copy link
Author

@mikkpokk lol good call, i'm dumb 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug:unverified feat:routing feat:scroll Issues related to scroll restoration
Projects
None yet
Development

No branches or pull requests

5 participants