Javascript canvas buffer / performance lento en iOs Safari y Chrome

Estoy tratando de actualizar un canvas de JavaScript dibujando píxeles directamente en el buffer de imageData. Básicamente estoy actualizando todos los píxeles en el buffer de imageData después de cada evento mousemove / touchmove, y tratando de get el mejor performance posible.

Antecedentes: estoy desarrollando una aplicación basada en emscripten, donde el dibujo en el canvas está completamente dibujado píxel por píxel por el código "nativo". El ejemplo que doy en esta pregunta es un ejemplo más simple donde reproduje mi problema.

Actualmente he encontrado dos problemas de performance:

  • en iOS safari (probado en un iPad air): la function de dibujo se llama a 31 fps, pero el canvas renderizado en la pantalla es lento (visualmente, diría que se actualiza a 10 fps máximo, más algunos intervalos de 0,5 segundos donde no se actualiza en absoluto)
  • en iOS Chrome: el performance es horrible, ya que tengo 2.9 fps

En una Mac de escritorio, obtengo un performance constante: 55 fps con Firefox y 45 fps con Chrome

Entonces, tengo dos preguntas

  • ¿Cómo forzaría el canvas a refrescarse más rápido en iOs safari (para tener un renderizado real de 30 fps, o puede ser un poco más bajo)?
  • ¿Cómo optimizarías el performance? ¿Perdí una posible optimization?

Consulte el siguiente código: es un único file html que reproduce mis problemas.

Sé que podría usar un webworker, pero dado que estoy usando emscripten, esto no sería óptimo (cada webworker comienza con un nuevo recuerdo, y necesito mantener el logging del estado).

Vea el código aquí (es un único file html, el js es independiente). Mueva el mouse dentro del canvas para ver los fps calculados.

<canvas width=800 height=600 id="canvas"> </canvas> <script> //Disable scroll : usefull for tablets where touch events //will scroll the page function DisableScroll() { window.addEventListener("touchmove", function(event) { if (!event.target.classList.contains('scrollable')) { // no more scrolling event.preventDefault(); } }, false); } window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); window.countFPS = (function () { var nbSamples = 20; //number of samples before giving a fps var counter = 0; var fps = 0; var timeStart = new Date().getTime(); return function() { counter++; if (counter == nbSamples) { var timeEnd = new Date().getTime(); var delaySeconds = (timeEnd - timeStart) / 1000; fps = 1 / delaySeconds * nbSamples; counter = 0; timeStart = timeEnd; } return fps.toFixed(2); } }()); function getMousePos(canvas, evt) { var rect = canvas.getBoundingClientRect(); return { x: evt.clientX - rect.left, y: evt.clientY - rect.top }; } function getTouchPos(canvas, evt) { var rect = canvas.getBoundingClientRect(); return { x: evt.targetTouches[0].clientX - rect.left, y: evt.targetTouches[0].clientY - rect.top }; } DisableScroll(); var canvas = document.getElementById('canvas'); var ctx = canvas.getContext("2d"); var canvasData = "empty"; function myDraw(pos) { canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height); var binaryData = canvasData.data; var idx = 0; for (y = 0; y < canvas.height; y++) { for (x = 0; x < canvas.width; x++) { //Red binaryData[idx ++] = x % 255; //Green : add a little animation on the green channel //var dist = Math.sqrt( (pos.x - x) * (pos.x - x) + (pos.y - y) * (pos.y - y)); var dist = Math.abs(pos.x - x) + Math.abs(pos.y - y); var g = 255 - dist; if ( g < 0 ) g = 0; binaryData[idx++] = g; //Blue binaryData[idx ++] = y % 255; //Alpha binaryData[idx ++] = 255; } } ctx.putImageData(canvasData, 0, 0); } var OnLoad = function() { myDraw({x:0, y:0}); } // // Mouse & touch callbacks // function CanvasMouseMove(pos) { myDraw(pos); var elem = document.getElementById("fps"); elem.value = window.countFPS(); } canvas.addEventListener("touchmove", function(e){ CanvasMouseMove( getTouchPos(canvas, e)); } , false); canvas.addEventListener("mousemove", function(e){ CanvasMouseMove( getMousePos(canvas, e) ); }); </script> <body onload=OnLoad()> <br/> FPS<input type=text id="fps" />&nbsp;&nbsp;&nbsp; </body> 

Rq:
– evite la filtración global y declare x, y como vars en myDraw.
Las sugerencias:
– caching canvas.width y canvas.height para evitar el acceso DOM,
– caching pos.x y pos.y
– trade (% 255) para (& 0xFF)
– caching Math.abs
– solo crea UN imageData que sigas modificando (alivia el gc).
– dibujar en requestAnimationFrame (de lo contrario, puede que tenga que esperar a que se dibuje un marco).
– almacenar en caching el rect delimitador del canvas (y sus valores superiores / izquierdos).

jsbin está aquí:

http://jsbin.com/saruzoqo/4/

puedes cambiar lo viejo / nuevo con 2 botones.

parece

 var staticCanvasData = ctx.getImageData(0, 0, canvas.width, canvas.height); function myDraw2(pos) { canvasData = staticCanvasData; var binaryData = canvasData.data; var cw = canvas.width, ch = canvas.height; var posX = pos.x, posY = pos.y; var idx = 0; var abs = Math.abs; for (var y = 0; y < ch; y++) { var yDiff = abs(posY - y) ; for (var x = 0; x < cw; x++) { //Red binaryData[idx++] = x & 0xFF; //Green : add a little animation on the green channel //var dist = Math.sqrt( (pos.x - x) * (pos.x - x) + (pos.y - y) * (pos.y - y)); var dist = abs(posX - x) + yDiff; var g = 255 - dist; // if (g < 0) g = 0; // useless array is clamped binaryData[idx++] = g; //Blue binaryData[idx++] = y & 0xFF; //Alpha binaryData[idx++] = 255; } } ctx.putImageData(canvasData, 0, 0); } 

Los resultados son bastante buenos, FF toma el time medio (10 vs 20 ms), Chrome 15 ms less (116 (!) A 100), y safari toma 7 en lugar de 20 !! (Mac OS)

No investigé mucho, pero parece ser el hecho aislado no crear / copyr un imageData en cada networkingibujo de counts para más del 60% de las ganancias.