15

In Chrome, if the user is scrolling all XHRs and setTimeouts will be delayed until scrolling stops and I need a workaround for this. The behavior is described in this blog post. Although this feature helps mobile scrolling, it is disastrous for infinite scroll, which is what I'm trying to do.

Evidence That This Is Happening:

  • All other browsers work fine, Chrome shows an empty screen until the user stops scrolling.

  • The network panel will show all requests as pending until scrolling is over, then they all finish at once.

  • Put this in a snippet, run it then start scrolling immediately. The setTimeout will not be called until after the scroll finishes.

var p = new Promise(function (resolve) {
    setTimeout(function () {
        console.log('resolving');
        resolve();
    }, 1000)
});

p.then(function () {
    console.log('DONE!!');
})
sakabako
  • 1,150
  • 7
  • 14
  • Does using `requestAnimationFrame()` rather than `setTimeout()` happen to help? The blog post makes it pretty clear that Chrome is prioritizing animation and GPU-related tasks so the user experience (what _they_ are doing) does not suffer. – arthurakay Jan 26 '16 at 21:49
  • Yes, it does! However, that doesn't help with XHRs, which is what matters for infinite scroll. – sakabako Jan 26 '16 at 23:09
  • I'm having this exact issue, would love to get an answer. Started a bounty for you. – Sebastian Olsen Nov 01 '16 at 12:27
  • 1
    @SebastianOlsen I cannot seem to reproduce this issue, even with XHR. Can you make an MCVE? – Alexander O'Mara Nov 04 '16 at 18:21
  • @sakabako, did you resolve this, and if so, how? I'm dealing with this exact issue for infinite scrolling, and it's driving me mad. All other browsers work fine except for Chrome. – Lush Jul 24 '17 at 03:13

4 Answers4

6

Depending on your exact scenario and desired browser support, I'd try Service Workers - which are meant for tasks such as the one you're handling (and can intercept all traffic), or a Web Worker to do your AJAX in the background.

Community
  • 1
  • 1
Nick Ribal
  • 1,959
  • 19
  • 26
1

Since this has to do with the engine, I don't think a workaround is possible. Instead I have filed a bug report with the Chrome team.

https://bugs.chromium.org/p/chromium/issues/detail?id=661155

Todd Chaffee
  • 6,754
  • 32
  • 41
1

I believe this may be related to this Chrome feature:

https://www.chromestatus.com/feature/5745543795965952 (Passive event listeners)

Chrome tries to avoid doing work on mobile when the user is scrolling, so listening to touchmove events can sometimes delay handlers being run and timeouts from firing.

Demo:
https://rbyers.github.io/scroll-latency.html
(check Handler Jank and scroll around the page)

NDavis
  • 1,127
  • 2
  • 14
  • 23
  • I checked dev tools which showed some unwanted touch listeners coming from a 3rd party lib. Disabling this lib solved my scroll issues. – Josh Ribakoff Feb 11 '18 at 06:59
  • I added an empty event listener to the "mousewheel" event and that fixed it for me (similar to the suggestion here https://stackoverflow.com/a/47684257/6111857) – NDavis Feb 12 '18 at 15:36
0

I was having the same issue with an infinite scroll and a masonry of images and finally was able to "force" chrome to stop debouncing the request for more images by simulating clicks on the document. I know it sounds weird but when I was testing, as long as I was scrolling, the request would infinitely debounce but as soon as I clicked once, it would instantly execute the XHR.

Here's how I did it...:

export function getPosts() {
  return (dispatch: Dispatch<any>, getState) => {
    const { postFeedState }: IGlobalState = getState();

    dispatch(handlePostsCancellation())

    dispatch({ type: postFeedTypes.GET_POSTS_REQUEST });

    const feedFilterDto = getFeedFilterDto(getState);

    // this is where the clicks are simulated  
    setTimeout(() => {
      for (let i = 0; i < 3; i++) {
        const evt = document.createEvent("Events");
        evt.initEvent("click", true, true);
        window.dispatchEvent(evt);
      }
    }, 50);

    return dispatch(setCancelSource(postFeedTypes.SET_MAIN_FEED_POSTS_CANCEL_SOURCE))
      .then((source) => PostController.getMainFeedPosts(postFeedState.get("postsToSkip"), FeedDisplay.THREE_COLUMNS, feedFilterDto, source))
      .then(posts => dispatch(getPostsSuccess(posts)))
      .catch(error => {
        if (axios.isCancel(error)) return;

        dispatch({ type: postFeedTypes.GET_POSTS_FAILURE });

        dispatch(handleApiError(error, null));
      });
  }
}

I first tried with only one click and it worked almost all the time and then tried 2-3 clicks and worked all the time. To be honest I still feel a little weird about that timeout/click code but what ever works, works! Also, time will tell if this will keep on working but for now this is best solution and easiest solution I've found yet.

P.S: I use redux and axios and have my own custom classes for my endpoints (PostController) but all it does is an axios(XHR) request.