Skip to content

Commit

Permalink
Fixed rAF throttling issue caused by new Chrome flag
Browse files Browse the repository at this point in the history
Chrome's new "Throttle non-visible cross-origin iframes" flag causes problems for code running inside of hidden iframes. In this case, animation frames get scheduled without any error but never get called. This causes problems for the React DevTools browser extension specifically.

This commit adds a workaround by scheduling a backup timeout along with animation frames. In the normal case, these timeout will be cancelled by the animation frame (which will run first).

For more info, see facebook/react#21986
  • Loading branch information
Brian Vaughn committed Aug 13, 2021
1 parent 25ccd4f commit 68bdcdf
Showing 1 changed file with 116 additions and 77 deletions.
193 changes: 116 additions & 77 deletions src/vendor/detectElementResize.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,84 @@
* 4) Add nonce for style element.
**/

export default function createDetectElementResize(nonce) {
// Check `document` and `window` in case of server-side rendering
var _window;
if (typeof window !== 'undefined') {
_window = window;
} else if (typeof self !== 'undefined') {
_window = self;
} else {
_window = global;
}
// Check `document` and `window` in case of server-side rendering
let windowObject;
if (typeof window !== 'undefined') {
windowObject = window;

// eslint-disable-next-line no-restricted-globals
} else if (typeof self !== 'undefined') {
// eslint-disable-next-line no-restricted-globals
windowObject = self;
} else {
windowObject = global;
}

let cancelFrame = null;
let requestFrame = null;

var attachEvent = typeof document !== 'undefined' && document.attachEvent;
const TIMEOUT_DURATION = 20;

const clearTimeoutFn = windowObject.clearTimeout;
const setTimeoutFn = windowObject.setTimeout;

const cancelAnimationFrameFn =
windowObject.cancelAnimationFrame ||
windowObject.mozCancelAnimationFrame ||
windowObject.webkitCancelAnimationFrame;

const requestAnimationFrameFn =
windowObject.requestAnimationFrame ||
windowObject.mozRequestAnimationFrame ||
windowObject.webkitRequestAnimationFrame;

if (cancelAnimationFrameFn == null || requestAnimationFrameFn == null) {
// For environments that don't support animation frame,
// fallback to a setTimeout based approach.
cancelFrame = clearTimeoutFn;
requestFrame = function requestAnimationFrameViaSetTimeout(callback) {
return setTimeoutFn(callback, TIMEOUT_DURATION);
};
} else {
// Counter intuitively, environments that support animation frames can be trickier.
// Chrome's "Throttle non-visible cross-origin iframes" flag can prevent rAFs from being called.
// In this case, we should fallback to a setTimeout() implementation.
cancelFrame = function cancelFrame([animationFrameID, timeoutID]) {
cancelAnimationFrameFn(animationFrameID);
clearTimeoutFn(timeoutID);
};
requestFrame = function requestAnimationFrameWithSetTimeoutFallback(
callback
) {
const animationFrameID = requestAnimationFrameFn(
function animationFrameCallback() {
clearTimeoutFn(timeoutID);
callback();
}
);

const timeoutID = setTimeoutFn(function timeoutCallback() {
cancelAnimationFrameFn(animationFrameID);
callback();
}, TIMEOUT_DURATION);

return [animationFrameID, timeoutID];
};
}

export default function createDetectElementResize(nonce) {
let animationKeyframes;
let animationName;
let animationStartEvent;
let animationStyle;
let checkTriggers;
let resetTriggers;
let scrollListener;

const attachEvent = typeof document !== 'undefined' && document.attachEvent;
if (!attachEvent) {
var requestFrame = (function() {
var raf =
_window.requestAnimationFrame ||
_window.mozRequestAnimationFrame ||
_window.webkitRequestAnimationFrame ||
function(fn) {
return _window.setTimeout(fn, 20);
};
return function(fn) {
return raf(fn);
};
})();

var cancelFrame = (function() {
var cancel =
_window.cancelAnimationFrame ||
_window.mozCancelAnimationFrame ||
_window.webkitCancelAnimationFrame ||
_window.clearTimeout;
return function(id) {
return cancel(id);
};
})();

var resetTriggers = function(element) {
var triggers = element.__resizeTriggers__,
resetTriggers = function(element) {
const triggers = element.__resizeTriggers__,
expand = triggers.firstElementChild,
contract = triggers.lastElementChild,
expandChild = expand.firstElementChild;
Expand All @@ -61,14 +99,14 @@ export default function createDetectElementResize(nonce) {
expand.scrollTop = expand.scrollHeight;
};

var checkTriggers = function(element) {
checkTriggers = function(element) {
return (
element.offsetWidth != element.__resizeLast__.width ||
element.offsetHeight != element.__resizeLast__.height
element.offsetWidth !== element.__resizeLast__.width ||
element.offsetHeight !== element.__resizeLast__.height
);
};

var scrollListener = function(e) {
scrollListener = function(e) {
// Don't measure (which forces) reflow for scrolls that happen inside of children!
if (
e.target.className &&
Expand All @@ -79,65 +117,66 @@ export default function createDetectElementResize(nonce) {
return;
}

var element = this;
const element = this;
resetTriggers(this);
if (this.__resizeRAF__) {
cancelFrame(this.__resizeRAF__);
}
this.__resizeRAF__ = requestFrame(function() {
this.__resizeRAF__ = requestFrame(function animationFrame() {
if (checkTriggers(element)) {
element.__resizeLast__.width = element.offsetWidth;
element.__resizeLast__.height = element.offsetHeight;
element.__resizeListeners__.forEach(function(fn) {
element.__resizeListeners__.forEach(function forEachResizeListener(
fn
) {
fn.call(element, e);
});
}
});
};

/* Detect CSS Animations support to detect element display/re-attach */
var animation = false,
keyframeprefix = '',
animationstartevent = 'animationstart',
domPrefixes = 'Webkit Moz O ms'.split(' '),
startEvents = 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split(
' ',
),
pfx = '';
let animation = false;
let keyframeprefix = '';
animationStartEvent = 'animationstart';
const domPrefixes = 'Webkit Moz O ms'.split(' ');
let startEvents = 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split(
' '
);
let pfx = '';
{
var elm = document.createElement('fakeelement');
const elm = document.createElement('fakeelement');
if (elm.style.animationName !== undefined) {
animation = true;
}

if (animation === false) {
for (var i = 0; i < domPrefixes.length; i++) {
for (let i = 0; i < domPrefixes.length; i++) {
if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
pfx = domPrefixes[i];
keyframeprefix = '-' + pfx.toLowerCase() + '-';
animationstartevent = startEvents[i];
animationStartEvent = startEvents[i];
animation = true;
break;
}
}
}
}

var animationName = 'resizeanim';
var animationKeyframes =
animationName = 'resizeanim';
animationKeyframes =
'@' +
keyframeprefix +
'keyframes ' +
animationName +
' { from { opacity: 0; } to { opacity: 0; } } ';
var animationStyle =
keyframeprefix + 'animation: 1ms ' + animationName + '; ';
animationStyle = keyframeprefix + 'animation: 1ms ' + animationName + '; ';
}

var createStyles = function(doc) {
const createStyles = function(doc) {
if (!doc.getElementById('detectElementResize')) {
//opacity:0 works around a chrome bug https://code.google.com/p/chromium/issues/detail?id=286360
var css =
const css =
(animationKeyframes ? animationKeyframes : '') +
'.resize-triggers { ' +
(animationStyle ? animationStyle : '') +
Expand All @@ -163,25 +202,25 @@ export default function createDetectElementResize(nonce) {
}
};

var addResizeListener = function(element, fn) {
const addResizeListener = function(element, fn) {
if (attachEvent) {
element.attachEvent('onresize', fn);
} else {
if (!element.__resizeTriggers__) {
var doc = element.ownerDocument;
var elementStyle = _window.getComputedStyle(element);
if (elementStyle && elementStyle.position == 'static') {
const doc = element.ownerDocument;
const elementStyle = windowObject.getComputedStyle(element);
if (elementStyle && elementStyle.position === 'static') {
element.style.position = 'relative';
}
createStyles(doc);
element.__resizeLast__ = {};
element.__resizeListeners__ = [];
(element.__resizeTriggers__ = doc.createElement('div')).className =
'resize-triggers';
var expandTrigger = doc.createElement('div');
const expandTrigger = doc.createElement('div');
expandTrigger.className = 'expand-trigger';
expandTrigger.appendChild(doc.createElement('div'));
var contractTrigger = doc.createElement('div');
const contractTrigger = doc.createElement('div');
contractTrigger.className = 'contract-trigger';
element.__resizeTriggers__.appendChild(expandTrigger);
element.__resizeTriggers__.appendChild(contractTrigger);
Expand All @@ -190,44 +229,44 @@ export default function createDetectElementResize(nonce) {
element.addEventListener('scroll', scrollListener, true);

/* Listen for a css animation to detect element display/re-attach */
if (animationstartevent) {
if (animationStartEvent) {
element.__resizeTriggers__.__animationListener__ = function animationListener(
e,
e
) {
if (e.animationName == animationName) {
if (e.animationName === animationName) {
resetTriggers(element);
}
};
element.__resizeTriggers__.addEventListener(
animationstartevent,
element.__resizeTriggers__.__animationListener__,
animationStartEvent,
element.__resizeTriggers__.__animationListener__
);
}
}
element.__resizeListeners__.push(fn);
}
};

var removeResizeListener = function(element, fn) {
const removeResizeListener = function(element, fn) {
if (attachEvent) {
element.detachEvent('onresize', fn);
} else {
element.__resizeListeners__.splice(
element.__resizeListeners__.indexOf(fn),
1,
1
);
if (!element.__resizeListeners__.length) {
element.removeEventListener('scroll', scrollListener, true);
if (element.__resizeTriggers__.__animationListener__) {
element.__resizeTriggers__.removeEventListener(
animationstartevent,
element.__resizeTriggers__.__animationListener__,
animationStartEvent,
element.__resizeTriggers__.__animationListener__
);
element.__resizeTriggers__.__animationListener__ = null;
}
try {
element.__resizeTriggers__ = !element.removeChild(
element.__resizeTriggers__,
element.__resizeTriggers__
);
} catch (e) {
// Preact compat; see developit/preact-compat/issues/228
Expand Down

0 comments on commit 68bdcdf

Please sign in to comment.