本文へスキップ

Vite Bundler / Tree Shaking

2024.03
Vite の Tree Shaking はどこまで効くのか。three.js と r3f を併用したときのバンドルサイズを実測した

Vite Bundler

Viteは開発モードではesbuildを利用し(高速化)、Rollupを本番ビルドに利用する(最適化)。esbuildよりもRollupのTree Shakingの方が洗練されている。

JSのESモジュールの静的な構造を利用してTree Shakingを行う。言い換えるとESモジュールでは動的な読み込みが禁止されているため、Tree Shaking が有効とも言える。

// できない
if (condition) {
	import {SomeModule} from 'Hoge'
}

Tree Shakingのプロセス自体は、TypeScriptとJavaScriptで基本的には同じ。コンパイル中にTypeScriptの型情報は削除され、ESモジュールのJSが生成されるため、Tree Shaking時には型は関係ないと思われる。

僕の理解では、ワイルドカードでimport * as HOGEしたとしても、不要なモジュールは削除される。

bundle size analyzer

viteのbundle size analyzerには下記を使用する。

yarn add -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ mode }) => {
  return {
    build: {
      rollupOptions: {
        plugins: [
          visualizer(),
        ],
      },
    },
  };
});

Next.jsのbarrel fileの最適化について

barrel fileとは複数のモジュールを1つのファイルでまとめてexportすること。

Rolldown

Viteが内部的に2種類のバンドラーを併用している点については、開発環境と本番環境で微妙な差異が生じるなどの欠点がある。

後継として開発されたRolldownは、RollupのAPI互換性を保ちつつも、esbuildのアーキテクチャにインスパイアされた設計を採用している。2024/3/8にオープンソース化。開発はRollupと同じEvan You。

ワイルドカードのimportについて

import { useEffect, useRef } from "react"
import * as THREE from "three"
// import { Scene, PerspectiveCamera, WebGLRenderer, BoxGeometry, MeshBasicMaterial, Mesh } from "three"

const CanvasContainer = () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const mountRef = useRef<any>(null)

  useEffect(() => {
    if (!mountRef.current) return

    const scene = new THREE.Scene()

    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
    camera.position.z = 5

    const renderer = new THREE.WebGLRenderer()
    renderer.setSize(window.innerWidth, window.innerHeight)
    mountRef.current.appendChild(renderer.domElement)

    const geometry = new THREE.BoxGeometry()
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    const cube = new THREE.Mesh(geometry, material)
    scene.add(cube)

    renderer.render(scene, camera)
  }, [])

  return <div ref={mountRef} />
}

export default function App() {
  return (
    <div id="canvas-container">
      <CanvasContainer />
    </div>
  )
}

viteのrollup optionsでtreeshake: falseを設定すると、three/build/three.module.jsは1.22MBあるということがわかる。treeshake: trueにして、ビルドすると798.73KBになるので、きちんとtree shakingされているようです。

ただしr3fを併用した場合、ワイルドカードを使おうが、モジュールごとにimportしようが、three/build/three.module.js のバンドルサイズは1.22MBのまま。(r3f内部でthreejsが使用されているのが、うまくtree-shakeされないのでは…?)

import { Canvas } from "@react-three/fiber"
import { Color } from "three"

const CanvasContainer = () => {
  const color = new Color("red")

  return (
    <Canvas>
      <mesh>
        <boxGeometry />
        <meshBasicMaterial color={color} />
      </mesh>
    </Canvas>
  )
}

fiberのCanvasではなく、Custom Canvasを作成すると、たしかに下記のようにthreeモジュールはtree-shakingされます。ただ、createRoot(canvas).render()ということは、ReactDOMのrootの外側に作成することになるので、いつ使うんだろうという気もします。

// import React from "react"
// import ReactDOM from "react-dom/client"
// import App from "./App"
// import "./styles/index.css"

// ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>
// )

import { extend, createRoot } from "@react-three/fiber"
import * as THREE from "three"
// import { Mesh, BoxGeometry, MeshStandardMaterial } from "three"

extend(THREE)
// extend({ Mesh, BoxGeometry, MeshStandardMaterial })

createRoot(document.getElementById("canvas-root") as HTMLCanvasElement).render(
  <>
    <mesh>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  </>
)