モダンフロントエンド = 宣言的 UI = 仮想 DOM

ターゲット

  • npm ツールチェインが使えない環境で、パフォーマンスを悪化させずにモダンフロントエンドをやりたい人
  • サードパーティスクリプトを提供する人

方向性

  • 省ビルドサイズを目指す
  • 実行時パフォーマンス要求
    • よほど複雑なアルゴリズムを実行するので無い限り、省ビルドサイズ制限を満たせば十分
    • モバイルで重いほとんどのケースでは、バンドルサイズによるダウンロードと、その重い JS を CPU でパースする時間に起因
  • 実行環境の古さと向き合うか
    • IE を含むか含まないかで、ビルドが必要かどうかが変わる
    • ターゲットに IE 含まず、RTT や 参照するライブラリ数が問題ないなら、CDN に対して dynamic import を使うのも可

ESM を扱う CDN

環境(バンドラ)

ESM を読み込み、それを ESM としてビルドして、dynamic import 経由で読み込むことを考慮すると、だいたい rollup 一択になる。

webpack は、現状は ESM 吐き出しと treeshake をサポートしていない。

自分は、 rollup ベースで自分用のコンパイラを作った。

mizchi/uniroll: Opinionated universal frontend bundler in browser

  • ブラウザ上のオンメモリでビルドできる
  • pika.cdn から npm module を解決
  • TypeScript Support

ここで遊べる。

Uniroll Playground

ブラウザ内でビルドしてプレビューしているので、他のバンドラと比べてだいぶ速いのがおわかりいただけると思う。

ライブラリ選定

とにかく軽量かつ複雑性に耐えられるものを選ぶ。この分野で著名なのは、preact の developit 氏。

基本的に、vanilla(0kb) or preact(10kb) or lit-html(11kb) の三択になる。

vue も考えたがランタイムだけでも 63kb なのでアウト。vue3 でもあまり小さくならなさそうだった。

preact + goober

例えば、 次のコードをビルドすると 11kb になる。

Uniroll Playground

/* @jsx h */
import { h, render } from "preact@10.4.4";
import { useState } from "preact@10.4.4/hooks";

import { styled, setup } from "goober@2.0.0";

setup(h);

const Container = styled("div")`
  width: 100px;
  height: 100px;
  background: wheat;
`;

function App(props: { text: string }) {
  const [state, setState] = useState(0);
  return (
    <Container onClick={() => setState((n) => n + 1)}>
      Hello, {props.text}, {String(state)}
    </Container>
  );
}

const el = document.createElement("div");
document.body.append(el);
render(<App text="xxx" />, el);

react がわかる人なら、 css in js + hooks で、だいたいのものが実装できるのがわかると思う。preact 独特の挙動はあるが、使っているうちになんとなく察する程度。

省ビルドサイズ環境での goober のような CSS in JS の良い点として、treeshake + terser による最適化時に、そのコンポーネントが実行パスに含まれるかどうかで、CSS の未使用コードの削除が自動的に行われる。ソースを解析する purgecss のようなアプローチもあるが、こちらのほうが手軽に最適化が掛かる。

preact + goober + shadow root 環境

サードパーティスクリプトを書いていると、CSS 干渉が問題になることがある。モダン環境では shadowRoot を使ってこの問題を回避できる。
styled-components では、 document.head に style タグを挿入するので、親スコープを参照できない shadowRoot 下では CSS が適用されない。
が、goober が任意のルートへの style の吐き出しでこれをサポートしている。

/* @jsx h */
import { h, render } from "preact@10.4.4";
import { styled as _styled, setup } from "goober@2.0.0";

// setup for shadow root
setup(h);
const root = document.createElement("div");
document.body.append(root);
const shadowRoot = root.attachShadow({ mode: "closed" });
const styled = _styled.bind({ target: shadowRoot });

const Container = styled("div")`
  width: 100px;
  height: 100px;
  background: wheat;
`;

function App(props: { text: string }) {
  return <Container>Hello, {props.text}</Container>;
}
render(<App text="xxx" />, shadowRoot);

lit-html を使う (+htm)

react, preact と同じように差分更新を行う View ライブラリ。polymer の中で使われている。

import { html, render } from "lit-html";

// A lit-html template uses the `html` template tag:
let sayHello = (name) => html`<h1>Hello ${name}</h1>`;

// It's rendered with the `render()` function:
render(sayHello("World"), document.body);

// And re-renders only update the data that changed, without VDOM diffing!
render(sayHello("Everyone"), document.body);

構文に多少の癖はあるが、基本的に template literal なので、覚えることはそう多くない。

vscode で lit-html プラグインを入れると、ハイライトされて便利。

preact を使っているが tsx(jsx) が使えない環境向けに、 lit-html の構文で preact のコンポーネントを生成してくれる htm というライブラリがある。

developit/htm: Hyperscript Tagged Markup: JSX alternative using standard tagged templates, with compiler support.

npm のフロントエンドツールチェインがない環境かつ IE を落としている環境で、自分が html のインラインでベタで ES201x を書くなら lit-html + htm かなという感じ。

ビルドを絞るテクニック: TreeShake

// 使う
export function foo() {
  //...
}

// import のみ
export function bar() {
  //...
}

// 読み込まない
export function baz() {
  //...
}
import { foo, bar } from "./xxx";

foo();

このとき、rollup の treeshake は、bar と baz の関数をビルドから消す。

ビルドを絞るテクニック: Dead Code Elimination

まず、 terser や rollup は次のコードをバンドル時に消し去る。

if (false) {
  console.log(1);
}

webpack ではビルドが production 向けかどうかで process.env.NODE_ENVdevelopmentproduction に置換する、という挙動がある。(これは uniroll でも実装している)

なので、次のコードは production ビルドで、定数置換のち定数同士が常に一致しないので、if(false){...}と同等と解釈され、消される。

if (process.env.NODE_ENV === "development") {
  console.log(1);
}

terser が解釈できそうな定数置換を意識してコードを書くことで、ビルドサイズを減らすことができる。

実際どういうコードが削られるかは、 npm install -g terser して terser foo.js -c -m して確かめてみると良い。

History
7f920a8 - Update Sun Jul 5 18:37:23 2020 +0900 
4407c3c - Update Fri Jun 26 20:23:13 2020 +0900