JavaScriptにおけるビルドの仕組みを考える
はじめに
これまでビルドについては、『トランスパイルしてバンドルして〜』と言ったように何となく雰囲気で捉えていました。しかし、設定ファイルを修正するにあたり理解が足りない箇所があったため、ビルドプロセス全体について調べてみました。今回は『具体的にビルドは何をしてくれるの?』と言った疑問を解消することを目的に記事を書いてみます。
ビルドとは
そもそも”ビルド”とはどのようなプロセスを表しているのでしょうか?
JavaScriptにおけるビルドプロセスは、開発中のコードを本番環境で効率よく実行できる形に「変換」するための一連のステップを指します。このプロセスには、コードの最適化や変換、静的アセットの管理が含まれます。つまり、開発中のコードを本番環境で効率よく実行するための変換処理と実行可能なアプリケーションを生成するプロセス全体を総称して”ビルド”と呼びます。
一言にビルドと言っても以下の通り複数の処理を行なっています。主要な処理に関しては後述していきます。
-
クリーンアップ
- ビルド前の残存ファイルを削除して、クリーンなビルド環境を準備します。
-
依存関係のインストール
- プロジェクトの依存関係をインストールします。
-
トランスパイル(Transpile)
- TypeScriptや新しいバージョンのJavaScript(ES6+)を古いバージョンのJavaScript(ES5)に変換します。
-
バンドル(Bundle)
- 複数のJavaScriptファイルやモジュールを1つのファイルに結合し、依存関係を解決します。
-
最適化(Optimize)
- コードを圧縮(ミニファイ)し、不要なコードを削除(ツリーシェイキング)して、最小限のサイズにします。
-
アセットの管理(Manage Assets)
- CSS、画像、フォントなどの静的アセットを処理し、最適化します。
-
ソースマップの生成(Generate Source Maps)
- デバッグ時に元のソースコードをトレースするためのソースマップを生成します。
-
ファイルのコピーと移動(Copy and Move Files)
- ビルドされたファイルを指定されたディレクトリにコピーまたは移動します。
-
テスト(Test)
- ビルドされたコードが意図した通りに動作するかをテストします。
-
デプロイ(Deploy)
- ビルドされたファイルを本番環境にデプロイします。
1. コードの変換 (Transpiling)
コードの変換 (Transpiling)とは
Transpiling(トランスパイリング) は、JavaScriptのコードをあるバージョンから別のバージョンに変換するプロセスを指しています。通常、これは新しいバージョンのJavaScript(ES6+)を古いバージョン(ES5)に変換するために行われます。例えばInternet Explorer(IE)では、ES6+のコードが機能しないため、ES5にトランスパイルする必要があります。最新の言語機能を使用しつつ、古いブラウザや実行環境でもコードが動作するようにするためには必要な処理になります。トランスパイルの中にも幾つか種類が存在します。
1-1. ES6+からES5への変換
一つは前述した通り、最新のJavaScriptコードを古いブラウザでも実行できるES5に変換する処理になります。アロー関数はES6の機能であり、ES5の環境では動作しないため、ES5の形式に変換することで、古いブラウザでも動作するようになります。他にもclass, テンプレートリテラルなどもトランスパイルが必要になります。
// アロー関数
// ES6+
const greet = () => console.log('Hello, World!');
// ES5
var greet = function() {
console.log('Hello, World!');
};
// class
// ES6+
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
const john = new Person('John');
console.log(john.greet());
// ES5
var Person = function Person(name) {
this.name = name;
};
Person.prototype.greet = function() {
return "Hello, my name is " + this.name;
};
var john = new Person('John');
console.log(john.greet());
// テンプレートリテラル
// ES6+
const name = 'World';
const greeting = `Hello, ${name}!`;
console.log(greeting);
// ES5
var name = 'World';
var greeting = 'Hello, ' + name + '!';
console.log(greeting);
以下にES6+とES5の差分やES6のアップデートについて詳しく言及されています。
1-2. TypeScriptからJavaScriptへの変換
TypeScriptは、ブラウザやNode.jsなどのJavaScriptの実行環境で直接動かすことができません。これは、TypeScriptがJavaScriptに加えて独自の型システムや構文を持っているからです。そのため、TypeScriptのコードをJavaScriptに変換する必要があります。
トランスパイルとコンパイル
TypeScriptコードを利用している場合、以下のようなプロセスを経て実行可能な形へ変換しています。
トランスパイル
- TypeScript (TS) -> JavaScript (JS): トランスパイルは、一つの高水準言語から別の高水準言語に変換するプロセスです。トランスパイラは、プログラムのソースコードを別のソースコード形式に変換します。TypeScriptはJavaScriptのスーパーセットであり、型注釈やクラスなどの追加機能があるため、これらの追加機能をJavaScriptに変換して、JavaScriptランタイム環境で実行できるようにすることです。
コンパイル
- JavaScript (JS) -> Binary Data: コンパイルは、ソースコード(通常は高水準言語)を機械語(バイナリコード)や中間言語(中間コード)に変換するプロセスです。最終的な出力は、コンピュータが直接実行できる形式です。例えば、JavaScriptがブラウザで実行されるときは、JavaScriptエンジン(例: V8エンジン)がJavaScriptコードをコンパイルして、バイナリ形式の中間コードに変換し、その後実行します。
イメージ図
2. モジュールのバンドリング (Bundling)
モジュールバンドリングとは、複数のJavaScriptファイル(モジュール)を1つまたは複数のファイルにまとめるプロセスです。
// バンドル前
<head>
<script type="text/javascript" src="/navbar.js"></script>
<script type="text/javascript" src="/sidebar.js"></script>
<script type="text/javascript" src="/some-modal.js"></script>
<script type="text/javascript" src="/footer.js"></script>
</head>
// バンドル後
<head>
<script type="text/javascript" src="/compressed-bundle.js"></script>
</head>
イメージ図
なぜバンドルが必要なのか?
複数のJavaScriptファイルを1つのファイルにまとめることで、HTTPリクエストの数を減らし、ページのロード時間を短縮することができます。WebpackやRollupなどのバンドラーが使用されます。
バンドルツール
代表的なバンドラーにはWebpack、Rollup、Parcelがあります。
-
Webpack:
- 最も広く使用されているモジュールバンドラーで、豊富なプラグインエコシステムと高度な設定が可能です。
-
Rollup:
- 主にライブラリのバンドリングに使用される軽量のバンドラーで、ツリーシェイキング機能が強力です。
-
Parcel:
- 設定なしで使えるシンプルなバンドラーで、迅速な開発をサポートします。
バンドルツールの比較については以下の記事で言及されています。
また、各バンドラーのリンクも併せて添付しておきます。
3. コードの圧縮 (Minification)
Minificationとはコードを圧縮してファイルサイズを小さくするプロセスを指します。不要なスペースやコメントを削除し、変数名を短縮します。Terserなどのツールが使用されます。
以下、Kinstaのブログより抜粋しました。
コードの圧縮は、容量を節約し、ページの表示時間を短縮し、ウェブサイトの帯域幅の使用料を減らすことにつながります。ここで大事なのは、機能を変更することなくコードを圧縮することです。
コードの圧縮例
// Minification前
//文字列が回文であるかどうかを調べるプログラム
function checkPalindrome(str) {
//文字列の長さを求める
const len = string.length;
//文字列の半分をループする
for (let i = 0; i < len / 2; i++) {
//最初と最後の文字列が同じかどうかをチェックする
if (string[i] !== string[len - 1 - i]) {
return 'これは回文ではありません';
}
}
return 'これは回文です';
}
//インプットを受け付ける
const string = prompt('文字列を入力してください');
//関数を呼び出す
const value = checkPalindrome(string);
// Minification後
function checkPalindrome(n){const t=string.length;for(let n=0;n<t/2;n++)if(string[n]!==string[t-1-n])return"It is not a palindrome";return"It is a palindrome"}const string=prompt("Enter a string: "), value=checkPalindrome(string);console.log(value);
この例では、529バイトを324バイトに減らすことで、205バイトの空き領域が手に入り、ページの読み込みにかかる時間をほぼ40%削減することができました。529ページある本を、324ページに凝縮したようなものです。もちろん、人間が読むには苦労しますが、機械なら問題なく読み進められるでしょう。
また、複数のJavaScriptファイルを連結して1つのファイルとして圧縮すると、サーバーへのHTTPリクエストの数が減少し、サイトの帯域幅の消費も抑えることができます。さらに、コードの圧縮によってスクリプトの実行時間が短縮され、Time to First Byte (TTFB)が低下します。
コード圧縮によるメリット
JavaScriptコードの圧縮により得られるメリットは以下の通りです。
- ページ表示時間の短縮
- ウェブサイトの帯域幅消費量の削減
- スクリプト実行時間の短縮
- サーバーへのHTTPリクエストの減少(およびサーバーへの負荷の軽減)
- 盗難防止(圧縮したバージョンは、読み取り再利用するのが困難)
4. デッドコードの削除 (Tree Shaking)
Tree Shakingとは使用されていないコードを自動的に削除するプロセスを指します。これにより、最終的なバンドルサイズが小さくなり、パフォーマンスが向上します。厳密にはTree shakingはバンドルプロセスの中で実行されるため、Webpack, Rollupなどが実行してくれています。
// Before Tree Shaking
import { a, b, c } from './modules';
a();
c();
// After Tree Shaking
import { a, c } from './modules';
a();
c();
5. 静的アセットの管理
画像、CSS、フォントなどの静的アセットを効率的に管理し、最適化します。これには、画像の圧縮やCSSのミニファイ、フォントの最適化が含まれます。
6. 環境設定
ビルドプロセスでは、開発環境と本番環境で異なる設定を使用することが一般的です。環境変数を使用して、環境ごとの設定を管理します。
// Webpackの例
module.exports = (env) => ({
mode: env.production ? 'production' : 'development',
// その他の設定
});
7. ソースマップの生成(Generate Source Maps)
ソースマップ(Source Map) とは、ミニファイ(圧縮)されたコードと元のソースコードとの対応関係を示すファイルです。これにより、開発者はミニファイされたコードではなく、元のソースコードを参照してデバッグやトラブルシューティングを行うことができます。
例えば本番環境で障害が発生した時、コンソールにはエラーが出ていますが、どのファイルでエラーが発生しているの分からないケースがあると思います(例:main.bd5cc2a6.js)。
身に覚えがないファイル名でエラーが表示されるのは、圧縮もしくはコンパイルされたファイル名で表示されているのが原因となります。
ソースマップを利用することで、圧縮前のソースコードと比較することが可能となり、以下のように圧縮前のファイル名でエラー箇所を特定することができます。
まとめ
JavaScriptのビルドプロセスは、開発中のコードを最適化し、本番環境で効率よく実行できる形に変換するための重要なステップです。これには、トランスパイル、バンドリング、ミニファイ、ツリーシェイキング、静的アセットの管理などが含まれます。Webpack、Rollup、Viteなどのビルドツールを使用して、これらのプロセスを効率的に管理できます。
これらの知識は設定ファイルの修正やエラーハンドリング時に役立つと思うので、今後も調べていこうかと思います。この記事を通じて、少しでも学びがあれば嬉しいです!
Discussion