John White John White - 1 month ago 10
Javascript Question

How to prevent IE11 and Microsoft Edge from throttling events aggressively?

Background



I have a pannable app window which works by listening for
mousemove
events and then using
transform: translate3d(...)
to move the screen accordingly. It's a large app and there is considerable UI work associated to facilitate this functionality. Here comes the MCVE, in which the real work is mocked by a dummy
for
loop:



var container = document.getElementById("container");
var contents = document.getElementById("contents");
var input = document.getElementById("iterations");

var posX = 50;
var posY = 50;
var previousX = null;
var previousY = null;

var mousedownHandler = function (e) {
window.onmousemove = globalMousemoveHandler;
window.onmouseup = globalMouseupHandler;
previousX = e.clientX;
previousY = e.clientY;
}

var globalMousemoveHandler = function (e) {
var now = Date.now();
for (var i = 0, n = parseInt(input.value); i < n; i++);
var elapsed = Date.now() - now;

posX += e.clientX - previousX;
posY += e.clientY - previousY;
previousX = e.clientX;
previousY = e.clientY;
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
contents.innerText = elapsed + "ms";
}

var globalMouseupHandler = function (e) {
window.onmousemove = null;
window.onmouseup = null;
previousX = null;
previousY = null;
}

container.onmousedown = mousedownHandler;
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";

#container {
height: 180px;
width: 600px;
background-color: #ccc;
overflow: hidden;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}

#container:active {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}

<label>Iterations: <input id="iterations" type="number" value="20000000" step="5000000" /></label>

<div id="container">
<div id="contents">
Pannable container contents...
</div>
</div>





JSFiddle example with larger drag-area

Please hold and drag the example.

This little snippet runs a dummy
for
loop every time
mousemove
fires, and the duration it takes for the loop to complete is displayed in the draggable container. This is needed to demonstrate the problem below. You may need to adjust the number of iterations, so that the loop takes somewhere above 10ms to run, but not much longer.


Problem



This snippet runs as fast as possible in Google Chrome, no problems there. Untested in Firefox.

However, in Microsoft Edge (and presumably, IE11 as well) if
globalMousemoveHandler
runs for longer than about 10ms, the browser starts throttling the event mercillesly, making it fire much less frequently, and obliterating the panning progress down to a crawl.

Also quite strange is that the for loop actually runs faster in Microsoft Edge than in Chrome, but the event still fires much slower.

This is observable in the above snippet when viewed from the mentioned browsers. Now I understand the theoretical desire behind this functionality, but it renders my application unusable on these browsers -- I also don't really understand what's the point for the throttling to kick in below 16ms (I'm well under the 60 FPS frame budget), but that's besides the question now (although I'd be glad to hear some details about this).

How can I prevent this from happening?

Answer

Here is an example that uses requestAnimationFrame.

var container = document.getElementById("container");
var contents = document.getElementById("contents");
var input = document.getElementById("iterations");

var posX = 50;
var posY = 50;
var previousX = null;
var previousY = null;

var mousedownHandler = function (e) {
    window.onmousemove = globalMousemoveHandler;
    window.onmouseup = globalMouseupHandler;
    previousX = e.clientX;
    previousY = e.clientY;
}

var globalMousemoveHandler = function (e) {
    posX += e.clientX - previousX;
    posY += e.clientY - previousY;
    previousX = e.clientX;
    previousY = e.clientY;
  
    window.requestAnimationFrame(function () {        
      var now = Date.now();
      for (var i = 0, n = parseInt(input.value); i < n; i++);
      var elapsed = Date.now() - now;

      contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
      contents.innerText = elapsed + "ms";
    });
}

var globalMouseupHandler = function (e) {
    window.onmousemove = null;
    window.onmouseup = null;
    previousX = null;
    previousY = null;
}

container.onmousedown = mousedownHandler;
contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
#container {
  height: 180px;
  width: 600px;
  background-color: #ccc;
  overflow: hidden;
  cursor: -webkit-grab;
  cursor: -moz-grab;
  cursor: grab;
  -moz-user-select: none;
  -ms-user-select: none;
  -webkit-user-select: none;
  user-select: none;
}

#container:active {
  cursor: move;
  cursor: -webkit-grabbing;
  cursor: -moz-grabbing;
  cursor: grabbing;
}
<label>Iterations: <input id="iterations" type="number" value="20000000" step="5000000" /></label>

<div id="container">
    <div id="contents">
        Pannable container contents...
    </div>
</div>

This stops the heavy lifting from blocking the handler function, effectively preventing the issue from happening, at the cost of a 16ms increased latency.

Comments