見出し画像

three.jsファイルサイズ軽量化計画

three.jsを使用したプロジェクトのファイルサイズを
できるだけ小さくしたい

three.jsは大変便利なWebGLのフレームワークですが、その他機能差ゆえ、minifyされたthree.min.jsですら600KBを超える大きなフレームワークです。

Webサイトで使用することを考えると、読み込むファイルのファイルサイズが大きいほどページが表示されるまでに時間がかかってしまいます。
とりわけモバイルデバイスでネットワークの状況がいまいち不安定だったりすると、ローディングを待っている間に離脱される可能性もあります。

つまりファイルサイズは小さければ小さいほど良いので、three.jsも使用する機能に絞ってできるだけファイルサイズを小さくしたいですね。

一番簡単な方法: カスタムビルド

three.jsはGitHub上にソースコードと、three.jsをビルドするための環境が置いてあります。

それを使用して、膨大な全機能の中から、必要な機能だけを集めたカスタム版three.jsをビルドしてしまおうという方法です。

ビルドの環境はNode.jsによるものですが、普段Node.jsによる開発環境を使用しないという方でも、ちょろっとNode.jsをマシンにインストールすればすぐこの方法が使えますので、試してみる価値はあると思います。

カスタムビルド Step1:
まずはビルド環境の構築

公式サイトまたはGitHubのthree.jsリポジトリからソースをダウンロードします。

three.jsのソースコードと、Node.js (Rollup)でビルドするための環境がまるっとダウンロードできます。

(ここからはNode.jsがマシンにインストールされている前提です。)

まずはダウンロードしたディレクトリで

$ npm install

すると必要な開発環境をインストールしてくれるので、完了したら、

$ npm run build

を実行すると

build/three.js (非圧縮版)
build/three.module.js (ES module版)

が出力されます (ソースをダウンロードした時点でこいつらは存在しますが、作成日時がビルドした日時になると思います)。また、

$ npm run build-closure

を実行すると、上記2つに加えて、圧縮版である

build/three.min.js

が出力されます。

カスタムビルド Step2:
必要な機能の選定

さて、必要な機能だけに絞ってビルドするにはどうするのか。ビルドするときのエントリーポイントファイルが

src/Three.js

となっており、ここに以下のように機能ごとにそれぞれモジュールへのパスが記述されています。

export { WebGLRenderer } from './renderers/WebGLRenderer.js';

(ES moduleとか今更な人もいますと思いますが)ここの「export」に続く{ WebGLRenderer }と書いてあるモジュールが、

THREE.WebGLRenderer

という形で使えるようになっています。

なので、使用するモジュールだけ残して、あとはコメントアウトまたは削除してしまいます。

特に注意したいのは、Geometry、Materialは

export * from './geometries/Geometries.js';
export * from './materials/Materials.js';

と記述されおり、それぞれすべてのモジュールが読み込まれてしまっているので、ここもできれば以下のように使用するものだけに絞ります。

export { BoxGeometry } from './geometries/Geometries.js';
export { MeshBasicMaterial } from './materials/Materials.js';

以下、 src/Three.js のサンプルです。

import './polyfills.js';
import { REVISION } from './constants.js';

export { WebGLRenderer } from './renderers/WebGLRenderer.js';
export { Scene } from './scenes/Scene.js';
export { Mesh } from './objects/Mesh.js';
export { BoxGeometry } from './geometries/Geometries.js';
export { MeshBasicMaterial } from './materials/Materials.js';
export { PerspectiveCamera } from './cameras/PerspectiveCamera.js';
export { Matrix4 } from './math/Matrix4.js';
export { Vector4 } from './math/Vector4.js';
export { Vector3 } from './math/Vector3.js';
export { Vector2 } from './math/Vector2.js';
export { Quaternion } from './math/Quaternion.js';
export { Color } from './math/Color.js';
export * from './constants.js';

if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

 /* eslint-disable no-undef */
 __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {
   revision: REVISION,
 } } ) );
 /* eslint-enable no-undef */

}

元のファイルはこちら
https://github.com/mrdoob/three.js/blob/dev/src/Three.js

これはかなり極端な例ですが、ここまで減らしたことによって
圧縮版であるthree.min.jsが 609KB から 409KB まで軽量化できました。

ここまで記述を減らしたのに200KBくらいしか減らせませんでしたが、そもそもWebGLの描画に必要なWebGLRendererが結構容量が大きいようなので仕方ないですね。

それでも1/3くらいは減らせたので良しとしましょう。

応用編:
モジュールバントラーによるTree Shaking

Node.jsで動くモジュールバンドラー(WebpackやRollupなど)を使用している場合、いくつか条件をクリアするとTree Shakingを有効にすることができ、使用していないコードを含めず最適化して出力できるようになります。

Tree Shakingに関してはこのあたりが詳しく説明してくれています。

Tree Shaking step1:
モジュールをES moduleの形式で書く

モジュール作る (exportする)際、読み込む(importする)際、CommonJS module形式の

module.exports = function() {
  ...
}
const aaa = require('aaa');

ではなく

export default function() {
  ...
}
import aaa from 'aaa';

と書くのが第一の条件です。(RollupはCommonJSでもTree Shakingしてくれるらしい?)

CommonJSとES moduleのimport/exportがよくわからない方はこちら。

※ただし、バンドラーの設定等でimport, exportをCommonJS module形式に変換されてしまうことがあるので、その場合はTree Shakingされない。

@babel/preset-envの場合はオプションで module: false を設定します。

Typescriptの場合はtsconfigのcompilerOptionsで module: "es2015" を設定します。

また、Webpackの場合は、オプションのmode: "production" になっていないとTree shakingされません。

Tree Shaking step2:
three.jsの読み込みで実践

もし普段、以下のようにthree.jsを読み込んでいる場合、

import * as THREE from 'three'

scriptタグでthree.min.jsを読み込んだときと同様に

var renderer = new THREE.WebGLRenderer();

みたいな感じで使えて便利ですが、これだとすべてのモジュールが読み込まれている状態です。この記述だと561KBでした。

(ビルド版と微妙にサイズが異なりますが、ビルド版に含まれる一部のスクリプトが含まれていないのでファイルサイズが違うのだと思います。おそらく後述のpolyfillsやLegacyだと思われます。未検証。)

これだと意味がないので以下のように必要なモジュールだけimportします。

import { WebGLRenderer, Mesh, PlaneGeometry, MeshBasicMaterial, PerspectiveCamera } from 'three';

この状態でビルドすれば適切にTree shakingが行われるはず!

Tree Shaking step3:
ビルドした結果は、、、

結果、561KB。あれ、、機能全部入りと変わらない

いろいろ調べた結果、どうやらthree.js自体がTree Shakingに対応していないらしい。。ここまで実践いただいた方、すみません。。

Tree Shaking step4:
結論、現状(rv114)ではthree.jsは
Tree Shakingできない

ということらしいです。いろいろ議論されてはいますが、threeのpackage.jsonをいじらなければいけなかったり、Webpackで設定が必要だったりとあまりスマートではない印象です。

https://github.com/mrdoob/three.js/issues/16059#issuecomment-535468967

解決策1:
カスタムビルドして使う

なんと最初に戻りました。しかし簡単で有効な策です。先に取り上げたカスタムビルドで予め機能を削ったものを読み込みます。

scriptタグで読み込んでもいいですが、生成された three.module.js をプロジェクトの任意のディレクトリに配置し、three.jsのモジュールを読み込みます。

import * as THREE from '[任意のディレクトリ]/three.module.js';

または

import { WebGLRenderer, PerspectiveCamera } from '[任意のディレクトリ]/three.module.js';

などのように使います。

解決策2:
モジュールのソースを直接読み込む

これはパスの記述がちょっとめんどくさいのですが、

import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer';
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera';

のように機能ごとにモジュールを直接読み込んでしまう方法です。

注意点としては、build版に含まれる

three/src/polyfills.js
three/src/Three.Legacy.js

などが含まれていないため、必要であればそれも適宜importします。

解決策3:
プラグインを使う

RollupとWebpackで使用できるthree-minifierというプラグインがあるようです。使い方はGitHubを参照。

説明は省きますが正しく動作しました!

・・・

というわけで、なかなかしんどいです。

ちなみに、three.jsではだめでしたが、モジュールバンドラの設定をしっかりやっておけば、Tree shakingに対応しているモジュールを読み込んだ際にshakingしてくれるので、やった事自体は無駄ではないです。

サポートいただければ、レッドブルを飲んでより頑張れると思います。翼を授けてください。