viteを使う

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の方がシンプル。
競合ツールと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(ESM)— 標準仕様

特徴

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>
index.html
// src/main.js
import { greet } from './greet.js';
greet('world');

// src/greet.js
export function greet(name) {
  console.log(`Hello, ${name}`);
}
src/main.js

このような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 };
CommonJS(CJS)— Node.js の従来の方式
ブラウザで動かない
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));

subtractmultiply は誰からも 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}]));

変わった点:

  • calculateTotalPricectotalPriceoitemstiteme (変数名の短縮)
  • 空白・改行・インデントの除去
  • オブジェクトのプロパティ名( pricequantity )は外部からアクセスされる可能性があるため変更されない

典型的なJSファイルでミニファイすると 30〜70% 程度サイズが減る。さらに gzip / brotli 圧縮と組み合わせると、元のサイズの 10〜20% 程度まで縮小できる。

Vite は vite build 時に esbuild(JS)および Lightning CSS(CSS)でミニファイを自動的に行う。設定不要。

開発サーバーとプレビューの違い

vite dev (開発サーバー)は ESM をそのままブラウザに配信する。バンドルしないため起動が速く、HMR で変更が即座に反映される。

vite previewvite build で生成された dist/ の中身をローカルで配信する静的ファイルサーバー。tree-shaking・コード分割・ミニファイが適用された本番ビルドの動作確認に使う。

開発サーバーでは ESM のまま動くが、本番ビルドでは Rollup でバンドルされるため、挙動が異なる場合がある。デプロイ前に vite preview で確認することで、本番環境との差異を事前に検出できる。

所感

viteについて学んだことをまとめた。細かいところや理解が落ちていたところも結構あったので、この作業で理解が深まったので良かった。

作成日
最終更新日