ビルドツールなしでJSを分割するのはあり?実際に検証してみた
本記事ではViteやWebpackなどのビルドツールを使用しない静的サイトにおいて、JavaScriptを分割する方法と、そのメリットとデメリットについて考察しています。
個人で受託している案件のうち、LPや小規模サイトなどのシンプルなものについては、ビルドツールを使わず静的サイトとして実装することが多いです。そのようなケースにおいてJavaScriptを分割すると、パフォーマンスを中心にどのような影響があるのか気になったので検証してみました。
ES Modulesで関数を分割する方法
ES Modulesの機能の一つであるimportとexportを使うと、関数をファイル毎に分割することが可能です。
関数をexportする
今回はhello.jsというファイルを作成して、helloという関数を書き出します。関数宣言の前にexportを付けることで、その関数を外部ファイルから読み込めるようになります。
export const hello = () => { console.log('hello');}関数をimportする
上記の書き出した関数を、別ファイルで参照します。importを使うことで、外部ファイルから書き出した関数を読み込み、その関数を実行することができます。
import { hello } from './utility/hello.js';
hello();注意点としては、関数をインポートして実行するmain.jsは、次のようにtype="module"を付けて読み込む必要があります。type="module"をつけることでES Modulesを有効化し、ファイルの読み込みができるようになります。
<script type="module" src="js/main.js"></script>以上がJavaScriptの関数を分割する最もシンプルな方法です。
Web制作でよく使う下記の処理などを機能ごとに分割しておくと、制作のスピードアップにつながることが想像できます。
- ハンバーガーメニュー
- スムーススクロール
- トップへ戻るボタン
- モーダル
- アコーディオン
import { hamburgerMenu } from './components/hamburgerMenu.js';import { smoothScroll } from './components/smoothScroll.js';import { topButton } from './components/topButton.js';import { modal } from './components/modal.js';import { accordion } from './components/accordion.js';
hamburgerMenu();smoothScroll();topButton();modal();accordion();JavaScriptを分割した場合のメリットとデメリット
とても便利そうな機能ですが、Web制作の現場でのメリットとデメリットについて考えてみます。
冒頭でも触れた通り、あくまでもビルドツールを使用しない環境におけるメリットとデメリットを想定しています。ビルドツールありの場合では、それぞれのメリットとデメリットは異なる可能性があります。
メリット
主に次のようなメリットが考えられます。
- 開発体験・可読性の向上:
script.jsを見るだけで何を初期化しているか一目でわかります。component/やutility/でUIとロジックを分離することで「どこに何を書くか」のルールが生まれ、コードの置き場所で迷いにくくなります。 - 保守性・再利用性の向上:ハンバーガーメニューを修正したければ
hamburgermenu.jsだけを触れば良く、影響範囲が限定されます。smoothscroll.jsのようなユーティリティは別ページや別プロジェクトからもimportして使い回せます。
デメリット
一方で次のようなデメリットも考えられます。
- 通信コストが増える:ファイルを分割しただけ、リクエスト数が増加します。HTTP/2以上の環境では並列取得できるため影響は小さいですが、HTTP/1.1環境では表示速度の低下につながる可能性があります。
- ブラウザの互換性:ES Modules非対応のブラウザでは動作しません。現代的なブラウザのみをターゲットにできる環境が前提となります。
両者の比較
分割した場合と分割しない場合を比較すると次のようになります。
| 項目 | JS分割あり | JS分割なし |
|---|---|---|
| 開発体験・可読性 | ◎ 明確に向上する | △ ファイルが肥大化しやすい |
| 保守性・再利用性 | ◎ ファイルが役割ごとに独立し、再利用しやすい | △ 変更時に全体へ影響する可能性がある |
| 通信コスト | △ リクエスト数が増える(HTTP/2なら影響軽減) | ◎ 1ファイルでリクエストが少ない |
| ブラウザ互換性 | △ モダンブラウザ限定になる | ◎ 幅広いブラウザに対応 |
ファイルを分割した場合、開発体験や保守性は上がる一方、通信コストやブラウザの互換性は下がることがわかります。
ブラウザの互換性については、type="module"のグローバル普及率は97%以上(2026年2月時点)のため、現在は互換性の問題はほとんどないでしょう。主要ブラウザではほぼ対応しており、非対応なのは実質IE11等のレガシーブラウザのみで、現在の開発においてこれらをサポート対象にするケースはあまりないと思います。
一方で通信コストについては実際にどの程度パフォーマンスに影響があるのか疑問に思い、簡易的に検証してみました。
パフォーマンスへの影響を実際に検証してみた
次の条件で検証してみました。
- ブラウザ:Chrome 147
- デプロイ先:Cloudflare Pages
- プロトコル:HTTP/3
- モジュールのファイルサイズ:746バイト
- 計測方法:DevToolsのNetworkパネルとPerformanceパネル
構成Aでは外部の関数を一つ読み込んでいます。
import { hello } from './utility/hello.js';
hello();一方で構成Bでは外部の関数を50個読み込んでいます。
import { hello1 } from './utility/hello1.js';import { hello2 } from './utility/hello2.js';import { hello3 } from './utility/hello3.js';import { hello4 } from './utility/hello4.js';import { hello5 } from './utility/hello5.js';// 残り45個続く
hello1();hello2();hello3();hello4();hello5();// 残り45個続く今回はCloudflare PagesでHTTP/3を利用しています。HTTP/3は複数ファイルを同時取得できるため、モジュール分割によるリクエスト増加がパフォーマンス低下に直結しにくい最新環境で検証しました。
ページのコンテンツは「こんにちは」の5文字のテキストを表示しただけのシンプルなものです。
ブラウザのキャッシュを無効化(Disable Cache)した状態でリロードし、DomContentLoaded等の各項目について10回ずつ計測を行い、平均化した数値を算出しました。その結果がこちらです。
通常速度では次のようになりました。
| 項目 | 構成A(1モジュール) | 構成B(50モジュール) | 差分 |
|---|---|---|---|
| DomContentLoaded | 177ms | 233ms | +56ms |
| Load | 185ms | 240ms | +55ms |
| LCP | 0.15s | 0.16s | +0.01s |
低速モード(slow 4G)では次のようになりました。
| 項目 | 構成A(1モジュール) | 構成B(50モジュール) | 差分 |
|---|---|---|---|
| DomContentLoaded | 1.74s | 2.16s | +0.42s |
| Load | 1.75s | 2.16s | +0.41s |
| LCP | 0.66s | 0.69s | +0.03s |
超低速モード(3G)でも念の為確認してみました。
| 項目 | 構成A(1モジュール) | 構成B(50モジュール) | 差分 |
|---|---|---|---|
| DomContentLoaded | 6.22s | 7.56s | +1.34s |
| Load | 6.23s | 7.57s | +1.34s |
| LCP | 2.12s | 2.13s | +0.01s |
今回の結果から、JSファイルの分割はDOMContentLoaded等には影響するが、ユーザーが実際に画面を見られるタイミング(LCP)にはほとんど影響しないということがわかります。
LCPとは
LCP(Largest Contentful Paint)は、ページの読み込み中に表示される最大の画像またはテキストブロックが画面に描画されるまでの時間を表す指標です。Core Web Vitalsの一つで、ユーザーが「主要なコンテンツをいつ頃見られたか」の目安になります。
LCPに影響が少ないのは、JSの読み込みがレンダリングをブロックしていないためだと考えられます。type="module"は自動的にdeferと同等の挙動になり、HTMLのパースを止めずにJSを取得するため、画面の描画自体は遅延しません。
一方でDOMContentLoadedに差が出るのは、deferスクリプトの実行完了を待ってから発火するイベントだからです。モジュール数が増えるほど依存関係の解決に時間がかかるため、その分だけ発火が遅れます。通常速度での差分は+56ms程度であり、この程度の遅延はユーザーが知覚できるレベルではないと考えられます。
なお、以下の点に留意する必要があります。
- 検証環境がCloudflare(HTTP/3)という非常に高速なCDNであり、一般的なレンタルサーバーではパフォーマンスへの影響がより大きくなる可能性がある
- モジュールのファイルサイズが小さい(746バイト)ため、影響が小さく留まった可能性がある
- モジュール数50個は極端なケースで、現実的な10〜20個では影響はさらに小さくなる
- ネットワークが遅い環境(モバイル回線など)では
DOMContentLoadedやLoadへの影響が大きくなりやすい
まとめ
結局のところビルドツールなしでJSを分割するのはありなのか?
LPや小規模サイトレベルの規模で10〜20モジュール程度であればLCPへの影響はほぼなく、開発体験の向上を考えるとありだと言えます。
逆にモジュール数が増えるほどDOMContentLoadedの遅延は大きくなるため、規模が大きいサイトではビルドツールでモジュールをバンドル(結合・最適化)することを検討した方が良さそうです。
