Tuning Three.js for Lightweight 3D
The spinning 3D assets on this site are a fun detail, here is how I implemented them.
Three.js UofT logo - same as shown on the home page
Right-Sizing GLTF Assets
I created my own .gltf file in blender using a UofT jpg as a sample.
The scene normalizer recenters the model and enforces a consistent scale. That keeps rotations predictable when swapping models.
function resizeCanvasToDisplaySize(renderer, camera) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
if (canvas.width !== width ||canvas.height !== height) {
// you must pass false here or three.js sadly fights the browser
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// set render target sizes here
}
}
function uoft() {
const canvasElement = document.querySelector(".logo.uoft canvas");
if (!canvasElement) {
console.log("UofT canvas not found, skipping initialization");
return;
}
const renderer = new THREE.WebGLRenderer({
canvas: canvasElement,
alpha: true,
antialias: true
});
renderer.setClearColor( 0x000000, 0 ); // the default
// There's no reason to set the aspect here because we're going
// to set it every frame anyway so we'll set it to 2 since 2
// is the the aspect for the canvas default size (300w/150h = 2)
const camera = new THREE.PerspectiveCamera(43, 1, 5, 200);
camera.position.set( 1, 90, 0 );
camera.lookAt( 0, 0, 0 );
const scene = new THREE.Scene();
var loader = new GLTFLoader();
let model;
loader.load( '../assets/objects/uoft/uoft.gltf', function ( gltf ) {
model = gltf.scene
var box = new THREE.Box3().setFromObject( model );
var center = new THREE.Vector3();
box.getCenter( center );
model.position.sub( center ); // center the model
model.rotation.y = Math.PI/2; // rotate the model
scene.add( model );
}, undefined, function ( error ) {
console.error( error );
} );
const light1 = new THREE.PointLight(0xffffff, 2, 500);
light1.position.set(0, 150, 0);
scene.add(light1);
function animate(time) {
time *= 0.001; // seconds
resizeCanvasToDisplaySize(renderer, camera);
if (model) {
model.rotation.x = -Math.PI/2 - 0.4 + (time * 0.5);
}
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
setTimeout(() => {
requestAnimationFrame(animate)
},10);
}
uoft();
}
Limiting the Render Loop
Animations pause when the tab loses focus thanks to requestAnimationFrame. For extra savings on low-power devices, I gate the animation until the canvas is visible by checking IntersectionObserver events.
The goal is a steady 60fps on desktop and a graceful 30fps on older phones.
With these tweaks, Lighthouse keeps reporting "Minimal main-thread work" and the WebGL experience feels snappy everywhere.