sodcalculator

10 Tech How-To

Fix TensorFlow.js Browser Freezes on New Tensor Shapes

Guide 10-42135 · Updated · Step by step

TensorFlow.js browser freezes caused by new tensor shapes are typically the result of the WebGL backend recompiling shaders or reallocating GPU memory when a tensor with an unseen shape is created. The fix is to pre-allocate and reuse tensors with fixed shapes, or to switch to the CPU/WASM backend for predictable performance, and to avoid shape-changing operations inside tight loops.

Why New Tensor Shapes Cause Freezes

TensorFlow.js in the browser runs on one of three backends: WebGL (GPU), WebAssembly (WASM), or plain JavaScript (CPU). The WebGL backend is the default for most browsers because it is fast for large matrix operations. However, it is also the most sensitive to shape changes. When a tensor with a new shape is created, the backend must:

  • Compile a new GLSL shader program for the operations that will run on that shape.
  • Allocate a new texture on the GPU, which may involve waiting for the browser’s GPU process.
  • Upload the tensor data from CPU to GPU memory.

Shader compilation is the biggest culprit. The WebGL backend caches shaders by operation and shape, but the first time a shape is seen, compilation can take hundreds of milliseconds. If your code creates tensors with ever-changing shapes (e.g., variable-length sequences, dynamic batch sizes), the browser may freeze as it compiles new shaders on the main thread.

How TensorFlow.js Handles Shapes Internally

TensorFlow.js represents tensors as typed arrays (e.g., Float32Array) on the CPU, but when using the WebGL backend, it also creates a WebGLTexture on the GPU. The texture dimensions must be powers of two for some operations, so the backend pads shapes internally. For example, a tensor of shape [3, 5] might be stored as a texture of size [4, 8]. This padding is invisible to the user but affects memory usage and shader compilation.

Shaders are compiled per operation and per shape. The kernel (e.g., matMul, add) is compiled into a GLSL program that takes the shape as a uniform. When a new shape arrives, the backend checks its shader cache. If the shape is not in the cache, it calls gl.compileShader and gl.linkProgram, which are synchronous and block the main thread. This is the freeze you see.

Diagnosing the Freeze

Before applying a fix, confirm that shape changes are the cause. Open the browser’s DevTools Performance panel and record a profile while the freeze occurs. Look for a long task with a call stack that includes createProgram, compileShader, or linkProgram. If you see those, the issue is shader compilation.

Another clue is that the freeze happens only the first time a particular shape is used, and subsequent calls with the same shape are fast. If the freeze happens every time, it could be a memory allocation issue or a different bottleneck like data transfer.

Fix 1: Pre-Allocate Fixed-Shape Tensors

The most effective fix is to ensure your model only ever sees a fixed set of shapes. For example, if you are processing sequences, pad or truncate them to a constant length before feeding them into the model. This way, the WebGL backend compiles shaders once and reuses them.

  1. Determine the maximum shape your model will need (e.g., max sequence length, max batch size).
  2. Create a buffer tensor with that shape using tf.buffer or tf.zeros.
  3. Update the buffer’s values using buffer.set() and then call buffer.toTensor() each time you need a new tensor. This reuses the same underlying memory, so no new shader compilation is needed.

If you cannot use a buffer, at least pre-create tensors for each shape you know you will use. For instance, if you have a loop that processes batches of size 1, 2, and 4, create a tensor for each size once and reuse them by copying data into them with tensor.assign() (available in TensorFlow.js 3.x) or by using tf.tidy() to manage memory.

Fix 2: Switch to the CPU/WASM Backend

If your model uses many different shapes and pre-allocation is impractical, consider switching to the WASM or CPU backend. These backends do not compile shaders; they run plain JavaScript or WebAssembly, which is much more predictable for variable shapes. The trade-off is speed: WebGL is often 10-100x faster for large matrix multiplications, but for small models or infrequent inference, the difference may be negligible.

To switch, call tf.setBackend('wasm') (after importing the WASM backend) or tf.setBackend('cpu') before running your model. You can also use tf.ready() to wait for the backend to initialize. Test both to see which gives acceptable performance without freezes.

Fix 3: Avoid Shape-Changing Operations in Loops

Operations like tf.slice, tf.concat, tf.reshape, and tf.gather can produce tensors with new shapes. If you use them inside a loop that runs many times, each iteration may trigger shader compilation. Instead, try to vectorize the operation or precompute the result once.

For example, if you are extracting a window from a sequence at each step, consider using tf.signal.frame to create all windows at once, rather than looping and slicing. This reduces the number of distinct shapes and improves performance.

Common Misconception: tf.tidy() Fixes Freezes

Some developers believe that wrapping code in tf.tidy() will prevent freezes. While tf.tidy() is essential for cleaning up intermediate tensors and preventing memory leaks, it does not affect shader compilation. The freeze is caused by the creation of new shapes, not by memory accumulation. Use tf.tidy() for memory management, but combine it with the shape fixes above.

Frequently Asked Questions

Why does the freeze happen only on the first inference?

Because the first time a shape is used, the WebGL backend must compile shaders for every operation in your model. Subsequent calls with the same shape reuse the cached shaders, so they are fast. If you see a freeze on every call, your shapes are changing each time.

Can I precompile shaders for all possible shapes?

Not directly. TensorFlow.js does not expose a public API to precompile shaders. The workaround is to run a dummy inference with each shape you plan to use during initialization, so the shaders are compiled before the user interacts with the page. This moves the freeze to the loading phase, which is less noticeable.

Will using the WASM backend always be slower than WebGL?

Not necessarily. For small models (e.g., a few thousand parameters) or when the input size is small, WASM can be comparable or even faster because it avoids GPU upload overhead. For large models, WebGL is usually faster, but the freeze may be unacceptable. Benchmark both backends with your specific model.

How do I know which backend is being used?

Call tf.getBackend() to see the current backend. You can also check tf.backend() for more details. In the browser console, the TensorFlow.js library logs the backend when it initializes.

What if I cannot change the shapes because they come from user input?

You can still apply padding and masking. For example, if you are processing sentences of varying lengths, pad them to a fixed maximum length and use a mask to ignore the padding. This is a common technique in natural language processing and works well with TensorFlow.js.

Related

General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.

More in this bed

The whole Tech How-To bed →