OGL and Choosing a Library

OGL 4,660 (github.com/oframe/ogl (https://github.com/oframe/ogl 4,660 ), Unlicense, 1.0.11) is "a small, effective WebGL library" for developers who write their own shaders: Renderer, Transform, Mesh, Program and Geometry mirror Three.js 30,815 's concepts, but a Program is your GLSL, with no lighting model or material system. It asks for WebGL 2 and falls back to WebGL 1, and it has no WebGPU path. It suits a hero effect on a marketing page, where Three.js's size is not worth paying.

To compare sizes, each library's one-box app (npm Packages's orbit-camera scene for Babylon.js 89,603 and Three.js) was bundled with esbuild 0.28.2 126 (--bundle --minify --format=esm, gzip -9); A-Frame 172,534 's is its prebuilt CDN build:

Minimal one-box apps, measured 26 September 2026
Library License Bundle, min / gzip Choose it for
OGL 1.0.11 Unlicense 49 KB / 14 KB Small custom-shader effects
Three.js 0.186.1 MIT 556 KB / 139 KB General 3D, the largest ecosystem
Babylon.js 9.28.0 Apache-2.0 1,516 KB / 352 KB A complete engine with tools built in
A-Frame 1.8.0 MIT 1,323 KB / 348 KB (full build) Declarative VR and AR
PlayCanvas 2.22.4 62,178 MIT; paid Editor tiers 1,994 KB / 518 KB Games made in a browser editor

For shaders on a 2D page, OGL or raw WebGL (WebGL) is enough. For a product viewer, visualization or configurator, Three.js has the most examples and answers. When you want physics, GUI and tools from one vendor, Babylon.js (Babylon.js); when a team wants to build in an editor, PlayCanvas; for WebXR content written as markup, A-Frame. BookNest's bookshelf stays in Three.js; its bookstore moves to Babylon.js.

A 3D bar chart of minimal one-box bundle sizes for OGL, Three.js, A-Frame, Babylon.js and PlayCanvasHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  .ui { padding: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="ui">Show <label><input type="radio" name="m" value="gzip" checked> gzip</label> <label><input type="radio" name="m" value="min"> minified</label> KB · drag to orbit</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Minimal one-box apps, bundled with esbuild and gzip -9 (measured 26 September 2026)
const LIBS = [
  { name: 'OGL 1.0.11', min: 49, gzip: 14, color: '#2a9d8f' },
  { name: 'Three.js 0.186.1', min: 556, gzip: 139, color: '#1f5f8b' },
  { name: 'A-Frame 1.8.0', min: 1323, gzip: 348, color: '#5b3f99' },
  { name: 'Babylon.js 9.28.0', min: 1516, gzip: 352, color: '#b5452f' },
  { name: 'PlayCanvas 2.22.4', min: 1994, gzip: 518, color: '#e09a10' },
];

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 300);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 2, 0.1, 50);
camera.position.set(1.5, 2.2, 6.2);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.3));
const sun = new THREE.DirectionalLight('#ffffff', 2);
sun.position.set(3, 5, 4);
scene.add(sun);
const base = new THREE.Mesh(new THREE.BoxGeometry(5.6, 0.05, 1.4), new THREE.MeshStandardMaterial({ color: '#e0d8c8' }));
base.position.y = -0.025;
scene.add(base);

// A text label on a canvas texture, as a plane that always faces the camera (a Sprite)
function label(text, size = 40) {
  const canvas = Object.assign(document.createElement('canvas'), { width: 512, height: 96 });
  const g = canvas.getContext('2d');
  g.font = `bold ${size}px system-ui, sans-serif`;  g.textAlign = 'center';  g.fillStyle = '#263238';
  g.fillText(text, 256, 60);
  const map = new THREE.CanvasTexture(canvas);
  map.colorSpace = THREE.SRGBColorSpace;
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map, transparent: true }));
  sprite.scale.set(1.6, 0.3, 1);
  return { sprite, canvas, g, map };
}

const bars = LIBS.map((lib, i) => {
  const bar = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1, 0.6), new THREE.MeshStandardMaterial({ color: lib.color, roughness: 0.5 }));
  bar.position.x = (i - 2) * 1.1;
  scene.add(bar);
  const name = label(lib.name, 34);
  name.sprite.position.set(bar.position.x, -0.25, 0.75);
  const value = label('', 44);
  scene.add(name.sprite, value.sprite);
  return { lib, bar, value, height: 0.01 };
});

let metric = 'gzip';
document.querySelectorAll('input[name=m]').forEach((r) => r.addEventListener('change', () => { metric = r.value; }));
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0.9, 0);
controls.update();

renderer.setAnimationLoop(() => {
  const max = Math.max(...LIBS.map((l) => l[metric]));
  for (const b of bars) {
    const target = 2.4 * b.lib[metric] / max;
    b.height += (target - b.height) * 0.08;             // ease the bars to their new heights
    b.bar.scale.y = b.height;
    b.bar.position.y = b.height / 2;
    b.value.sprite.position.set(b.bar.position.x, b.height + 0.25, 0);
    const text = `${b.lib[metric].toLocaleString()} KB`;
    if (b.value.text !== text) {                         // redraw the label only when it changes
      b.value.text = text;
      b.value.g.clearRect(0, 0, 512, 96);
      b.value.g.fillText(text, 256, 60);
      b.value.map.needsUpdate = true;
    }
  }
  renderer.render(scene, camera);
});
</script>