viteを使う可能性が出てきたので使い方を簡単に押えておく。
viteについてのおおまかな理解
役割
ネイティブ ES Module を活用した超高速な開発サーバーと、Rollup ベースの本番ビルドを組み合わせた次世代フロントエンドビルドツール。
フランス語で「速い」を意味する。Evan You(Vue.js の作者)が開発した。
競合のツール
競合をリストアップし、それらと比べてviteの何が優れているのかを確認する。
| 競合ツール | Viteの優位点 |
|---|---|
| webpack | 設定量が圧倒的に少ない。開発サーバーの起動がバンドル不要のため数秒で完了する(webpackは全ファイルを事前バンドルするため遅い)。 |
| Parcel | 設定ゼロ志向は共通だが、Viteはプラグインエコシステムが成熟しており、フレームワーク公式テンプレートとの統合が充実。 |
| Rollup | ViteはRollupをプロダクションビルドのコアとして内包しており、Rollupの設定を直接書かずに済む上位レイヤー。開発サーバーもセットで提供。 |
| Turbopack | VercelのNext.js専用色が強く、Next.js以外での利用が限定的。Viteはフレームワーク非依存でReact/Vue/Svelte/Solid等を幅広くサポート。 |
| esbuild | トランスパイル・バンドルの速度に特化したツール。Vite自身が依存関係のプリバンドルにesbuildを内部利用している。HMRや開発サーバー機能はViteが担う。 |
| Rspack | Rustベースのwebpack互換レイヤーで既存webpackプラグイン資産を再利用できるが、既存プロジェクトの移行用途が主。新規プロジェクトではViteの方がシンプル。 |
基本的な機能
- ネイティブ ESM ベースの開発サーバー
- ソースコードをバンドルせず、ブラウザが必要なモジュールをリクエストしたときにのみ変換・配信する。プロジェクト規模が大きくなっても起動時間がほぼ変わらない。
- HMR(Hot Module Replacement)
- 変更されたモジュールのみを即座に差し替える。フレームワーク固有の状態(ReactのuseStateなど)を保持したままUIが更新される。
- 依存関係のプリバンドル(esbuild)
node_modules内の CommonJS / UMD モジュールを初回起動時に esbuild で高速変換・キャッシュし、ESM として配信する。- Rollup ベースのプロダクションビルド
- Tree-shaking・コード分割・動的インポート対応の最適化されたバンドルを生成する。
- CSS / アセットハンドリング
- CSS Modules、PostCSS、CSS プリプロセッサ(Sass/Less/Stylus)を設定ほぼ不要で利用可能。静的アセットはハッシュ付きファイル名でバンドルされる。
- TypeScript サポート
- 設定なしで
.ts/.tsxファイルを読み込める(esbuildで高速トランスパイル)。型チェックはtsc --noEmitまたはvite-plugin-checkerで別途行う。 - JSON / Web Worker / WebAssembly のネイティブサポート
- 追加プラグインなしで
import可能。 - プラグインシステム
- Rollup プラグイン互換のAPIを持ち、フレームワーク公式プラグインが充実している。
- SSR サポート
- サーバーサイドレンダリング向けのAPIを提供。Nuxt・SvelteKit・Remixなどのフレームワークがviteをコアに採用している。
簡単な使い方
プロジェクト作成:
npm create vite@latest my-app
cd my-app
npm installプロジェクト構造(React + TypeScript の例):
my-app/
├── index.html # エントリポイント(src/main.tsxを<script type="module">で読み込む)
├── vite.config.ts
├── tsconfig.json
├── package.json
└── src/
├── main.tsx
├── App.tsx
└── assets/
vite.config.ts の最小構成:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});主要コマンド:
npm run dev # 開発サーバー起動(デフォルト: http://localhost:5173)
npm run build # プロダクションビルド(dist/ に出力)
npm run preview # ビルド結果をローカルでプレビューユースケースごとの使い方
標準機能
npmパッケージを配布する — ライブラリモードでのビルド
npmパッケージとしてライブラリを配布したい場合:
import { defineConfig } from "vite";
export default defineConfig({
build: {
lib: {
entry: "./src/index.ts",
name: "MyLib",
fileName: "my-lib",
},
},
});公開ページと管理画面を分ける — マルチページアプリケーション
複数のHTMLエントリポイントを持つ場合:
import { defineConfig } from "vite";
export default defineConfig({
build: {
rollupOptions: {
input: {
main: "index.html",
admin: "admin.html",
},
},
},
});開発中にバックエンドAPIへ繋ぐ — プロキシ設定
バックエンドAPIへのプロキシ:
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
},
},
},
});本番と開発でAPIのURLを切り替える — 環境変数
.env ファイルで環境変数を管理する。 VITE_ プレフィックス付きの変数のみクライアントに公開される。
# .env
VITE_API_URL=https://api.example.com
SECRET_KEY=xxx # クライアントには公開されない// コード内での使用
const apiUrl = import.meta.env.VITE_API_URL;内部的な仕組みと歴史
ES Module
ES Module(ESM)は JavaScriptのファイル間でのコードを共有する仕組みのことだ。 import / export を使ってコードを共有する。現在では公式のモジュールシステムとなっている。
import lodash from 'lodash';
export const myFunc = () => {};特徴
ES Moduleの特徴を少しまとめておこう。
- 静的構造
import/exportはファイルのトップレベルに書く。条件分岐の中には書けない。これにより、コードを実行しなくてもどのモジュールが使われているか解析できる(tree-shaking の前提)。- 非同期読み込み
- ブラウザはモジュールを非同期で取得するため、HTMLの描画をブロックしない。
- strict mode
- ESM は常に strict mode で動作する。
ブラウザでのES Module
ブラウザは <script type"module">= を指定すると、ESM として読み込む。
<script type="module" src="/src/main.js"></script>// src/main.js
import { greet } from './greet.js';
greet('world');
// src/greet.js
export function greet(name) {
console.log(`Hello, ${name}`);
}
このような2つのファイルがあるとする。ブラウザは main.js を読み込み、 import 文を見つけると greet.js をHTTPリクエストで取得する。バンドラなしでモジュールが動く。Viteの開発サーバーはこの仕組みをそのまま使う。ソースファイルをバンドルせず、ブラウザが import するたびにサーバーが該当ファイルを返す。だから起動が速い。
JS モジュールシステムの変遷
ES Moduleについて見てきたが、ES Moduleに辿りつくまでのこれまでの道のりも確認しておこう。
この変遷の間に僕はWebの仕事をしていたけれど、実際にいろいろな技術が登場して、どれが良いのか悪いのか、何があるのかなど、結構混乱気味だった。まあ、今も混乱してなくはないのだけれど。
自分達で頑張る世界
JavaScript は1995年の誕生時にモジュールシステムを持っていなかった。全てのスクリプトがグローバルスコープを共有しており、名前の衝突や依存関係の管理が困難だった。
<!-- 全部グローバル。読み込み順にも依存する -->
<script src="a.js"></script>
<script src="b.js"></script>非公式な仕組みの登場
これを解決するために、非公式なモジュールの仕組みが次々と生まれた。
| 年 | 仕組み | 概要 |
|---|---|---|
| ~2008 | IIFE | (function(){ ... })() でスコープを閉じる。手動の名前空間管理 |
| 2009 | CommonJS | Node.js が採用。 require / module.exports 。サーバー側のモジュール問題を解決 |
| 2009 | Closure Library | Google の独自モジュールシステム。 goog.provide / goog.require で依存関係を管理。Closure Compiler による未使用コード除去(tree-shaking の先駆け)も備えていた |
| 2010 | AMD | RequireJS。ブラウザ向け非同期モジュール。 define(['dep'], function(dep){ }) |
| 2014 | UMD | CommonJS と AMD の両方で動くラッパー。互換性のためのボイラープレート |
| 2015 | ES Module | ECMAScript 2015 で import / export が言語仕様として標準化 |
Closure Compiler
Google が2009年に公開した JavaScript の最適化コンパイラ。Closure Library と組み合わせて使うことが想定されていたが、単体でも利用できた。Java で書かれている。
3つのコンパイルレベルがある。
- WHITESPACE_ONLY
- 空白・コメントの除去のみ。今で言う単純なミニファイ。
- SIMPLE
- 変数名の短縮を加える。ローカル変数のみ対象。
- ADVANCED
- 未使用コードの除去、プロパティ名の短縮、関数のインライン化、定数の畳み込みなど。最も攻撃的な最適化。
ADVANCED モードの動作例:
// 入力
function hello(name) {
return 'Hello, ' + name;
}
function unused() {
return 'this is dead code';
}
console.log(hello('world'));// ADVANCED モードの出力
console.log("Hello, world");
unused 関数が除去され(tree-shaking)、 hello 関数がインライン化され、文字列結合が定数に畳み込まれている。2009年の時点でこれをやっていた。
ただし ADVANCED モードは制約が厳しく、JSDoc 形式のアノテーション( @param {string} 、 @return {number} など)を付ける必要があった。外部ライブラリとの連携には externs ファイルの定義も必要で、導入のハードルが高かった。Google 社内では広く使われたが、一般に普及はしなかった。現在の tree-shaking やミニファイの概念的な先駆けと言える。
CommonJS とその限界
CommonJS は Node.js のために作られたもので、根本的な問題があった。
const lodash = require('lodash');
module.exports = { myFunc };- ブラウザで動かない
requireは同期的にファイルを読む前提。ブラウザの HTTP リクエストには使えない。- 静的解析できない
requireは関数呼び出しなので、実行しないと何を読み込むか分からない。tree-shaking 不可。- エコシステムの分裂
- サーバーは CommonJS、ブラウザは AMD と、別々の仕組みが併存していた。
ES Module による統一
ES2015 で import / export が標準化された目的は明確だった。
- 1つの仕組みでサーバーもブラウザも統一する
- 静的構造にして最適化(tree-shaking)を可能にする
- 非同期読み込みに対応する
ただし、仕様策定から実際に使えるようになるまでには時間がかかった。
| 年 | 出来事 |
|---|---|
| 2015 | ES2015 仕様策定。ただしブラウザ・Node.js の実装はまだ |
| 2017 | Chrome 61、Safari 11 でブラウザサポート開始 |
| 2018 | Firefox 60 でサポート |
| 2019 | Node.js 12 で実験的サポート |
| 2021 | Node.js 16 で安定版サポート |
仕様から実用まで6年かかった。特に Node.js のエコシステムは CommonJS で巨大になっていたため、移行が遅れた。2026年現在、主要なライブラリは ESM 対応が進み新規プロジェクトでは ESM がデフォルトだが、CommonJS のパッケージもまだ大量に残っている。Vite が esbuild でプリバンドルしているのは、この互換性問題を吸収するためでもある。
ビルド時の最適化
Vite が本番ビルド時に行う最適化について掘り下げる。
Tree-shaking
使われていないコードをビルド時に除去する最適化手法。ESM の静的構造を利用して、どの export が実際に import されているかを判定する。
// utils.js — 3つの関数をエクスポート
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}// main.js — add だけ使う
import { add } from './utils.js';
console.log(add(2, 3));ビルド後の出力(概念):
// subtract と multiply は消える。add だけが残る
function add(a, b) {
return a + b;
}
console.log(add(2, 3));
subtract と multiply は誰からも import されていないので、最終的なバンドルに含まれない。ファイルサイズが小さくなる。
CommonJS では tree-shaking が効かない。 require は関数呼び出しなので、条件分岐の中にも書ける。静的に解析できないため、何が使われているか判定できず、全部バンドルに含めるしかない。
// CommonJS — require は動的に呼べる
const utils = require('./utils');
// utils のどのプロパティが使われるか、実行しないと分からない
コード分割(Code Splitting)
アプリケーション全体を1つの巨大なJSファイルにまとめず、複数のファイル(チャンク)に分割する手法。必要なコードだけを必要なタイミングで読み込める。
// main.js — ボタンを押したときだけ chart.js を読み込む
document.getElementById('btn').addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart();
});ビルド後の出力:
dist/
├── index.js # main.js のバンドル(初期読み込み)
└── chart-a1b2c3.js # chart.js のバンドル(ボタン押下時に読み込み)
chart.js は初期表示時にはダウンロードされない。ユーザーがボタンを押して初めて取得される。初期表示が速くなる。
ルートベースの分割(React の例):
import { lazy } from 'react';
// 各ページを動的インポートで分割
const Home = lazy(() => import('./pages/Home'));
const Settings = lazy(() => import('./pages/Settings'));
const Dashboard = lazy(() => import('./pages/Dashboard'));ビルド後:
dist/
├── index.js # ルーター等の共通コード
├── Home-x1y2z3.js # /home にアクセスしたときだけ読み込む
├── Settings-a4b5c6.js # /settings にアクセスしたときだけ読み込む
└── Dashboard-d7e8f9.js全ページのコードを一括ダウンロードせず、表示するページのコードだけ取得する。
ミニファイ(Minify)
コードの動作を変えずにファイルサイズを小さくする処理。空白・改行の除去、変数名の短縮などを行う。
ミニファイ前:
function calculateTotalPrice(items) {
let totalPrice = 0;
for (const item of items) {
totalPrice += item.price * item.quantity;
}
return totalPrice;
}
console.log(calculateTotalPrice([
{ price: 500, quantity: 2 },
{ price: 1200, quantity: 1 },
]));ミニファイ後:
function c(t){let o=0;for(const e of t)o+=e.price*e.quantity;return o}console.log(c([{price:500,quantity:2},{price:1200,quantity:1}]));変わった点:
calculateTotalPrice→c、totalPrice→o、items→t、item→e(変数名の短縮)- 空白・改行・インデントの除去
- オブジェクトのプロパティ名(
price、quantity)は外部からアクセスされる可能性があるため変更されない
典型的なJSファイルでミニファイすると 30〜70% 程度サイズが減る。さらに gzip / brotli 圧縮と組み合わせると、元のサイズの 10〜20% 程度まで縮小できる。
Vite は vite build 時に esbuild(JS)および Lightning CSS(CSS)でミニファイを自動的に行う。設定不要。
開発サーバーとプレビューの違い
vite dev (開発サーバー)は ESM をそのままブラウザに配信する。バンドルしないため起動が速く、HMR で変更が即座に反映される。
vite preview は vite build で生成された dist/ の中身をローカルで配信する静的ファイルサーバー。tree-shaking・コード分割・ミニファイが適用された本番ビルドの動作確認に使う。
開発サーバーでは ESM のまま動くが、本番ビルドでは Rollup でバンドルされるため、挙動が異なる場合がある。デプロイ前に vite preview で確認することで、本番環境との差異を事前に検出できる。
所感
viteについて学んだことをまとめた。細かいところや理解が落ちていたところも結構あったので、この作業で理解が深まったので良かった。