あらゆるものから逃げるReact+WebGLテンプレートを作った
React+WebGL で SPA の何かしらを作る際のテンプレートを作りました。あらゆるものから逃げます。
リポジトリはこちら。
Netlify にプレビュー放り投げたのでこちらもどうぞ。
https://did0es-react-twgl-boilerplate.netlify.app
構成
React を使いつつ、CRA を使わず webpack のコンフィグを書いています(どうせ eject するので webpack から書いた)。
- React (17.x)
- Webpack (5.x)
- Babel (17.x)
- TypeScript (4.x)
- Linaria (2.x)
- Universal Router (9.x)
- TWGL.js (4.x)
何から逃げたのか
まず前提として純粋なSPAを作りたいと思ったので、 Next.js は使わない方針でいきました。 Next.js は SSG/SSR するときの踏み台にしたい気持ちがあります。
以下のものから逃げました。
- react-router
- 代用: universal-router
- three.js
- 代用: TWGL.js
- CSS Modules, styled-component
- 代用: linaria
逃げた理由
react-router
react-rouer は使う時に手の届かないところがあるとか、破壊的変更を繰り返しているのでついて行くのに疲れたからといった感じです。あとは universal-router を試してみたかった気持ちもあります。SPA でページ遷移を表現するには history とか location 周辺を自分で書く必要があるので、そういった部分の学びになりました。
完成したものが以下のリンクから見れます。isomorphic さはそこまで求めていなかったのですが、path-to-regexp でかなりシンプルな Router ということで使い勝手が良さそうというのが所感です。
react-twgl-boilerplate/src/routes
three.js
以下のように非常に苦しみました。
https://twitter.com/did0es/status/1355506728507371520
バンドルサイズを減らそうと思ったのですが tree shaking がまともに出来ない webpack の plugin で tree shaking は出来るみたいなんですが、平面に GLSL 描画するだけにしか three の用途がなかったので、もう少し軽量な TWGL に逃げることにしました。
以下は React で TWGL.js を使う際のコードです。
// sourced from <http://twgljs.org/examples/tiny.html>
import {
createBufferInfoFromArrays,
createProgramInfo,
drawBufferInfo,
resizeCanvasToDisplaySize,
setBuffersAndAttributes,
setUniforms,
} from 'twgl.js';
import { css } from '@linaria/core';
export const Canvas: React.FC = () => {
const onCanvasLoaded = (element: HTMLCanvasElement) => {
if (!element) return;
const gl = element.getContext('webgl');
if (gl == null) return;
const programInfo = createProgramInfo(gl, [
require('./shaders/vertex.glsl').default,
require('./shaders/fragment.glsl').default,
]);
const arrays = {
position: [-1, -1, 0, 1, -1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, 1, 0],
};
const bufferInfo = createBufferInfoFromArrays(gl, arrays);
function render(time: number) {
if (!gl?.canvas) return;
resizeCanvasToDisplaySize(gl.canvas as HTMLCanvasElement);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
const uniforms = {
time: time * 0.001,
resolution: [gl.canvas.width, gl.canvas.height],
};
gl.useProgram(programInfo.program);
setBuffersAndAttributes(gl, programInfo, bufferInfo);
setUniforms(programInfo, uniforms);
drawBufferInfo(gl, bufferInfo);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
};
return <canvas className={wrap} ref={onCanvasLoaded} />;
};
const wrap = css`
width: 100%;
`;
WebGL API を叩いたときほど書く量は減ったのではないでしょうか。 three ほどライブラリの容量もないので快適です。
three の軽量化について
試してはいないんですが調べるだけ調べました。
Three.js の issue の discussion とか、 この記事 (日本語です) を見てもらうとわかるんですが、import { Foo } from 'three' をしても three.js 全体を読み込んでしまうので、デフォルトで tree shaking は機能しません。
こういった状況を打開するため、 webpack / rollup 向けの three-minifier という plugin があるようなので、どうしても three で tree shaking をしたいなら試してみるといいかもしれません(THREE.WebGLRenderer 単体のサイズが大きいので、 tree shaking した恩恵をあまり受けられないような気もしています)。
他に three を使わなくなった理由など
three 本体の動きとして、型定義を削除したPRがマージされた ことがあり、若干の不信感が募っていました。
The side effect of that decision was a considerable increase on maintenance burden and made things harder for new contributors too. For the sake of continuity of the project I think it's best to move them back to DefinitelyTyped (or any other repo interested in maintaining them).
Mr.doob 氏曰く、とにかくメンテが辛かったので負荷を減らしたかったらしいですね。お疲れさまでした。
three の型はもういいとして、平面に GLSL を描画する以外のことがしたくなったらまた戻ってくるかもしれないですが、しばらくは TWGL 使うつもりです。
CSS
CSS が JS のランタイムに組み込まれるのは嫌なんですが、 CSSinJS はやりたかったので linaria を使いました(CSSはファイルとして読み込んでほしい)。
おわりに
Next.js に頼らないかつ最小の構成で React + WebGL を組みました。その他の疑問点や展望については以下のとおりです。
- 画像最適化
- 適切なチャンク分割
- かなり雑にやった
- GLSL ファイルを読み込むための raw-loader
- 他に良さそうな loader があるならそちらに移行
- 動的な routing 対応
まだ劣化 Next.js に WebGL の環境のせましたという感じなのでここらへんやっていきます。