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
androidAndroidCanvasPropsAndroid-only rendering options: the native view behind the canvas (backingView) and its options (opaque, zOrderOnTop, zOrderMediaOverlay), see Android rendering options. 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.} />
    </>
  );
}

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 a walkthrough of the frame loop, pixel density and transparency.

Presenting frames

Call context.present() after every device.queue.submit() to display the frame: unlike the Web, React Native does not swap the frame to the screen for you. present() is not part of the WebGPU spec. It runs synchronously on the calling thread, and works the same on the main JS runtime, the UI thread and dedicated worklet runtimes. See Frame presentations.

Pixel density

As on the Web, clientWidth / clientHeight are the layout size in points and track the view's layout, while width / height are the drawing-buffer size in pixels. The buffer is not scaled to the pixel ratio by default: set it to the layout size times PixelRatio.get(), and again whenever the layout changes. See Pixel density.

Transparent compositing

To show the views behind the canvas, configure the context with alphaMode: "premultiplied" and clear with an alpha of 0, as the Usage example does. On iOS and on the web that is all it takes. On Android, alphaMode is ignored: the backing view decides, and the default TextureView composites with alpha unless android={{ opaque: true }} asks it not to (see below). Setting both gives the same result on every platform.

Color space

context.configure({ colorSpace }) says which primaries the canvas's values are in: "srgb" (the default) or "display-p3". Both use the sRGB transfer function, as on the Web. On iOS the layer is tagged Display P3 (extended Display P3 for an rgba16float canvas), so a shader's output reaches a wide-gamut display unconverted and matches other Display P3 content, such as a Skia canvas. Android currently ignores colorSpace.

HDR (extended dynamic range)

context.configure({ toneMapping: { mode } }) says what happens to values above 1.0 on an rgba16float canvas: "standard" (the default) clamps them to the display's SDR range, "extended" lets them use the display's headroom, as in a browser. On iOS 16 and later the CAMetalLayer opts into extended dynamic range content for "extended"; the mode is accepted and has no effect on an 8-bit canvas, which cannot hold values above 1.0. Android currently ignores toneMapping.

Android rendering options

On Android, Canvas can be backed by one of three native views, with different trade-offs between speed and how well the canvas composites with the views around it. The android prop names it, along with its options; it is ignored on iOS and web.

OptionTypeEffect
backingView"TextureView" | "SurfaceView" | "HardwareBufferView"The native view behind the canvas. Defaults to TextureView
opaquebooleanDeclares that the canvas covers every pixel of its bounds, so it is composited without blending. Defaults to false on a TextureView and to true on a SurfaceView; a HardwareBufferView always composites with alpha. Applied in place
zOrderOnTopbooleanSurfaceView only: draws the canvas above every React Native view in the window. Defaults to false
zOrderMediaOverlaybooleanSurfaceView only: draws the canvas above the other SurfaceViews below the window (video players, other canvases). Defaults to false; zOrderOnTop takes precedence
TextureViewSurfaceViewHardwareBufferView
Minimum Android8 (API 26)8 (API 26)10 (API 29)
Behaves like a regular viewyesnoyes
Latencyhighestlowestin between
Opaque by defaultnoyesnever
Statusdefaultopt-inexperimental, opt-in

TextureView is the default: a regular view, so transforms, clipping, alpha and z-order all apply, and the canvas can be composited over other React Native views. The cost is an extra copy, typically a frame of latency, and occasional stalls during some view animations.

SurfaceView is the fastest option. It is not regular view content, though: parent transforms, clipping, rounded corners and z-ordering with sibling views do not apply to it. Use it for a canvas that is a plain rectangle: a full-screen scene or a game. It is opaque unless asked otherwise: a translucent SurfaceView blends against the window background (usually black) instead of the React Native views beneath it, which are punched out. Pair it with zOrderOnTop to composite over React Native content, and note that zOrderOnTop draws above all React Native views in the window, including navigation screens, regardless of zIndex.

HardwareBufferView composites like a TextureView, with lower latency. It is experimental: if the device cannot allocate the buffers the canvas shows nothing instead of falling back, and some devices may show a stale or torn frame. Test it on your target devices, and drop the backingView to go back to TextureView. Below Android 10 it uses a TextureView, with a warning in development.

The backing view is created when the canvas mounts and kept while it is mounted. opaque applies in place, and so do the z-order options on Android 11 and later; a change of backingView is ignored, with a warning in development. To switch backing views, remount the canvas with a key. The remount leaves the canvas off screen for a frame or two, showing what is behind it, or black, until the new view draws its first frame: the old view leaves and the new one arrives in different window frames. Keeping a canvas on screen through such a swap is possible, but takes hold-and-retire logic specific to each Android release (#563) that the library does not carry.

// The backing view is chosen at mount: a canvas that switches it remounts
<Canvas key={backingView} android={{ backingView }} />

Some combinations for other trade-offs:

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

// Full-screen scene on a SurfaceView: no composition pass
<Canvas android={{ backingView: "SurfaceView" }} />

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

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

See also