Resizing on-screen WebGL canvas in iOS Safari causes memory leak

This is specific to iOS Safari or a WebKit-powered application on iOS.

If you have an HTML canvas on-screen that renders using a WebGL context, memory will leak when you resize that canvas’ width and height properties. This behavior does not happen on any desktop browser.

I made a barebones example using jsfiddle which reproduces the issue on iOS Safari. However, I am unable to include that link in this post.
That example automatically resizes a WebGL canvas every 1 second using two different sets of dimensions.
Every frame it renders a quad – all I do is call a handful of GL routines to get the quad on the screen (I do not create any new GL objects explicitly after the initial setup).

When this sample runs on Safari iOS on an iPad Air 3 running iOS 14.2, Safari page memory usage steadily increases every resize.
After letting it run for a while, the browser process gets terminated after it reaches 1.25GB memory. It started at less than 300MB.

If I run this same example in a desktop browser, the memory usage is steady the entire time.

The issue appears to be limited to canvases that use WebGL contexts. Canvases using 2D rendering contexts appear unaffected.

Anyone else seeing this?

Accepted Reply

This appears fixed for me when upgrading to iOS 14.3. You can consult bug #219780 I filed on WebKit's bug tracker for more discussion.

https://bugs.webkit.org/show_bug.cgi?id=219780

Replies

Here is the jsfiddle code.

HTML:

Code Block
<canvas>Your browser does not support HTML5 canvas.</canvas>


CSS:
Code Block
* { margin:0; padding:0; }
html, body { width: 100%; height: 100%; }
canvas { display: block; }

JavaScript:
Code Block
var _canvas;
var _gl;
var _buffer;
var _shaderProgram;
var _coordinatesVar;
var _widths = [768, 1024];
var _heights = [1024, 768];
var _dimIndex = 0;
var _resizeStartTime = undefined;
function _setupScene() {
_canvas = document.getElementsByTagName("canvas")[0];
_gl = _canvas.getContext("webgl") || _canvas.getContext("experimental-webgl");
if (!_gl)
throw new Error("Cannot create WebGL context!");
var vertices = [
-0.5, 0.5,
-0.5, -0.5,
0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
0.5, 0.5,
];
_buffer = _gl.createBuffer();
_gl.bindBuffer(_gl.ARRAY_BUFFER, _buffer);
_gl.bufferData(_gl.ARRAY_BUFFER, new Float32Array(vertices), _gl.STATIC_DRAW);
var vertCode =
'attribute vec2 coordinates;' +
'void main(void) {' +
' gl_Position = vec4(coordinates, 0.0, 1.0);' +
'}';
var vertShader = _gl.createShader(_gl.VERTEX_SHADER);
_gl.shaderSource(vertShader, vertCode);
_gl.compileShader(vertShader);
if (!_gl.getShaderParameter(vertShader, _gl.COMPILE_STATUS))
throw new Error(_gl.getShaderInfoLog(vertShader));
var fragCode =
'void main(void) {' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);' +
'}';
var fragShader = _gl.createShader(_gl.FRAGMENT_SHADER);
_gl.shaderSource(fragShader, fragCode);
_gl.compileShader(fragShader);
if (!_gl.getShaderParameter(fragShader, _gl.COMPILE_STATUS))
throw new Error(_gl.getShaderInfoLog(fragShader));
_shaderProgram = _gl.createProgram();
_gl.attachShader(_shaderProgram, vertShader);
_gl.attachShader(_shaderProgram, fragShader);
_gl.linkProgram(_shaderProgram);
if (!_gl.getProgramParameter(_shaderProgram, _gl.LINK_STATUS))
throw new Error(_gl.getProgramInfoLog(_shaderProgram));
_gl.useProgram(_shaderProgram);
_coordinatesVar = _gl.getAttribLocation(_shaderProgram, "coordinates");
_gl.enableVertexAttribArray(_coordinatesVar);
_gl.bindBuffer(_gl.ARRAY_BUFFER, _buffer);
_gl.vertexAttribPointer(_coordinatesVar, 2, _gl.FLOAT, false, 0, 0);
/* initial auto-resize */
_canvas.width = _widths[_dimIndex];
_canvas.height = _heights[_dimIndex];
_dimIndex = _dimIndex > 0 ? 0 : 1;
}
function _drawScene() {
_gl.drawArrays(_gl.TRIANGLES, 0, 6);
}
function _render(timestamp) {
/* The following makes the canvas resizing based on browser window (fullscreen):
_canvas.width = window.innerWidth;
_canvas.height = window.innerHeight;
*/
/* The following automatically changes the canvas dimensions back and forth between two dimensions every second: */
if (undefined === _resizeStartTime)
_resizeStartTime = timestamp;
const elapsed = timestamp - _resizeStartTime;
if (elapsed > 1000) {
_canvas.width = _widths[_dimIndex];
_canvas.height = _heights[_dimIndex];
_dimIndex = _dimIndex > 0 ? 0 : 1;
_resizeStartTime = undefined;
}
_gl.viewport(0, 0, _canvas.width, _canvas.height);
_gl.clearColor(0.8, 0.8, 0.8, 1.0);
_gl.clear(_gl.COLOR_BUFFER_BIT);
_drawScene();
window.requestAnimationFrame(_render);
}
window.addEventListener("load", function() {
_setupScene();
window.requestAnimationFrame(_render);
}, false);

This memory leak might very well be the reason for thousands of sites crashing and randomly reloading as reported here: https://developer.apple.com/forums/thread/662251
This appears fixed for me when upgrading to iOS 14.3. You can consult bug #219780 I filed on WebKit's bug tracker for more discussion.

https://bugs.webkit.org/show_bug.cgi?id=219780