Making my homepage small with Three.js and a lighter 3D model

2025-08-25

Goal: keep the homepage under 1 MB (inspired by the 1MB Club). I still wanted a small Three.js scene with a 3D avatar of me, so I focused on two things:

Build flow is simple: JS with Vite, pages with Zola, then deploy.

Step 1: trim the bundle

Three.js was most of the weight, so I split it into its own chunk (using manualChunks for more info), turned on aggressive treeshaking, removed console/debug code, and shipped Brotli.

// vite.config.js (only the bits that matter for size)
import { defineConfig } from 'vite'
import compression from 'vite-plugin-compression'
import { terser } from 'rollup-plugin-terser'

export default defineConfig({
  publicDir: false,
  build: {
    minify: 'terser',
    treeshake: { moduleSideEffects: false, propertyReadSideEffects: false, tryCatchDeoptimization: false },
    brotliSize: false,
    rollupOptions: {
      output: {
        // put Three.js in its own cacheable chunk
        manualChunks: (id) => id.includes('node_modules/three') ? 'three' : undefined,
      },
      plugins: [
        terser({
          compress: { drop_console: true, drop_debugger: true, dead_code: true, unused: true, passes: 3 },
          mangle: { toplevel: true },
          toplevel: true,
          module: true,
        })
      ]
    },
  },
  plugins: [
    compression({ algorithm: 'brotliCompress', include: /\.(js|css|html|json)$/ })
  ]
})

Result from my build:

Step 2: trim the model

The old non-compressed avatar is on /blog/3dme/.

The first avatar I generated was big (~4.6 MB). I re-generated it with Hunyuan3D 2.1 targeting about 5k faces, then simplified and re-compressed with glTF-Transform:

gltf-transform simplify me_5000.glb compressed_me_5000.glb
info: me_5000.glb (461.27 KB) → compressed_me_5000.glb (329.47 KB)

The optimized one is embedded below.

End: Final size

Final size

For now this is good enough; if I need more I might revisit with dynamic imports or KTX2 compressed textures.