Key answer: WebGL is the low-level browser API that talks directly to the GPU; Three.js is a JavaScript library built on top of it that hides the boilerplate. Raw WebGL gives you total control and the smallest possible footprint, but you write everything yourself — buffers, shaders, matrices, the render loop. Three.js gives you a scene graph, cameras, lights, loaders, and materials out of the box. For most production 3D websites, Three.js is the right default; reach for raw WebGL only when you need tight control over the pipeline or you are doing shader-only, full-screen effects where a scene graph would be dead weight.
Table of contents
- What each one actually is
- Control vs convenience
- A short code comparison
- When to reach for each
- Common mistakes
- FAQ
- Conclusion
What each one actually is
WebGL is a rasterization API — a JavaScript binding to OpenGL ES that runs on the GPU. It does not know what a "cube" or a "camera" is. It knows about buffers of numbers, shader programs, and draw calls. Everything visual you see in a WebGL scene is something you computed and uploaded yourself.
Three.js is a rendering library that wraps WebGL (and, increasingly, WebGPU) in familiar 3D concepts: a Scene you add objects to, Mesh objects made of a Geometry and a Material, PerspectiveCamera, lights, and a WebGLRenderer that walks the scene graph and issues the draw calls for you. It ships loaders for glTF models, texture handling, and a large ecosystem of helpers.
The relationship matters: Three.js is not an alternative rendering technology. It is WebGL, organized. When you profile a Three.js page, the GPU work underneath is the same kind of work you would have written by hand.
Control vs convenience
The trade is straightforward once you name it.
Raw WebGL gives you convenience nothing, control everything. You decide the exact vertex format, how many draw calls happen, which uniforms update per frame, and how the pipeline is structured. Nothing runs that you did not write. The cost is boilerplate: setting up a single spinning triangle takes context creation, shader compilation, attribute binding, and a manual render loop before anything appears.
Three.js gives you convenience by default and control when you ask for it. Common tasks — loading a model, adding a light, orbiting a camera — are a few lines. You still get access to custom shaders through ShaderMaterial, and you can drop to raw GL calls via the renderer's context when needed. The cost is a library payload and abstractions you occasionally have to understand to debug.
Neither choice is "faster" in raw GPU terms. Performance on a 3D website comes from how many draw calls you issue, texture sizes, and shader cost — not from which of these you picked. Both can be fast, and both can be slow if used carelessly.
A short code comparison
Drawing a full-screen shader gradient. First in raw WebGL — abbreviated, but honest about the boilerplate:
const gl = canvas.getContext('webgl');
const vs = `attribute vec2 p; void main(){ gl_Position = vec4(p,0.,1.); }`;
const fs = `precision highp float; uniform float t;
void main(){ gl_FragColor = vec4(0.5+0.5*sin(t+gl_FragCoord.x*0.01),0.2,0.6,1.); }`;
function compile(type, src){ const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s; }
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs));
gl.linkProgram(prog); gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, 'p');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const tLoc = gl.getUniformLocation(prog, 't');
function frame(now){ gl.uniform1f(tLoc, now*0.001); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
The same effect in Three.js — the library handles context, buffers, and the fullscreen quad, so you focus on the shader:
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
const camera = new THREE.Camera();
const material = new THREE.ShaderMaterial({
uniforms: { t: { value: 0 } },
fragmentShader: `precision highp float; uniform float t;
void main(){ gl_FragColor = vec4(0.5+0.5*sin(t+gl_FragCoord.x*0.01),0.2,0.6,1.); }`
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
renderer.setAnimationLoop((now) => { material.uniforms.t.value = now * 0.001; renderer.render(scene, camera); });
For a shader-only effect the two are close in length, which is exactly why raw WebGL stays viable for that narrow case. Add a loaded 3D model, lighting, and camera controls, and the raw-WebGL version grows by hundreds of lines while the Three.js version grows by a handful. This is why AETumi authors its full-page scenes in Three.js and reserves raw WebGL for standalone shader backgrounds.
When to reach for each
Reach for Three.js when you are building a product-style 3D website: loaded models, materials, lighting, camera movement, or anything a designer will iterate on. You get an ecosystem — glTF loaders, post-processing, and React Three Fiber if you work in React — and far less code to maintain.
Reach for raw WebGL when the whole visual is a single full-screen fragment shader (a gradient, noise field, or background effect), when you are shipping to an extreme size budget, or when you need a custom render pipeline that a scene graph would fight you on. Shader-heavy background work is the classic case, and you can go deeper on that in WebGL backgrounds and shader effects.
If you want to learn the fundamentals — the pipeline, GLSL, uniforms — writing raw WebGL once is genuinely worth it, even if you ship Three.js afterward. Our WebGL guide hub collects that material. In practice, most AETumi templates are built on Three.js for product-style scenes and drop to raw WebGL only for full-screen shader backgrounds — the same rule of thumb this section describes.
Common mistakes
Do not treat "raw WebGL is faster" as a rule — it is not automatically faster, and a naive raw implementation with too many draw calls will lose to well-structured Three.js. Do not reimplement a scene graph in raw WebGL for a product site; at that point you are writing a worse Three.js. And do not reach for Three.js's full machinery to render one fragment shader — you will ship a library you barely use.
For working shader examples you can read and adapt, see the open-source repo at https://github.com/AETumiApp/webgl-shader-examples.
FAQ
Is Three.js just a wrapper around WebGL? Largely, yes — it organizes WebGL into a scene graph, materials, cameras, and loaders. Recent versions also target WebGPU through the same high-level API, so the abstraction is doing more than a thin wrapper.
Do I need to know WebGL to use Three.js? No. You can build complete 3D websites with Three.js without writing a shader. Knowing WebGL and GLSL helps when you want custom materials or need to debug performance, but it is not a prerequisite.
Which is better for performance on a 3D website? Neither wins automatically. Performance is driven by draw call count, texture size, and shader complexity. Both raw WebGL and Three.js can hit the same ceiling; discipline matters more than the choice.
Can I mix them? Yes. Three.js exposes its WebGLRenderer context, so you can issue raw GL calls or supply custom shaders via ShaderMaterial inside an otherwise standard Three.js app.
Do I have to choose before I start building? Not really. AETumi, an AI-native 3D web platform, ships both Three.js scenes and raw-WebGL shader backgrounds as editable source, so you can adopt the right layer per section — Three.js for models and lighting, raw WebGL for a full-screen effect — without committing the whole site to one approach.
Conclusion
WebGL and Three.js are not competitors so much as two layers of the same stack. Raw WebGL is the control layer — pick it for shader-only effects, tiny budgets, or custom pipelines. Three.js is the productivity layer — pick it for almost every real 3D website, because the scene graph, loaders, and materials save you from rebuilding them badly. Most production sites land on Three.js and keep raw WebGL in their pocket for the one full-screen shader — which is precisely how the AETumi library is organized.
If you would rather start from production-ready 3D web components and shaders instead of assembling the pipeline yourself, AETumi ships Three.js and WebGL templates at aetumi.app that you buy once and own for life — with the full source code and the AETumi MCP available in the Full Stack plan.
More from the AETumi library
Real, production-ready assets — preview the motion, grab the source.

