TL;DR

  • フロントエンドのビルドは パース → トランスパイル → モジュール解決 → バンドル → ミニファイ の5工程で構成される
  • パース はソースコードをAST(抽象構文木)に変換する
  • トランスパイル はJSX/TypeScriptをJavaScriptに変換する
  • モジュール解決 はimport文のパスを実ファイルに紐付ける
  • バンドル は複数ファイルを1つにまとめ、Tree ShakingやCode Splittingで最適化する
  • ミニファイ はコードを圧縮してファイルサイズを最小化する

はじめに

フロントエンド開発では、npm run buildnpm run dev といったコマンドを日常的に使います。 しかし、その裏側で何が行われているかを意識することは少ないかもしれません。

この記事では、フロントエンドのビルドを構成する5つの工程を解説します。 各工程が「何をしているのか」を理解することで、ビルド設定の意味が分かるようになり、トラブルシューティングもしやすくなります。

ビルド周りの改善をしていると、今どこを触っているのか見失うことがあります。そんな時に見直せるよう、ここにまとめておきます。


ビルドの全体像

フロントエンドのビルドは、以下の流れで進みます。

dist/

ビルドプロセス

src/

ソースコード

.tsx, .ts, .jsx

① パース

Parse

② トランスパイル

Transpile

③ モジュール解決

Resolve

④ バンドル

Bundle

⑤ ミニファイ

Minify

成果物

.js, .css

各工程の役割を簡単にまとめると、以下のようになります。

工程役割
パースソースコードをAST(抽象構文木)に変換
トランスパイルJSX/TypeScriptをJavaScriptに変換、古いブラウザ向けに構文を変換
モジュール解決import文のパスを実ファイルに紐付け
バンドル複数ファイルを1つにまとめる
ミニファイコードを圧縮してサイズを最小化

ここからは、各工程を詳しく見ていきます。


① パース:ソースコードをASTに変換する

パース(Parse) は、ソースコードを AST(Abstract Syntax Tree) に変換する工程です。

後続のトランスパイル・最適化・バンドルなどの処理は、文字列のまま扱うと安全に変換できません。 そのため、構造を持ったASTに変換してから処理する必要があります。


パースが何をしているか

以下のコードを例に、パースが何をしているかを説明します。

export function Hello() {
  return <div className="greet">Hi {name}</div>;
}

パースは大きく以下の2段階で構成されます。

字句解析(Lexing / Tokenization)

ソースコードを「意味のある最小単位(トークン)」に分割します。

  • export(キーワード)
  • function(キーワード)
  • Hello(識別子)
  • return(キーワード)
  • <div>(JSX構文としての開始)
  • {name}(JS式コンテナ)
  • " = { } ( ) ; など(記号)

構文解析(Parsing)

トークン列を言語仕様に沿って組み立て、ASTを生成します。


ASTのイメージ

ASTはツールごとに細部が異なりますが、概念的には以下のような木構造になります。

{
  "type": "Program",
  "body": [
    {
      "type": "ExportNamedDeclaration",
      "declaration": {
        "type": "FunctionDeclaration",
        "id": { "type": "Identifier", "name": "Hello" },
        "params": [],
        "body": {
          "type": "BlockStatement",
          "body": [
            {
              "type": "ReturnStatement",
              "argument": {
                "type": "JSXElement",
                "openingElement": {
                  "type": "JSXOpeningElement",
                  "name": { "type": "JSXIdentifier", "name": "div" },
                  "attributes": [
                    {
                      "type": "JSXAttribute",
                      "name": { "type": "JSXIdentifier", "name": "className" },
                      "value": { "type": "StringLiteral", "value": "greet" }
                    }
                  ]
                },
                "children": [
                  { "type": "JSXText", "value": "Hi " },
                  {
                    "type": "JSXExpressionContainer",
                    "expression": { "type": "Identifier", "name": "name" }
                  }
                ],
                "closingElement": {
                  "type": "JSXClosingElement",
                  "name": { "type": "JSXIdentifier", "name": "div" }
                }
              }
            }
          ]
        }
      }
    }
  ]
}

代表的なパーサー

パースを行うツールは複数存在し、ツールごとにAST表現(ノード名や構造)が異なります。 ただし「ソースコードの意味を木構造で表現する」という目的は共通です。

パーサー言語(実装)使用ツール
acornJavaScriptwebpack
@babel/parserJavaScriptBabel
TypeScript Compiler APITypeScriptts-loader
esbuild(内蔵パーサ)GoVite、esbuild
swc(内蔵パーサ)Rustswc、Next.js、rspack
oxc-parserRustOxc、Rolldown

パースのまとめ

  • パースはソースコードをASTへ変換する
  • 後続のトランスパイル・最適化・バンドルはASTを前提にしている
  • ASTはツールごとに構造が異なるが、表している意味は共通

② トランスパイル:ASTを別の表現へ変換する

トランスパイル(Transpile) は、パースによって生成したASTを別のASTへ変換する工程です。

フロントエンドのビルドでは、ブラウザがそのまま実行できない構文を、実行可能な形へ変換する目的でトランスパイルが行われます。

代表的な変換は以下です。

  • TypeScript → JavaScript
  • JSX → JavaScript
  • 新しいJavaScript構文 → 古い構文

トランスパイルが何をしているか

以下のコードを例に、トランスパイルが何をしているかを見ていきます。

export function Hello() {
  return <div className="greet">Hi {name}</div>;
}

このコードはパース後、概念的には以下のようなASTになります(簡略化しています)。

{
  "type": "ReturnStatement",
  "argument": {
    "type": "JSXElement",
    "openingElement": {
      "type": "JSXOpeningElement",
      "name": { "type": "JSXIdentifier", "name": "div" }
    }
  }
}

トランスパイルでは、この JSXElement をJavaScriptとして実行可能な構文を表すASTに変換します。


JSXトランスフォーム

JSXはJavaScriptエンジンが直接解釈できないため、ビルド時に変換する必要があります。

ここでは Automatic runtime による変換を見てみます。

// トランスパイル後
import { jsx as _jsx } from "react/jsx-runtime";

export function Hello() {
  return _jsx("div", { className: "greet", children: ["Hi ", name] });
}

変換のポイントは以下です。

  • JSXのタグ名は文字列("div")として渡される
  • JSXの属性はオブジェクトとしてまとめられる
  • 子要素は children として渡される
  • JSX構文はAST上から消え、JavaScript関数呼び出しに置き換わる

TypeScriptトランスパイル

TypeScriptの型情報は、ランタイムでは使用されないため、トランスパイル時にASTから削除されます。

// 入力
type Props = { name: string };

export function Hello(props: Props) {
  return <div>{props.name}</div>;
}
// トランスパイル後
export function Hello(props) {
  return _jsx("div", { children: props.name });
}

型注釈や型定義はAST上から完全に消える という点が重要です。

  • type Props = ... はASTに残らない
  • props: Props もASTに残らない
  • 実行に必要な構文だけを表すASTへ変換される

なぜトランスパイルが必要なのか

トランスパイルが必要になる理由は大きく3つあります。

  1. 実行環境との互換性: ブラウザの対応状況に合わせて、ASTを安全な構文表現へ変換する
  2. JSXやTypeScriptはそのままでは実行できない: JavaScriptエンジンが直接解釈できないため、変換が必須
  3. 後続の最適化を行いやすくする: Tree ShakingやMinifyは、統一されたJavaScript ASTの方が行いやすい

代表的なトランスパイラ

ツール主な用途備考
BabelJS/JSX/TSの変換pluginによる柔軟な変換が可能
TypeScript Compiler(tsc)TS → JS型チェックと相性が良い
esbuild高速なTS/JSX変換速度重視
swc高速なJS/TS/JSX変換Rust製

トランスパイルのまとめ

  • トランスパイルはASTを別のASTへ変換する工程
  • JSXやTypeScriptといった構文は、ASTレベルでJavaScript表現へ置き換えられる

③ モジュール解決:import文のパスを実ファイルに紐付ける

モジュール解決(Module Resolution) は、import文に書かれたパスを実際のファイルパスに解決する工程です。

バンドラがファイルを結合するためには、まず「このimportはどのファイルを指しているのか」を特定する必要があります。 モジュール解決は、この紐付けを行う役割を担っています。


モジュール解決が何をしているか

モジュール解決では、以下のような変換を行います。

  • パッケージ名 → node_modules内の実ファイル
  • エイリアス → 実際のファイルパス
  • 相対パス → 実ファイルパス(拡張子や index ファイルを含めて探索)
  • package.json の exports / main などの解釈

具体例

パッケージ名の解決

import { useState } from "react";

このとき "react" はファイルパスではなくパッケージ名であるため、 まず node_modules を上方向に探索して react パッケージを見つけます。

例えば、次のファイルから import している場合を考えます。

/project/src/components/Foo.tsx

この場合、次のような順番で react パッケージを探索します。

/project/src/components/node_modules/react
/project/src/node_modules/react
/project/node_modules/react
/node_modules/react

このように、現在のファイルが存在するディレクトリから親ディレクトリへ向かって、 node_modules/react が存在するかを順に確認していきます。

見つかった node_modules/react ディレクトリが、 以降の解決処理におけるパッケージのルートになります。

見つかったパッケージの package.json は次のようになっているとします。

{
  "main": "index.js",
  "exports": {
    ".": {
      "react-server": "./react.shared-subset.js",
      "default": "./index.js"
    },
    "./package.json": "./package.json",
    "./jsx-runtime": "./jsx-runtime.js",
    "./jsx-dev-runtime": "./jsx-dev-runtime.js"
  }
}

このパッケージには exports フィールドが定義されているため、main ではなくexports が優先して参照されます。

import "react" はパッケージのメインエントリを指すため、exports"." に対応する定義が評価されます。通常はdefault が選択されます。

".": {
  "react-server": "./react.shared-subset.js",
  "default": "./index.js"
}

その結果、次のファイルがエントリとして解決されます。

react
→ node_modules/react/index.js

エイリアスの解決

// 入力(tsconfig.json で paths が設定されている場合)
import { Button } from "@/components/Button";

このとき "@/components/Button" は通常のファイルパスではないため、 まずエイリアス設定を使って実際のパスに変換されます。

例えば、次のような設定があるとします。

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

この設定に基づいて、まずパスの書き換えが行われます。

@/components/Button
→ src/components/Button

エイリアスが実際のパスに変換された後は、通常の相対パスと同じルールで ファイルの解決が行われます。

src/components/Button
→ src/components/Button.tsx
→ src/components/Button/index.tsx
→ ...

その結果、例えば次のようなファイルが解決先として選ばれます。

@/components/Button
→ /src/components/Button/index.tsx

拡張子・index ファイルの解決

// 入力
import { utils } from "./utils";

このとき "./utils" は拡張子やファイル名の末尾が省略された相対パスであるため、 指定されたパスを起点に、実在するファイルを探索します。

例えば、次のファイルから import している場合を考えます。

/project/src/pages/Foo.tsx

この場合、まず次のパスが基準になります。

/project/src/pages/utils

ここを起点として、複数の候補を順に試します。

./utils
→ ./utils.ts
→ ./utils.tsx
→ ./utils.js
→ ./utils/index.ts
→ ...

このように、指定されたパスに対して拡張子を補完したり、 ディレクトリとして解釈して index ファイルを探索したりしながら、 実在するファイルを探します。

※実際に試される拡張子や探索順は、Node.js、Vite、webpack などのツールや設定によって異なります。

その結果、例えば次のようなファイルが解決されます。

./utils
→ /project/src/pages/utils.ts

解決時に参照される設定

モジュール解決では、以下の設定ファイルを参照してパスを特定します。

設定ファイルフィールド用途
package.jsonmainCommonJS エントリーポイント
package.jsonmoduleESM エントリーポイント
package.jsonexports条件付きエクスポート
tsconfig.jsonpathsパスエイリアス
tsconfig.jsonbaseUrl相対パスの基準

代表的なリゾルバー(モジュール解決のためのツール)

ツール言語使用ツール
enhanced-resolveJavaScriptwebpack
Vite内蔵リゾルバーJavaScriptVite
oxc-resolverRustOxc、Rolldown

モジュール解決のまとめ

  • モジュール解決はimport文のパスを実際のファイルに紐付ける工程
  • パッケージ名、エイリアス、相対パスなど、様々なパス形式を解決する
  • package.jsonやtsconfig.jsonの設定を参照して解決先を決定する

④ バンドル:複数ファイルを1つにまとめる

バンドル(Bundle)は、依存関係グラフをもとに、実行可能な単位(チャンク)としてファイル群を再構成する工程です。

モジュール解決によってimport先が特定されると、バンドラはエントリーポイントから依存関係をたどり、必要なすべてのファイルを結合します。


バンドルが何をしているか

バンドルでは、以下の処理を行います。

  • 依存関係グラフの構築
  • モジュールを1つの出力形式に変換してまとめる
  • Tree Shaking(副作用のない、到達不能なエクスポートの削除)
  • Code Splitting(コード分割)
  • 重複モジュールの排除

依存関係グラフの構築

バンドラはエントリーポイントからimportを再帰的にたどり、依存関係グラフを構築します。

src/index.ts

src/App.tsx

src/utils/api.ts

src/components/Header.tsx

src/components/Footer.tsx

src/hooks/useAuth.ts

node_modules/axios


Tree Shaking

Tree Shakingは、依存関係グラフの中から 実際に使用されているコードだけを抽出 し、未使用のコードをバンドルから除外する最適化です。

// utils.ts
export const usedFunction = () => "使われる";
export const unusedFunction = () => "使われない";

// index.ts
import { usedFunction } from './utils';
console.log(usedFunction());

// バンドル結果: unusedFunction は含まれない
const usedFunction = () => "使われる";
console.log(usedFunction());

ESMの静的なimport/export構文により、ビルド時に「どのexportが使われているか」を解析できるため、この最適化が可能になります。


Code Splitting

Code Splittingは、バンドルを複数のチャンクに分割し、必要なタイミングで読み込む最適化です。

// 動的インポートで分割
const HeavyComponent = lazy(() => import('./HeavyComponent'));

// 出力
// dist/
//   ├── index.js          (メインバンドル)
//   └── HeavyComponent.js (遅延ロード用チャンク)

初期ロード時に必要ないコードを分離することで、ページの読み込み速度を改善できます。


代表的なバンドラー

ツール言語特徴
webpackJavaScript最も機能豊富、Loader/Plugin豊富
RollupJavaScriptESM特化、ライブラリ向け、Viteの本番ビルド
esbuildGo超高速、機能はシンプル
ParcelJavaScriptゼロコンフィグ
rspackRustwebpack互換の高速版
RolldownRustRollup互換、Viteの将来のバンドラー
TurbopackRustNext.js向け、Vercel製

バンドルのまとめ

  • バンドルは複数ファイルを1つにまとめる工程
  • 依存関係グラフを構築し、必要なファイルをすべて結合する
  • Tree ShakingやCode Splittingによってバンドルサイズを最適化する

⑤ ミニファイ:コードを圧縮する

ミニファイ(Minify) は、コードを圧縮してファイルサイズを最小化する工程です。

バンドルによってファイルがまとめられた後、ミニファイによって不要な空白や改行を削除し、変数名を短縮することで、最終的なファイルサイズを削減します。


ミニファイが何をしているか

ミニファイでは、以下の処理を行います。

  • 空白・改行の削除
  • 変数名の短縮(Mangling)
  • 不要なコードの削除
  • コードの最適化

具体例

基本的な圧縮

// 入力
function calculateTotal(items) {
  let total = 0;
  for (const item of items) {
    total += item.price * item.quantity;
  }
  return total;
}

// 出力(ミニファイ後)
function calculateTotal(t){let e=0;for(const l of t)e+=l.price*l.quantity;return e}

変数名の短縮(Mangling)

// 入力
const userSettings = { theme: 'dark', language: 'ja' };
const notificationPreferences = { email: true, push: false };

// 出力
const a={theme:'dark',language:'ja'};const b={email:!0,push:!1};

ローカル変数やプライベートな変数は、短い名前に置き換えても動作に影響しないため、積極的に短縮されます。

コード最適化

// 入力
if (true) {
  console.log("always");
}
if (false) {
  console.log("never");
}
const result = 1 + 2 + 3;

// 出力
console.log("always");const result=6;

定数式の事前計算や、到達不能コードの削除も行われます。

Boolean最適化

// 入力
const isActive = true;
const isDisabled = false;

// 出力
const isActive=!0;const isDisabled=!1;

true!0false!1 に置き換えることで、1バイトずつ削減できます。


代表的なミニファイア

ツール言語特徴
TerserJavaScriptuglify-jsの後継、最も使われている
esbuildGo高速、Viteのデフォルト
swc minifyRustswcに内蔵
oxc-minifierRustOxcの一部、開発中

ミニファイのまとめ

  • ミニファイはコードを圧縮してファイルサイズを最小化する工程
  • 空白削除、変数名短縮、コード最適化などを行う
  • 人間が読みやすい形式から、実行に最適化された形式へ変換する
  • この工程を経て、最終的なdist/へ出力される

おわりに

この記事では、フロントエンドのビルドを構成する5つの工程を解説しました。

  1. パース - ソースコードをASTに変換
  2. トランスパイル - JSX/TypeScriptをJavaScriptに変換
  3. モジュール解決 - import文のパスを実ファイルに紐付け
  4. バンドル - 複数ファイルを1つにまとめる
  5. ミニファイ - コードを圧縮する

普段使っているビルドツールが「何をしているのか」を理解することで、設定の意味が分かるようになったり、ビルドエラーの原因を特定しやすくなります。

各工程を担うツールは日々進化していますが、ツールは変わってもビルドの基本的な構造は変わりません。