requestAnimationFrame
to allow developers to register a callback that they wish to be invoked before the next browser repaint. This is frequently used as an efficient way to update an on-screen animation. However, if you have embedded hidden iframes you cannot rely on this method being called. We will investigate the behaviour in this blog post.
Introduction
A responsible web developer who wants to apply custom animation to a web page will likely come across
requestAnimationFrame
. This allows the developer to register a callback and request that
the browser executes this callback before the next repaint. You can call it in this manner:
window.requestAnimationFrame(function(timestamp){
// animate something
});
The timestamp
value passed to the callback is an instance of
DOMHighResTimeStamp
,
which is effectively the time in milliseconds since the start of the current document's lifetime.
An animation is typically something that we want to update rapidly, but not constantly. We just want to
update at a rate that appears smooth to the user, without burning more CPU than is necessary.
For humans a rate of 60 frames-per-second is considered to be the limit that most of us can detect,
so if you are updating your animation with something approaching this frequency then it should appear smooth.
So why don't we just use setTimeout
or setInterval
to do all of our animations at this rate?
The benefit of requestAnimationFrame
is that the developer is explicitly revealing their intentions
to the browser; namely that this callback is related to animation. With setTimeout
or
setInterval
the browser has no indication of what we intend to execute in our callback. By expressing
our intent we allow the browser to optimize the animation (e.g. combining multiple animations) before a single repaint.
This also allows the browser to make decisions about whether the animation callback should even be run. For example, if
your tab has been moved to the background then running the animation loop is probably a waste of processor time,
so the browser can choose not to run the animation, saving processor cycles and the environment :)
So what's the catch? There isn't one, unless you have an animation that you need to run, but the browser decides that it doesn't want to run it. Read on to find out more.
The problem: Hidden embedded iframes
At GoConqr we offer a set of tools for creating learning content . One of the most
popular tools is MindMaps. When a user chooses to print their mindmap we make
an AJAX request for a specially prepared version of the mindmap from the server, we load this into a hidden iframe
on the page and then execute the print from that iframe. Before printing, the hidden iframe needs to execute JS to build the
mindmap on the canvas
, and some of this rendering makes use of requestAnimationFrame
.
This little dance happens unbeknownst to the user, who will just see the result of the print action. Things had worked this way for
years without any serious issue. However, more recently we have done some work on integrating the GoConqr tools with Microsoft Teams.
With our mindmaps now loaded within a frame in MS Teams the print function suddenly stopped working.
After some investigation we zeroed-in on the culprit: requestAnimationFrame
and it's behaviour when used inside an embedded
iframe
.
Example: Using requestAnimationFrame
within an iframe
Let's start by considering the frame that we want to embed, it is a simple HTML page with a script that uses
requestAnimationFrame
to execute some dummy animation:
<head></head>
<body>
<p>This is my frame</p>
<ol id="log_messages"></ol>
<script>
let start_time = null;
const $message_container = document.getElementById("log_messages"),
log_message = function(msg){
const $msg = document.createElement("li"),
$text = document.createTextNode(msg);
$msg.appendChild($text);
$message_container.appendChild($msg);
},
animate_continuously = function(timestamp){
if(!start_time){
start_time = timestamp;
} else if(timestamp-start_time>500) {
log_message("Finishing animation");
alert("Animation done");
return;
} else {
log_message("Animating: " + timestamp);
};
requestAnimationFrame(animate_continuously);
};
document.addEventListener("DOMContentLoaded", function(e){
log_message("DOMContentLoaded");
log_message("Starting animation");
animate_continuously();
})
</script>
<body>
</html>
The script in the frame registers a handler for the DOMContentLoaded
event. Within that handler we
call the animate_continuously
function, which registers itself as a callback using requestAnimationFrame
.
So each time our animate_continuously
callback is executed by the browser, we will enqueue the next invocation using
requestAnimationFrame
. The animation function doesn't really do anything apart from logging message to the page. We
run the animation for 500ms and issue a browser alert once the animation is complete.
In our subsequent examples we will embed this page as an iframe. To support the examples I have hosted this page frame.html
at two separate locations:
Loading the frame from these different locations we will contrast the impact of using requestAnimationFrame
within iframes
when they are loaded from the same domain or a different domain. For each example we will link you out to a separate simple page on this domain to
cleanly demonstrate the effect.
The original problem was observed in Chrome, so for the examples below we will focus on the behaviour in Chrome. However, from our testing, this behaviour can also depend on browser vendor. So if you are seeing something different, jump to this section to see if your observations are consistent with ours.
Visible iframe
served from the current domain
The first case we will look at is if we just embed this frame directly into a page, like this:
<div class="frame-wrapper" style="border: 2px solid #ccc;height: 300px; width: 80%;">
<iframe width="100%" scrolling="auto" src="https://vector-logic.com/blog-support/on-request-animation-frame/frame.html" style="display: block">
</iframe>
</div>
You can visit this simple page on this domain by clicking the link below:
If you hit the button, you should have seen the nested iframe logging output from the animation loop and issuing an alert when complete. Cool.
Hidden iframe
served from the current domain
Now if we look back at the MDN Docs
on requestAnimationFrame
we see the following:
requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s in order to improve performance and battery life.
This looks interesting. This might be a potential reason for the print function not working on
GoConqr. However, this functionality has relied on a hidden iframe from day zero,
so it doesn't fully explain what we are seeing. Let's investigate further by taking our previous iframe
, but this time we
will embed it on the page with style="display: none"
, like so:
<div class="frame-wrapper" style="border: 2px solid #ccc;height: 300px; width: 80%;">
<iframe width="100%" scrolling="auto" src="https://vector-logic.com/blog-support/on-request-animation-frame/frame.html" style="display: none">
</iframe>
</div>
If you click the link below you can view this simple page. Will it trigger the animation?
If you visit this page you will hopefully have noticed that the logs were not displayed. This is because the iframe
containing the logs is hidden. You can use the console to inspect the DOM and you should see that the logs have, indeed, been generated.
Nonetheless, the alert will have been issued to indicate that the animation successfully completed .
This behaviour in Chrome a bit surprising; the MDN documentation had given us the impression that the browser may not run
requestAnimationFrame
callbacks when they are associated with hidden iframes. However, in our
simple test Chrome continues to execute the requestAnimationFrame
callbacks for this hidden frame.
(Note: A different
behaviour is observed for Firefox).
So if requestAnimationFrame
is firing for hidden iframes, how can we explain our problems with mindmap printing?
Perhaps the domain from which the iframe
is loaded is significant?
Visible iframe
served from a different domain
In the next test, instead of loading our iframe
from the same domain as the parent page, we will try to load it
from a different domain. As explained earlier, we can achieve that by hosting the same frame.hml
file on our S3
bucket (domain vector-logic-blog.s3.eu-west-1.amazonaws.com
). The frame will then be included on the page as
follows:
<div class="frame-wrapper" style="border: 2px solid #ccc;height: 300px; width: 80%;">
<iframe width="100%" scrolling="auto" src="https://vector-logic-blog.s3.eu-west-1.amazonaws.com/on-request-animation-frame/frame.html" style="display: block">
</iframe>
</div>
To visit this page click the link below. Note the page is still hosted on this domain, but the iframe
is loaded from S3:
Again we see the animation writing logs to the frame and an alert is issued at the end of the animation to indicated that it successfully
completed. So we can run the animation when the iframe
is loaded from another domain, but what if that
iframe
is hidden?
Hidden iframe
served from a different domain
In a simple extension to the last example let's hide the embedded frame, i.e.
<div class="frame-wrapper" style="border: 2px solid #ccc;height: 300px; width: 80%;">
<iframe width="100%" scrolling="auto" src="https://vector-logic-blog.s3.eu-west-1.amazonaws.com/on-request-animation-frame/frame.html" style="display: none">
</iframe>
</div>
Again, click the link below to view this page:
Hopefully if you clicked this last button you would notice that no alert was fired. The animation has not run in this case.
This was the root of our problem with mindmap printing. Using requestAnimationFrame
within a hidden
iframe
never caused us a problem, but when this was embedded within another domain (i.e. within MS Teams) then
the requestAnimationFrame
callbacks failed to run. Can we solve the problem in this case?
Variations across browser
Thus far we have been mainly concerned with the behaviour exhibited by Chrome. However, as alluded to before, this behaviour shows some variation across browser vendors. From our limited testing this is what we have observed:
Visible same domain | Hidden same domain | Visible other domain | Hidden other domain | |
---|---|---|---|---|
Chrome | ✓ | ✓ | ✓ | ✗ |
Firefox | ✓ | ✗ | ✓ | ✓ |
Edge | ✓ | ✓ | ✓ | ✗ |
Safari | ✓ | ✓ | ✓ | ✓ |
The solution: Back to setTimeout
One solution is to avoid reliance on requestAnimationFrame
in such cases. We can rewrite the frame.html
slightly to revert back to an implementation using setTimeout
instead:
<p>This is my frame</p>
<ol id="log_messages"></ol>
<script>
let start_time = null;
const $message_container = document.getElementById("log_messages"),
log_message = function(msg){
const $msg = document.createElement("li"),
$text = document.createTextNode(msg);
$msg.appendChild($text);
$message_container.appendChild($msg);
},
requestFrame = function(callback){
setTimeout(function(){
callback(performance.now());
}, 1000/60);
},
animate_continuously = function(timestamp){
if(!start_time){
start_time = timestamp;
} else if(timestamp-start_time>500) {
log_message("Finishing animation");
alert("Animation done");
return;
} else {
log_message("Animating: " + timestamp);
};
requestFrame(animate_continuously);
};
document.addEventListener("DOMContentLoaded", function(e){
log_message("DOMContentLoaded");
log_message("Starting animation");
animate_continuously();
})
</script>
In this case we define the requestFrame
method ourselves, which uses the setTimeout
method to enqueue
the animation at our desired frame-rate of 60 times per second. Note we also use the performance.now()
to generate a
timestamp to pass into the callback. See the MDN documentation for DOMHighResTimeStamp:
You can get the current timestamp value—the time that has elapsed since the context was created—by calling theperformance
methodnow()
We define our final page to load this updated frame_fixed.html
from our S3 bucket into a hidden iframe
:
<div class="frame-wrapper" style="border: 2px solid #ccc;height: 300px; width: 80%;">
<iframe width="100%" scrolling="auto" src="https://vector-logic-blog.s3.eu-west-1.amazonaws.com/on-request-animation-frame/frame_fixed.html" style="display: none">
</iframe>
</div>
Click the link to see the result:
Hoepfully when you visited this page you will not see any logs, owing to the frame being hidden, but you should get a browser alert
informing you that the animation is complete. Success.
Conclusion
If you are using requestAnimationFrame
you may find that your callbacks will fail to run from within a hidden iframe
.
This behaviour also seems to be fundamentally influenced by whether the iframe
is loaded from the same domain as the containing document,
and the behaviour varies across browsers. If you have code that must run in a hidden iframe
you might need to consider switch from using
requestAnimationFrame
to the old reliable setTimeout
.
Comments
There are no existing comments
Got your own view or feedback? Share it with us below …