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

Polyfill requestIdleCallback when native is not available #8833

Merged
merged 1 commit into from
Jan 21, 2017

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jan 20, 2017

I introduce the ReactDOMFrameScheduling module which just exposes rIC and rAF.

The polyfill works by scheduling a requestAnimationFrame, store the frame start time for the start of the frame, then schedule a postMessage which gets scheduled after paint. The deadline is set to frame start time + frame rate. By separating the idle call into a separate event tick we ensure that layout, paint and other browser work is counted against the available time.

| frame start time                                      deadline |
[requestAnimationFrame] [layout] [paint] [composite] [postMessage]

The frame rate is dynamically adjusted by tracking the minimum time between two rAF callbacks. This is not perfect because browsers can schedule multiple callbacks to catch up after a long frame. To compensate for this we only adjust if two consecutive periods are faster than normal.

This seems to guarantee that we will hit frame deadlines and we don't end up dropping frames. However, there is still some lost time so we risk starving by not having enough idle time.

Especially Firefox seems to have issues keeping up on the triangle demo. However, that is also true for native rIC in Firefox. It seems like more render work is scheduled on the main thread pipeline and also that JS execution just generally has more overhead. Safari has little trouble keeping up with this polyfill. I have not tested IE yet.

I introduce the ReactDOMFrameScheduling module which just exposes
rIC and rAF.

The polyfill works by scheduling a requestAnimationFrame, store the time
for the start of the frame, then schedule a postMessage which gets
scheduled after paint. The deadline is set to time + frame rate.
By separating the idle call into a separate event tick we ensure that
layout, paint and other browser work is counted against the available time.

The frame rate is dynamically adjusted by tracking the minimum time between
two rAF callbacks. This is not perfect because browsers can schedule
multiple callbacks to catch up after a long frame. To compensate for this
we only adjust if two consecutive periods are faster than normal.

This seems to guarantee that we will hit frame deadlines and we don't end
up dropping frames. However, there is still some lost time so we risk
starving by not having enough idle time.

Especially Firefox seems to have issues keeping up on the triangle demo
However, that is also true for native rIC in Firefox. It seems like more
render work is scheduled on the main thread pipeline and also that JS
execution just generally has more overhead.
@sebmarkbage
Copy link
Collaborator Author

I originally used setTimeout(idle, 0) but since it adjusts to at least 4ms and sometimes more, you end up dropping a bit more than I'd like from the available frame time. However, in practice postMessage can be delayed up to 4ms anyway because of the internal browser scheduling around the frame.

@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Jan 20, 2017

Perhaps we should be more aggressive about detecting native rIC? There are some bad polyfills out there which would work very poorly with React if they're used on the page.

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clever! Left a thought or two but overall I think this looks great.

return frameDeadline - performance.now();
} : function() {
// As a fallback we use Date.now.
return frameDeadline - Date.now();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud: performance.now() and Date.now() return very different values but they're being used interchangeably, driven by the param passed to requestAnimationFrame. Wouldn't this cause problems if we ever end up mixing the 2 due to polyfills? (requestAnimationFrame is supposed to be passed a time from performance.now() but some polyfills are Date based.)

Fortunately I don't think this is likely to occur. It seems both are supported by everything we target except for:

  • IE9, which supports neither; presumably this means we'd use Date.now for both.
  • iOS 8.4 which supports requestAnimationFrame but not performance.now. This version is 2.5 years old though and iOS has pretty strong upgrade adoption, so maybe we can just ignore this one point release.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If iOS 8.4 supports requestAnimationFrame but not performance.now my hypothesis is that the value passed to requestAnimationFrame is the old Date.now version of requestAnimationFrame.

Yes, it is very possible that there is a browser that implements the new requestAnimationFrame without implementing performance.now. Seems unlikely though.

However, even more likely is that there are browsers out there that implement the old requestAnimationFrame while already implementing performance.now. That's very possible.

We could probably do a guess based on the value passed to requestAnimationFrame which one it is (if it is a very low number) and then switch dynamically between the two. For the case where the new rAF is implemented but no performance.now we could use a Date.now time stamp instead. I'd rather not complicate it unless we know of at least one browser where either of this happens though. Perhaps we can call on the community to help out identifying issues here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Actually I wonder if performance.now might be implemented behind performance.webkitNow in some cases which would trigger the bad case.

}
};
// Assumes that we have addEventListener in this environment. Might need
// something better for old IE.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IE9+ supports addEventListener; as we no longer support IE8 this should be fine. 👍

@sebmarkbage sebmarkbage merged commit 5659bd2 into facebook:master Jan 21, 2017
@sebmarkbage
Copy link
Collaborator Author

sebmarkbage commented Jan 21, 2017

Oh, btw, it's possible that other input events gets scheduled after our postMessage in which case they might extend past the deadline. This may vary based on OS scheduling. I haven't seen this be a big issue yet. That might be because my input handlers are fast which is generally recommended anyway, or that the events end up scheduled before the postMessage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants