DEV Community

ANKUSH CHOUDHARY JOHAL
ANKUSH CHOUDHARY JOHAL

Posted on • Originally published at johal.in

How to Use WebGL 2.0 with TypeScript 5.9 and Next.js 16 for Browser Games 2026

How to Use WebGL 2.0 with TypeScript 5.9 and Next.js 16 for Browser Games 2026

Browser games in 2026 demand high performance, type safety, and seamless framework integration. This guide walks through building modern browser games using WebGL 2.0 for hardware-accelerated rendering, TypeScript 5.9 for robust type checking, and Next.js 16 for optimized asset delivery and routing.

Prerequisites

  • Node.js 22+ (LTS version for 2026)
  • Next.js 16 CLI installed globally or via npx
  • TypeScript 5.9 or later
  • Basic understanding of WebGL 1.0/2.0 APIs and game loop concepts

Step 1: Initialize Next.js 16 Project with TypeScript

Create a new Next.js 16 project with TypeScript enabled by running:

npx create-next-app@16 my-webgl-game --typescript --app --no-tailwind --no-eslint
Enter fullscreen mode Exit fullscreen mode

Navigate to the project directory: cd my-webgl-game. Next.js 16's App Router is used by default, so all components interacting with WebGL must be marked as client-side with the 'use client' directive, as WebGL APIs are only available in browser environments.

Step 2: Configure TypeScript 5.9 for WebGL 2.0

Update your tsconfig.json to include WebGL 2.0 type definitions and strict mode checks:

{
  "compilerOptions": {
    "target": "ES2026",
    "lib": ["dom", "webgl2", "esnext"],
    "strict": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

TypeScript 5.9 includes built-in types for WebGL 2.0, so no additional @types packages are required.

Step 3: Initialize WebGL 2.0 Context

Create a client-side component WebGLCanvas.tsx to set up the canvas and WebGL 2.0 context:

'use client';

import { useRef, useEffect, useCallback } from 'react';

const WebGLCanvas = () => {
  const canvasRef = useRef(null);

  const initWebGL = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) return null;

    const gl = canvas.getContext('webgl2', { antialias: true, alpha: false });
    if (!gl) {
      console.error('WebGL 2.0 is not supported in this browser.');
      return null;
    }

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0.1, 0.1, 0.15, 1.0);
    return gl;
  }, []);

  useEffect(() => {
    const gl = initWebGL();
    if (!gl) return;

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    return () => {
      const loseContext = gl.getExtension('WEBGL_lose_context');
      loseContext?.loseContext();
    };
  }, [initWebGL]);

  return (

  );
};

export default WebGLCanvas;
Enter fullscreen mode Exit fullscreen mode

Step 4: Write a Basic WebGL 2.0 Renderer

WebGL 2.0 requires vertex and fragment shaders to render content. Create a renderer.ts module with TypeScript-typed helper functions:

// renderer.ts
type WebGLProgramInfo = {
  program: WebGLProgram;
  attribLocations: { vertexPosition: number };
  uniformLocations: { uResolution: WebGLUniformLocation | null };
};

const vsSource = `#version 300 es
in vec4 aVertexPosition;
void main() {
  gl_Position = aVertexPosition;
}`;

const fsSource = `#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
  fragColor = vec4(0.2, 0.8, 0.4, 1.0);
}`;

export const initShaderProgram = (gl: WebGL2RenderingContext, vsSource: string, fsSource: string): WebGLProgram | null => {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

  if (!vertexShader || !fragmentShader) return null;

  const shaderProgram = gl.createProgram();
  if (!shaderProgram) return null;

  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);

  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    console.error('Shader program link failed:', gl.getProgramInfoLog(shaderProgram));
    return null;
  }

  return shaderProgram;
};

const loadShader = (gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null => {
  const shader = gl.createShader(type);
  if (!shader) return null;

  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('Shader compile failed:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }

  return shader;
};

export const drawScene = (gl: WebGL2RenderingContext, programInfo: WebGLProgramInfo) => {
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  const positions = [0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0];
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

  gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);

  gl.useProgram(programInfo.program);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
};
Enter fullscreen mode Exit fullscreen mode

Step 5: Add Game Input Handling

TypeScript 5.9's strict typing makes input state management safe. Create an inputHandler.ts module:

// inputHandler.ts
type InputState = {
  keys: Set;
  mouse: { x: number; y: number; isPressed: boolean };
};

let inputState: InputState = { keys: new Set(), mouse: { x: 0, y: 0, isPressed: false } };

export const initInputHandlers = (canvas: HTMLCanvasElement) => {
  const handleKeyDown = (e: KeyboardEvent) => inputState.keys.add(e.key.toLowerCase());
  const handleKeyUp = (e: KeyboardEvent) => inputState.keys.delete(e.key.toLowerCase());

  const handleMouseMove = (e: MouseEvent) => {
    const rect = canvas.getBoundingClientRect();
    inputState.mouse.x = e.clientX - rect.left;
    inputState.mouse.y = e.clientY - rect.top;
  };

  const handleMouseDown = () => { inputState.mouse.isPressed = true; };
  const handleMouseUp = () => { inputState.mouse.isPressed = false; };

  window.addEventListener('keydown', handleKeyDown);
  window.addEventListener('keyup', handleKeyUp);
  canvas.addEventListener('mousemove', handleMouseMove);
  canvas.addEventListener('mousedown', handleMouseDown);
  canvas.addEventListener('mouseup', handleMouseUp);

  return () => {
    window.removeEventListener('keydown', handleKeyDown);
    window.removeEventListener('keyup', handleKeyUp);
    canvas.removeEventListener('mousemove', handleMouseMove);
    canvas.removeEventListener('mousedown', handleMouseDown);
    canvas.removeEventListener('mouseup', handleMouseUp);
  };
};

export const getInputState = (): Readonly => inputState;
Enter fullscreen mode Exit fullscreen mode

Step 6: Implement a Game Loop

Use requestAnimationFrame for a smooth 60fps game loop, integrated with Next.js 16's client-side lifecycle:

// gameLoop.ts
import { drawScene } from './renderer';
import { initInputHandlers, getInputState } from './inputHandler';

export const startGameLoop = (gl: WebGL2RenderingContext, programInfo: any, canvas: HTMLCanvasElement) => {
  let animationFrameId: number;
  const cleanupInput = initInputHandlers(canvas);

  const loop = () => {
    const input = getInputState();
    // Update game state based on input here

    drawScene(gl, programInfo);

    animationFrameId = requestAnimationFrame(loop);
  };

  loop();

  return () => {
    cancelAnimationFrame(animationFrameId);
    cleanupInput();
  };
};
Enter fullscreen mode Exit fullscreen mode

Step 7: Load Game Assets with Next.js 16

Next.js 16's public directory serves static assets. Load textures using TypeScript-typed fetch calls:

// assetLoader.ts
export const loadTexture = async (gl: WebGL2RenderingContext, url: string): Promise => {
  return new Promise((resolve) => {
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));

    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => {
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
      gl.generateMipmap(gl.TEXTURE_2D);
      resolve(texture);
    };
    img.onerror = () => { console.error(`Failed to load texture: ${url}`); resolve(null); };
    img.src = url;
  });
};
Enter fullscreen mode Exit fullscreen mode

Step 8: Optimize for 2026 Browser Games

  • Use WebGL 2.0 Vertex Array Objects (VAOs) to reduce attribute setup overhead
  • Leverage Next.js 16's automatic code splitting to load game modules on demand
  • Use TypeScript 5.9's const type parameters for immutable game state
  • Enable WebGL 2.0 extensions like EXT_color_buffer_float for HDR rendering
  • Cache assets with Next.js 16's incremental static regeneration (ISR) for repeat visits

Conclusion

Combining WebGL 2.0's hardware acceleration, TypeScript 5.9's type safety, and Next.js 16's optimization features lets you build performant, maintainable browser games for 2026. Extend this foundation with 3D models, physics engines, and multiplayer support to create full-featured games.

Top comments (0)