🐶

# Rollup の使い方: 入門編 ~ESM と Tree-Shaking~

2020/09/21に公開

この記事は次のようなシリーズものの導入部になります。

  • Rollup の使い方: 入門編 ~ESM と Tree-Shaking~ (← 本稿)
  • Rollup の使い方: 実用編 ~目的別テンプレートを添えて~
  • Rollup の使い方: プラグイン開発編 ~module-name を制するは JS 開発を制する~

この記事では次のような内容を紹介します。

  • Rollup とは何か
  • ESM と Tree-Shaking
  • 初めての rollup.config.js
  • バンドルコードを覗いて知るバンドラーの挙動

Rollup とは何か

Rollup とは JS のモジュールバンドラーです。小さなコードの塊を、大きなバンドルファイルへと変換するためにライブラリやアプリケーションで用いられるツールです。
Rollup には ES モジュールという新しい標準規格をネイティブに採用しているという特徴があります。

ESM と Tree-Shaking

元来、JavaScript には 他のファイルの機能を利用するための構文 は用意されていませんでした。これは ES6 バージョンで import/export というシンタックスで導入されましたが、当時は最新のブラウザーでのみ対応されており、Node.js では確定していませんでした。(Node.js でも現在は Stability: 1 ですが条件を満たせば ESM での実行も可能です)

Rollup では最新の ESM 形式で記述されたコードを、既存の CommonJS や AMD, IIFE などのスタイルに変換することが可能です。これは開発者視点では ESM にのみ関心を払えばよい という事になります。

最新仕様である ESM には Tree-Shaking という大きな利点があります。

Tree-Shaking は Dead Code Elimination の文脈で利用されます。主にバンドルサイズの削減のためにインポートステートメントから未使用の関数を判断して削除することが可能です。

JavaScript のような動的言語の性質上、ステートメントレベルまたはファイルレベルで未使用のコードブロックを見つけることは困難でした。また CommonJS の性質として、グローバルスコープに影響を与える可能性のあるアプリケーションのフローをランタイムで動的に変更することが可能です。

CommonJS においても Tree-Shaking は適用可能ですが、上記のような理由があるため ESM よりも未使用のコードブロックを見つけることは ESM よりも難しくなるため、Tree-Shaking は ES6 モジュールほど効果的ではありません。

ESM の Tree-Shaking が優れている理由として、ESM のインポートとエクスポートは静的であり、条件付きでファイルをインポートすることはできないということが挙げられます。

// CommonJS
if (42) {
  const rollup = require('rollup');
  console.log(rollup);
}
// ESM
if (42) {
  import { rollup } from 'rollup';
  console.log(rollup);
}

上記のコードブロックでは、CommonJS での実装(require)は有効ですが、ES6 での実装(import)はエラーをスローします。これは reuiqre が単純な関数であることと、import/export が静的に実行されることによる振る舞いの差異です。CommonJS での振る舞いのように、動的に読み込まれうるファイルがコード内部で使用されているかどうかを決定づけることは非常に困難であることがわかります。ESM では 動的な読み込み が禁止されているため、Tree-Shaking が有効に働きます。

この Tree-Shaking を初めて実装したバンドラーが Rollup であり、後にインスパイアされ Webpack などにも導入されるようになりました。

Rollup は ESM の使用を可能にするだけでなく、インポートするコードを静的に分析し、実際に使用されていないものはすべて除外します。

初めての rollup.config.js

Rollup でビルドを組む場合、rollup.config.js というファイルを起点にビルドを行います。公式 のサンプル通りにまずはやってみます。

npm i -D rollup
mkdir src && touch src/main.js src/mod.js
code rollup.config.js
// rollup.config.js
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs',
  },
};
// src/main.js
import { mod } from './mod.js';
mod();

// src/mod.js
export function mod() {
  console.log(42);
}

この状態で、npx rollup -c とコマンドを叩くと PJ ルートに bundle.js というファイルが作成されます。これが Rollup におけるバンドルの最小構成で、ここから目的に応じて オプション を追加していきます。Rollup には膨大なオプションが存在しているため、目的/機能に応じた非常に高い拡張性を持っています。

バンドルコードを覗いて知るバンドラーの挙動

次にバンドルコードの中身を見ていきます。

先ほど生成された bundle.js の中身を覗いてみると、次のようになっていると思います。

'use strict';

function mod() {
  console.log(42);
}

mod();

2 つのモジュールとして物理的に分割されていたものたちが、1 つのモジュールとして同一コンテクスト上に宣言されているのが分かります。こうして別々に宣言されていたはずのモジュール間を単一ファイル上で相互に連携可能にするのがバンドラーとしての責務になります。

ここでモジュールスコープにおいて同名の変数宣言を行ってみます。

// src/main.js
import { mod } from './mod.js';
const same = 42;
mod();
console.log(same);

// src/mod.js
const same = 42;
export function mod() {
  console.log(same);
  console.log(42);
}

するとバンドルコードは、

'use strict';

const same = 42;
function mod() {
  console.log(same);
  console.log(42);
}

const same$1 = 42;
mod();
console.log(same$1);

このようになります。

モジュールスコープで宣言した変数についてはグローバルスコープに漏れることがあってはなりません。これを単一ファイル上でどのように実現しているかというと、挙動はシンプルで同名の変数があった場合は 特殊文字 + 数値 としてユニークになるよう扱っているため他のモジュールスコープの変数とコンフリクトすることを避けることができます。

次に Tree-shaking の例を見てみましょう。次のように修正してみます。

// src/main.js
import { mod, mod2 } from './mod.js';
mod();

// src/mod.js
export function mod() {
  console.log(42);
}

export function mod2() {
  console.log(42);
}

上記のコードでは mod2 を export/improt していますが、main.js 側では値として利用していません。この状態のバンドルコードは次のようになります。

'use strict';

function mod() {
  console.log(42);
}

mod();

mod2 の利用はもちろんのこと、関数宣言すらされていません。これが Dead Code Elimination の振る舞いであり、人間が関心を払わずともバンドルコードの削減に大きく貢献してくれます。

おわり

おわりです。

私は仕事でもプライベートでも常に JS/Web プロジェクトでは Rollup を使っています。理由としては

  • 次世代標準規格である ESM のネイティブサポート
  • オプションの豊富さ/粒度
  • 適切なドキュメンテーション
  • プラグイン開発のしやすさ(比較的安全に魔改造出来る)
  • 内部コードの読みやすや/拡張のしやすさ

が Rollup は優れていると感じているからです。

特に Web 開発では HTML/CSS/JS の三者が入り乱れ、複雑なアウトプットを求められるので、突き詰めていくとバンドラーのポテンシャルは非常に重要となってきます。

Using Native JavaScript Modules in Production Today

次回は実際の現場で利用されるような複雑なビルド、プラグインを使った 3rd-party ツールとのインテグレーションなどをご紹介する予定です。

Discussion