WebGL Animations and 3D Effects: Development and Optimization
When trying to add 3D animation through Canvas 2D, performance drops to 15 FPS on mobile devices — GPU sits idle. WebGL solves this by providing direct access to the graphics processor. When we need to render thousands of particles, deform geometry based on an audio signal, or build interactive 3D scenes without plugins — we use WebGL 2.0 (supported by 95%+ of browsers) via Three.js or directly through the WebGL API for custom tasks. For example, on a premium landing page project we replaced a CSS animation with a shader background — LCP dropped from 4.2 to 1.8 seconds, while visual complexity tripled. Contact us to assess your project.
Why WebGL Is the Standard for 3D on the Web?
WebGL is the only technology providing full 3D graphics without plugins. Unlike CSS animations or Canvas 2D, WebGL uses shaders on the GPU, delivering performance unattainable by other means. For data visualization, gamification, or premium design, it's the standard. The WebGL specification by Khronos Group is supported by all modern browsers. According to Google, interactive 3D elements increase conversion by 30–40% in e-commerce — but only with proper optimization.
How We Implement 3D Effects
We create shader backgrounds (animated noise, gradients that react to mouse), particle systems (100k+ particles animated on GPU), image deformation on hover, 3D product models with material configurators, and interactive scenes. Below is a table of typical effects.
| Effect | Tool | Performance | Prototype Time |
|---|---|---|---|
| Shader background | Raw WebGL / Three.js | 60fps on desktop | 1–2 days |
| Particle system (100k) | Three.js with BufferGeometry | 30–60fps on mobile | 2–3 days |
| Image distortion | Three.js with displacement map | 60fps | 1 day |
| 3D product model | Three.js + GLTF | Depends on polygons | 5–7 days |
Technical Implementation: Shaders, Particles, Deformations
Three.js and Raw WebGL
Three.js is the de facto standard for most web projects. It abstracts shaders and buffers, providing scene, camera, lighting. Version r169+ supports WebGPU as an alternative renderer. Raw WebGL is used when full control is needed: custom geometric primitives, non-standard blend modes, minimal bundle size. GLSL shaders are written manually for each effect.
// Vertex shader — plane deformation by noise uniform float uTime; uniform float uAmplitude; varying vec2 vUv; vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } void main() { vUv = uv; vec3 pos = position; float noise = snoise(vec2(pos.x * 0.5 + uTime * 0.3, pos.y * 0.5)); pos.z += noise * uAmplitude; gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } // Three.js — scene initialization with post-processing import * as THREE from 'three' import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js' import { RenderPass } from 'three/addons/postprocessing/RenderPass.js' import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js' const renderer = new THREE.WebGLRenderer({ canvas: document.querySelector('#webgl'), antialias: true, alpha: true, }) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.setSize(window.innerWidth, window.innerHeight) renderer.toneMapping = THREE.ACESFilmicToneMapping const composer = new EffectComposer(renderer) composer.addPass(new RenderPass(scene, camera)) composer.addPass(new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 0.8, // strength 0.4, // radius 0.85 // threshold )) Particle System
For 100k+ particles, we use BufferGeometry with attributes in Float32Array. Animation runs entirely in the vertex shader — CPU is not involved at runtime.
const COUNT = 150000 const positions = new Float32Array(COUNT * 3) const randoms = new Float32Array(COUNT) for (let i = 0; i < COUNT; i++) { positions[i * 3 + 0] = (Math.random() - 0.5) * 10 positions[i * 3 + 1] = (Math.random() - 0.5) * 10 positions[i * 3 + 2] = (Math.random() - 0.5) * 10 randoms[i] = Math.random() } const geometry = new THREE.BufferGeometry() geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1)) const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uSize: { value: 3.0 * renderer.getPixelRatio() }, }, vertexShader: particleVertexShader, fragmentShader: particleFragmentShader, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }) Image Distortion on Hover
The image texture is deformed using a displacement map based on cursor position. A 'liquid' hover effect.
// Uniforms for passing to shader const uniforms = { uTexture: { value: texture }, uDisplacement: { value: displacementTexture }, uMouse: { value: new THREE.Vector2(0, 0) }, uVelo: { value: 0 }, } // Tracking mouse movement speed let lastMouse = new THREE.Vector2() let currentVelo = 0 window.addEventListener('mousemove', (e) => { const current = new THREE.Vector2( e.clientX / window.innerWidth, 1.0 - e.clientY / window.innerHeight ) const delta = current.distanceTo(lastMouse) currentVelo = Math.min(delta * 10, 1.0) lastMouse.copy(current) uniforms.uMouse.value.copy(current) }) Loading Assets
3D models in .glb format (binary GLTF). Compression via Draco (geometry) + KTX2 (textures).
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js' import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js' const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('/draco/') const ktx2Loader = new KTX2Loader() ktx2Loader.setTranscoderPath('/basis/') ktx2Loader.detectSupport(renderer) const loader = new GLTFLoader() loader.setDRACOLoader(dracoLoader) loader.setKTX2Loader(ktx2Loader) loader.load('/models/scene.glb', (gltf) => { scene.add(gltf.scene) }, (progress) => { const pct = (progress.loaded / progress.total * 100).toFixed(0) onProgress(pct) }) Performance Optimization and Adaptivity
Framerate target is 60 FPS on desktop, 30 FPS on mobile with automatic quality reduction. Determined via navigator.hardwareConcurrency and a benchmark on first render. Key rules: one drawcall instead of a thousand (InstancedMesh), renderer.setPixelRatio(Math.min(devicePixelRatio, 2)), dispose on unmount, post-processing only when prefersReducedMotion === false. A comparison of approaches is below.
| Parameter | Three.js | Raw WebGL |
|---|---|---|
| Development speed | High (ready-made components) | Low (manual control) |
| Performance | Sufficient for 90% of tasks | Maximum with custom optimization |
| Bundle size | ~600 KB (min) | ~50 KB (only shaders) |
| When to choose | Standard effects, critical deadlines | Unique scenes, minimal size |
How to Optimize WebGL for Weak Devices?
Automatically check GPU power via a benchmark on load. If FPS drops below target, reduce pixel ratio, particle count, disable post-processing. For mobile, use InstancedMesh and avoid transparent materials. Support for prefers-reduced-motion disables animation entirely. This approach saves up to 40% of traffic and ensures a smooth experience on any device.
What's Included in the Project?
- Prototype of the effect with source shaders
- Integration into your framework (React, Vue, Angular, vanilla)
- Optimization for Core Web Vitals and mobile devices
- Documentation for setup and maintenance
- Access to the code repository
Get a consultation within one business day — contact us to discuss your task.
Work Stages and Estimated Timelines
- Briefing and requirements analysis — 1–2 days
- Prototyping the main effect — 3–5 days
- Development and optimization — 5–10 days
- Device testing — 2–3 days
- Deployment and documentation — 2 days
Total: from 10 to 20 business days depending on complexity. Cost is calculated individually. Contact us to assess your project.







