How To Optimize Loading WebGL Scenes

If you don’t want 3D animations to tank your page speed, here is a quick optimization snippet.

Problem

If you’ve ever played with WebGL in your designs, you know that generating or importing 3D models can easily tank your google page speed score.

The reason is straightforward: the main thread spends too much time parsing the Javascript and slows the rendering of the rest of the page.

Solution

Put all the rendering logic in a web worker:

/public/workers/scene.js

import * as THREE from "https://unpkg.com/three@0.165.0/build/three.module.js";

let renderer, camera, scene;

self.onmessage = (e) => {
  const data = e.data;

  if (data.canvas) {
    const canvas = data.canvas;

    const width = data.width;
    const height = data.height;

    scene = new THREE.Scene();

    // ... scene setup

    function animate() {
      self.requestAnimationFrame(animate);

      // ...animation logic

      renderer.render(scene, camera);
    }

    animate();
  }

  if (data.type === "resize" && renderer && camera) {
    renderer.setSize(data.width, data.height, false);
    camera.aspect = data.width / data.height;
    camera.updateProjectionMatrix();
  }
};

In your main thread, specify you want to use an offscreen canvas to render your 3D animation:

<canvas id="mainCanvas" class="fixed top-0 left-0 w-full h-full z-[-1]"
></canvas>

<script>
    const worker = new Worker("/workers/scene.js", { type: "module" });

    const canvas = document.getElementById("mainCanvas");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const offscreen = canvas.transferControlToOffscreen();

    worker.postMessage(
        {
            canvas: offscreen,
            width: window.innerWidth,
            height: window.innerHeight,
        },
        [offscreen],
    );

    window.addEventListener("resize", () => {
        const width = window.innerWidth;
        const height = window.innerHeight;
        worker.postMessage({ type: "resize", width, height });
    });
</script>

We use one message type for setting up the scene, and another to handle window resizing.

The 3D scene will be rendered in a web worker, separate from the main thread so as to not block it.

See it for yourself: simply copy/paste the URL of this article in google page speed or run lighthouse directly from the chrome devtools, and you’ll see the performance is unaffected by the worker script I used to load the 3D background animation.