React Native WebGPU

Canvas

Rendering pixels on-screen

Canvas follows the semantics of the Web <canvas> element as closely as we can: call getContext("webgpu") to get the GPUCanvasContext, read its size synchronously, manage pixel density the same way, and resize through the same clientWidth / clientHeight model, so browser WebGPU code largely carries over. Where a platform forces a difference we re-surface it rather than hide it. The primary example is frame presentation: on the Web the browser swaps the rendered frame automatically, but on React Native you call present() yourself after submitting your commands. Compositing a transparent canvas over native views on Android (see Transparent compositing) is another.

Props

PropTypeDescription
opaquebooleanDefaults to true. false alpha-composites the canvas over the views behind it (Android and web; iOS uses alphaMode alone)
androidAndroidCanvasPropsAndroid-only rendering options: surfaceType and zOrderOnTop. Ignored on iOS and web
...ViewPropsViewPropsAll standard React Native View props (style, onLayout, etc.)

Usage

Minimal on-screen rendering loop:

import { ,  } from "react";
import { , StyleSheet,  } from "react-native";
import { , , type CanvasRef } from "react-native-webgpu";

const  = `
@vertex fn main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
  var pos = array<vec2f, 3>(vec2(0, 0.5), vec2(-0.5, -0.5), vec2(0.5, -0.5));
  return vec4f(pos[i], 0.0, 1.0);
}`;

const  = `@fragment fn main() -> @location(0) vec4f {
  return vec4(1.0, 0.0, 0.0, 1.0);
}`;

export function () {
  const  = <CanvasRef>(null);
  const {  } = ();

  (() => {
    if (!) return;

    let  = false;
    let  = 0;

    const  = () => {
      const  = .?.("webgpu");
      if (! || ) {
         = ();
        return;
      }

      const  = . as HTMLCanvasElement;
      if (. === 0 || . === 0) {
         = ();
        return;
      }

      const  = ..();
      . = . * .();
      . = . * .();
      .({ , , : "premultiplied" });

      const  = .({
        : "auto",
        : {
          : .({ :  }),
          : "main",
        },
        : {
          : .({ :  }),
          : "main",
          : [{  }],
        },
        : { : "triangle-list" },
      });

      const  = () => {
        if () return;

        const  = .();
        const  = .({
          : [{
            : .().(),
            : [0, 0, 0, 0],
            : "clear",
            : "store",
          }],
        });
        .();
        .(3);
        .();

        ..([.()]);
        .();
         = ();
      };

       = ();
    };

     = ();

    return () => {
       = true;
      ();
    };
  }, []);

  return (
    < ={{ : 1 }}>
      < ={{ : 1, : "#3498db" }} />
      < ={} ={StyleSheet.} ={false} />
    </>
  );
}

Steps in order:

  1. Mount <Canvas ref={ref} /> and wait for layout (non-zero clientWidth / clientHeight).
  2. Request a device with useDevice or GPUDeviceProvider.
  3. Call ref.current.getContext("webgpu") and context.configure({ device, format, alphaMode }).
  4. Each frame: encode passes → device.queue.submit()context.present().

See Canvas for the frame loop and Learn WebGPU for a step-by-step walkthrough.

Presenting frames

On the Web, the browser swaps your rendered frame to the screen automatically when you submit GPU commands. In React Native there is no automatic swap: presentation is a manual step. After you submit your commands to the queue, call present() on the context to display the frame.

..([.()]);
.(); // React Native only

present() is a React Native only method (it is not part of the Web WebGPU spec). It runs synchronously on the calling thread, so the frame is presented from whichever thread did the rendering. This works the same on every runtime: the main JS runtime, the UI thread, and dedicated worklet runtimes (createWorkletRuntime / runOnRuntime, or a Vision Camera frame processor).

Always call present() after submit(), never before. See Frame presentations for the full frame loop and Native APIs for threading details.

Pixel density

Just like on the Web, a canvas has two independent sizes:

  • clientWidth / clientHeight are the on-screen (layout) size in logical points. They follow the view's layout and update automatically when it resizes.
  • width / height are the drawing-buffer size in physical pixels. This is the resolution WebGPU actually renders at.

The drawing buffer is not scaled to the device pixel ratio by default, exactly as on the Web. To render crisply on high-density screens, set the buffer size to the layout size multiplied by the pixel ratio. PixelRatio.get() is the React Native equivalent of window.devicePixelRatio.

const  = . as HTMLCanvasElement;
. = . * .();
. = . * .();

Because clientWidth / clientHeight track layout, recompute the buffer size whenever they change (for example on rotation or a resize). See Pixel density for a resize-aware render loop.

Transparent compositing

To render the canvas over the React Native views beneath it, you control transparency from two places, and you normally set both:

  • opaque={false} on <Canvas> makes the underlying native surface translucent so the views behind it show through.
  • alphaMode: "premultiplied" on context.configure, together with a clear color whose alpha is 0, makes WebGPU produce a transparent frame.
<Canvas ref={ref} style={StyleSheet.absoluteFill} opaque={false} />

The Usage example above wires up both: opaque={false}, alphaMode: "premultiplied", and a clearValue of [0, 0, 0, 0].

Platform note: on Android the alphaMode passed to configure() is ignored, so the Android backing view controls surface transparency and opaque is what selects it. On iOS alphaMode alone controls it and opaque is ignored. Setting opaque={false} together with alphaMode: "premultiplied" and an alpha-0 clear color gives you the same result on both platforms.

Android rendering options

Android has no single view type that is both cheap and composited like a regular view, so Canvas can be backed by one of three native views. The android prop selects it; it is ignored on iOS and web.

OptionTypeEffect
surfaceType"SurfaceView" | "TextureView" | "HardwareBufferView"Backing view. Defaults to SurfaceView when opaque and to TextureView otherwise. HardwareBufferView is opt-in and needs Android 10 (API 29)
zOrderOnTopbooleanSurfaceView only: setZOrderOnTop, composites above every React Native view in the window. Defaults to false

SurfaceView is the default for an opaque canvas and the fastest path. The frame goes straight to the system compositor as its own layer, with no extra copy and no involvement of the React Native view hierarchy. The price is that it is not really view content: it punches a hole through the window, so parent transforms, clipping, rounded corners, and z-ordering with sibling views do not apply to it. Use it whenever the canvas is a plain opaque rectangle.

TextureView is the default for a non-opaque canvas. It is a regular Android view: frames go through a SurfaceTexture that the UI toolkit samples as an external texture when it draws the view, so parent transforms, clipping, alpha, and z-order all apply and the canvas can be composited over other React Native views. That routing costs an extra texture copy and typically a frame of latency, and it can stall during some view animations. It works on every supported Android version.

HardwareBufferView is an opt-in, experimental alternative to TextureView for the non-opaque case, available on Android 10 (API 29) and above. It is also a regular view, so it composites exactly like TextureView, but WebGPU renders into an AHardwareBuffer that the UI toolkit draws inline, with no SurfaceTexture and no extra copy. It is designed to have lower latency than TextureView while keeping the same compositing behavior; that gain has not been measured yet, which is one reason it is not the default. Requesting it on a device below API 29 silently uses TextureView.

SurfaceViewTextureViewHardwareBufferView
Minimum Androidanyany10 (API 29)
Composited like a regular viewnoyesyes
Extra copynoneonenone
Expected latencylowesthighestbetween the two (unmeasured)
Selected whenopaque (default)opaque={false} (default)surfaceType: "HardwareBufferView"

opaque applies to whichever view is selected: on a SurfaceView it picks PixelFormat.OPAQUE or PixelFormat.TRANSLUCENT, on a TextureView it calls setOpaque, and a HardwareBufferView always composites with alpha. Changing opaque at runtime updates the view in place; changing surfaceType or zOrderOnTop replaces the backing view.

The defaults composite correctly in React Native stacking order without further flags. Set surfaceType when you need a different trade-off:

// Opaque TextureView: stays in stacking order, e.g. inside a ScrollView
<Canvas android={{ surfaceType: "TextureView" }} />

// Translucent SurfaceView above every React Native view, no composition pass
<Canvas opaque={false} android={{ surfaceType: "SurfaceView", zOrderOnTop: true }} />

// Translucent HardwareBufferView, opt-in (uses TextureView before Android 10)
<Canvas opaque={false} android={{ surfaceType: "HardwareBufferView" }} />

Two things to know before opting into HardwareBufferView. If the device cannot allocate or import the AHardwareBuffer, the canvas currently keeps rendering to an offscreen texture and shows nothing, rather than falling back to TextureView; test on your target devices. And because the UI toolkit exposes no "finished reading" signal for an inline buffer, it recycles its buffers with a small holding ring rather than a fence, so a device or driver quirk could show up as a stale or torn frame. If either happens, drop the surfaceType to get TextureView back. The plan is to make HardwareBufferView the non-opaque default once it falls back to TextureView at runtime and its latency advantage is measured.

A non-opaque SurfaceView without zOrderOnTop sits below the app window, so its alpha blends against the window background (usually black) and the React Native views beneath it are punched out. Pair it with zOrderOnTop to composite over React Native content, or use one of the two view-based backends. zOrderOnTop draws above all React Native views in the window, including navigation screens, regardless of zIndex.

See also