Build and Publish 2023

@mizchi | Plaid, Inc.

jsconf.jp 2023

今日もツールチェインに消耗してますか?

https://2022.stateofjs.com/en-US/libraries

今日のテーマ

  • 今日話すこと
    • 現代のビルドツールチェインの進化と目的
    • エコシステムに振り回されないようになろう!
  • 今日話さないこと
    • 個別のツール/ライブラリの使い方
  • フロントエンドの話?
    • NO. 最近のサーバーサイドJSとバンドラは一体化している
    • サイズ制限のある CDN Edge Worker も最適化が必要

About Me

Alt text

JSのビルドとは何か

  • Transpile
    • AltJS(TS,JSX)や CSS Modules の変換
  • Bundle
    • 静的解析によるファイルの結合
    • 動的チャンク分割
  • Minify
    • 空白除去やローカル変数のリネーム(Mangle)
  • これらを管理するプリプロセスパイプラインの集合体

本当に必要だったビルド処理とは?

  • 思考停止でビルドツールに投げ込んでいませんか
  • ランタイム仕様を再考し、Vite や Next.js について考えたい

Runtime 2023

<script src="jquery.js">
</script>
<script src="foo.js">
</script>
<script src="main.js">
</script>

this.App = typeof App == "object"
  ? App : {};
// foo.js
App.Foo = {
  render: function() {
    $(".app").html("<p>foo</p>");
  }
}
// main.js
App.Foo.render();

バンドラ以前

  • Frontend (~ES5)
    • モジュールシステムがない
    • パッケージマネージャがない
    • スクリプト間の依存でブロッキングを要求
  • Node.js (2009~)
    • V8 + CommonJS

Node.js CommonJS

  • Node.js で採用されたモジュールシステム
  • 構文ではなく 関数ユーティリティ
  • ESMと比べて静的解析に優しくない
    • exports.* の同期/動的評価に依存
// lib.js
exports.foo = () => {/*...*/};
// main.js
const lib = require('./lib');
lib.foo();

// Always dynamic
exports[Math.random()] = true;
// sub.js
export const x = 1;
export const y = 2;
export default 3;

// main.js
import sub, { x, y } from "./sub.js";
await import("./dyn.js");

ES Modules (ES2015)

  • ファイルスコープの導入
  • シンボル解決が決まっているだけ
  • インポート先をどのようにロードするかは実行環境依存
    • Node.js: FS
    • Frontend: HTTP Request

ESMの問題: RTT


// entry (html)
<script 
  type="module"
  src="./main.js"
></script>
  // main.js: 1 RTT
  import { o } from './sub.js';
    // sub.js: 2 RTT
    export { o } from './nest.js'
      // nest.js: 3 RTT
      export const o = 1;
  • N層目の構文解析するまで N+1 の依存が判明しない
  • IEがサポートしないのもあって積極利用されなかった

2023 年ならRTT問題を解決できるか?

modulepreload

<!-- header -->
<link
  rel="modulepreload"
  href="main.js"
>

<!-- body -->
<script
  type="module"
  src="main.js"
></script>
  • 対象モジュールを再帰的に先読み
  • 優先的な先読み開始の指示に過ぎない
    • 深さ問題は解決しない
    • Chrome’s Loading Performance with (Many) Modules
    • モジュール数 > 100 or 深さ > 5 ならバンドルしたほうがいい
    • モジュール内依存を考えると、単一ファイルのライブラリをピックする必要がある

HTTP 103 Early Hints

Content-Type: text/javascript
Link: </sub.js>; rel=preload
Link: </nest.js>; rel=preload
  • HTTP Header で先読みを指示
  • Pros
    • 仮想ストリーム内で RTT を排除
  • Cons
    • RTT を減らすには 事前に依存解析が必要
    • バンドルと同等のプリプロセスが必要なら、バンドルでよくね問題
<script type="importmap">
  {
    "imports": {
      "foo": "/script/foo.js"
    }
  }
</script>

<script type="module">
  import foo from "foo";
</script>

ImportMaps

module countModule {
  let i = 0;
  export function count() {
    i++;
    return i;
  }
}
import { count } from countModule;

TC39 Proposal: Module Declaration

  • ファイルスコープ内で module を宣言 (Stage 2)
  • 単純なコード変形でバンドル相当に
  • TS module (非標準) と排他な構文...
import {
  createElement as h
} from "https://esm.sh/react";
import {
  renderToString
} from "https://esm.sh/react-dom/server";
renderToString(
  // <p>Hello</p>
  h("p", {}, "Hello")
);
//=> <p>Hello</p>

HTTP Imports

Runtime 2023: まとめ

  • 先読み関連の仕様が増えたが, RTT の問題は解決していない
  • Node/Deno/ブラウザで同じESMのコードが動く土壌が整ってきた

Toolchains

Closure Compiler (2009)

// goog.module
goog.module('foo');
const Quux = goog.require('baz.Quux');
exports.Bar = function() { /* … */ };

/**
 * Enum
 * @enum {number}
 */
project.Constants = {
  A: 1,
  B: -1,
  C: 0
};

browserify (2011)

  • ブラウザ向けにCJSの実行を模倣するエミュレータ
  • 導入が簡単 $ browserify input.js -o out.js
  • フロントエンドとnpmエコシステムを接続

webpack (2014)

  • CJS のバンドル + チャンク分割
  • カスタムローダーによるCSS対応で、パイプラインがワンストップ化
  • Reactのビルドツールとして普及した?
    • JSXのついでに全部やってしまえ
// src/main.ts
import "./style.css";
(async function run() {
  await import("./sub");
})();
/* Output
src
├── index.ts
├── style.css
└── sub.ts
dist
├── main.js
└── 370.js # generated chunk
*/

webpack の普及と課題

  • 良いデフォルト設定の不在
    • webpack.config.js によって動作が完全に別物
  • CPU/FileIO への負荷が激しい
    • グラフ構築にインメモリに全部読み込む
    • とくに Docker on Mac 上で低速化
  • ESM 対応が後付
  • 多段階ビルドがあまりにも難しい: Module Federation

Webpack 以降の世界

Rollup

// sub.js
export const sub = 1;
export const unused = 2;

// main.js
import {sub, unused} from "./sub.js";
console.log(sub);

// $ rollup -i main.js
const sub = 1;
console.log(sub);
  • ESM ファーストなバンドラ
  • ESM 出力をサポート
  • TreeShake による DCE
    • minifier の一部の処理も兼ねるように
    • #__PURE__
    • #__NO_SIDE_EFFECTS__

Vite

  • index.html を入力とするバンドラ
  • Zero-Config: よく練られたデフォルト設定
    • ts(x) や CSS Module, チャンク自動最適
    • Webpack 運用のベストプラクティスを集約した感じ
  • No Bundle (Development)
  • 実体は esbuild(transpile, minify) と
  • SSR Support

Vite: No Bundle

  • 開発時はバンドルしない
  • 実行にはブラウザ標準の ESM を使う
    • localhost にネットワーク遅延はない

Vite: Pros/Cons

  • Pros
    • 開発環境での再ビルドが高速/低負荷
  • Cons
    • development と production の差異が大きい

https://2022.stateofjs.com/en-US/libraries/build-tools/

# https://github.com/mizchi/monorepo
$ turbo build # 1st
 Tasks:    8 successful, 8 total
Cached:    0 cached, 8 total
  Time:    2.808s 

$ turbo build # 2nd
 Tasks:    8 successful, 8 total
Cached:    8 cached, 8 total
  Time:    68ms >>> FULL TURBO

$ turbo build --graph # emit graph

Turborepo

  • https://github.com/vercel/turbo
  • ワークスペース環境のビルド最適化
  • 依存のトポロジカルソートでビルド順とスキップ有無を決定

Toolchains: まとめ

  • パフォーマンス要求の複雑さに応じて出力が高度化している
    • 前提を重ねているので経緯を知らないと出力が意味不明
    • Zero-Config で設定を減らす方向性
  • 依然 Webpack のシェアは大きいが Vite の躍進がすごい
    • Turbopack Next.js の Webpack を置き換える実験的コンパイラ
    • Rspack

SSR Frameworks

$ tree src
src/
└── app
    ├── favicon.ico
    ├── globals.css
    ├── layout.tsx
    ├── page.module.css
    ├── page.tsx
    └── sub
        └── page.tsx
$ next build
+ First Load JS shared by all            83.9 kB
  ├ chunks/331-8de3f1403fdec9cb.js       28.7 kB
  ├ chunks/4ae7487b-c3bb4f474d997312.js  53.3 kB
  ├ chunks/main-app-400cf913b3975db6.js  217 B
  └ chunks/webpack-e3c6517d4ab8d680.js   1.68 kB

Next.js

  • SSR/CSR の Hybrid フレームワーク
  • Client/Server のコードを同時に最適化する(一昔のIsormorphic)
  • 現代の要求に見合う API とセマンティクスを発明した

Next.js の発明: Filesystem Routing

  • URL とファイルパスに規約を導入
  • 対応するファイルの default = その画面のルート要素
  • サーバー用のSSRコードとクライアントHydrationコードを同時に生成
/* Pages Router Example */

// pages/post/[slug].tsx
// => /post/*
export default () => {
  return <div>Hello</div>
}

Next.js の発明: DataLoader

  • 関心の限定: Client/Server間で受け渡す props 初期化だけを記述
  • SSR/CSRの初期化と再レンダリングの分岐で複雑だった部分を隠蔽
export const getServerSideProps =
async () => {
  const res = await fetch('...')
  const data = await res.json()
  return { props: { data } }
};
 
export default function Page(props) {
  return <>...</>
}

React Server Component (RSC)

// src/app/sub/page.tsx
import {transform} from "@babel/core";

async function loadData() {
  return 'export const x = 1';
}
// async component
async function Compiled() {
  const data = await loadData();
  // サーバーのみの処理
  const result = transform(data).code;
  return <pre>{result}</pre>
};
export default () => <Compiled />;

Vite SSR Frameworks

// Counter Component
import {
  component$,
  useStore
} from "@builder.io/qwik";
const Counter =  component$(() => {
  const store = useStore({ count: 0 });
  return <button type="button"
    onClick$={() => store.count++}>
    {store.count}
  </button>;
});

// output(html)
<button type="button"
  on:click="q-dbdde6e9.js#s_ccRkpRA0Ias[0]"
  q:key="H1_0" q:id="8">0
</button>

極端な実例: Qwik

  • Qwik: SSR前提でクライアント処理を最小限にしたUIライブラリ
  • QwikCity: Qwik の Next 風フレームワーク
  • TSX ベースの React 方言

Qwik City Example

  • 初期状態でSSRのみ
  • クリックしてからHydrationを開始(選択的 Hydration)

QwikCity はロマンの塊

SSR Frameworks: まとめ

  • フロントエンド最適化がサーバーサイドに含めたものに
    • フロントエンド?の専門化が深まっている
  • Next.js が API 設計のオピニオンリーダー
  • Vite SSR ベースのフレームワークが多く出ている
  • 今の SSR/RSC はサーバーサイドにチャンクを払い出す最適化技術の一種

Optimizer (の話をしたかった)

統括

複雑性 vs メリットの狭間で考えること

  • ある複雑さを管理するのに、どれだけ新しい概念を持ち込むか?
  • そのツール/ライブラリが提供する機能は自分に必要なものか?
  • 複雑なエコシステムの中でメリットを引き出し続けられるか?

自分の結論

  • 今後のビルドツールチェインの争点
    • Next以外の React 系の RSC 導入時に混乱が予想される
    • React 以外のUIライブラリで Server Component 概念の輸入
  • 常に崩壊する Zero-Config
    • 本質的に前世代のベストプラクティス集に過ぎない
    • 本当に Zero-Config だけではトレンドからは一歩遅れる
  • 自分が本当に必要なものは手を動かさないとわからない
    • 日頃のペインポイントを解消するのが本当に必要なもの
    • とにかく手を動かして審美眼を高めよう!!!

おわり

ふわっとしたタイトルをつけてしまったので、内容もややふわふわしています

![bg fit](../images/image.png)

Python に似ている

おそらく preload を列挙すること自体のテキスト量が大きい

E2E で機械学習で先読みするのも過剰

Rails 7 ではデフォルト

もし free-wifi に乗っ取られて悪意あるリクエストを流し込まれたら?

ImportMaps と組み合わせて似た書き味に

Java製。オーパーツ感ある

実行時のFSをスナップショットと見立てる

これ以前は grunt gulp などを組み合わせてビルドパイプラインを構築していました

一応 `experiments.outputLibrary: module` は存在するが... Module Federation は中間チャンクをつなぎ合わせるものなんですが、今考えと ESM と Treeshake があれば、必要性はそう高くありません。

バンドラというよりその一個上の水準

リリース時点以外でしか Zero Config でいられないです。 そもそも理解せずに使うのも選択肢ではあります

App Router は説明が難しい

中身の解説をしようと思ったが、見るたびに実装が変わってて困難

- 過激な Svelte + Astro