はじめに
「ちょっとReactの挙動を試したいだけなのに、npm installして、bundlerを設定して……」そんな経験はありませんか?たった数行のコードを動かすために、node_modulesに数百MBのファイルが生成され、ビルド設定に時間を溶かす。モダンフロントエンド開発の"儀式"に疲れている方は多いはずです。
そこで注目したいのがesm.shです。URLをひとつ書くだけで、npmパッケージをESモジュールとしてブラウザから直接インポートできるCDNサービスです。この記事では、esm.shの基本から実践的な活用法までを解説します。
とはいえ、説明を読むより実際に動かした方が早いはずです。https://esm.sh/経由のimportがブラウザ上でそのまま動く様子を用意したので、先に挙動を確かめたい方はこちらからどうぞ。
esm.shとは
esm.shは「A no-build JavaScript CDN」を掲げるコンテンツ配信サービスです。npmに公開されているパッケージを、リクエストに応じてESモジュール形式に変換して配信してくれます。CommonJS形式でしか提供されていない古いパッケージでも、esm.shを経由すればブラウザのimport文でそのまま読み込めるのが最大の魅力です。
Cloudflareのグローバルネットワーク上で動作しており、変換結果はエッジにキャッシュされるため、2回目以降のアクセスは高速です。
主な特徴
- ノービルド -
npm installもbundlerも不要。URLを書くだけでパッケージが使えます - CommonJSの自動変換 - ESM非対応のパッケージもESモジュールとして配信されます
- TypeScript型定義の自動配信 -
X-TypeScript-Typesヘッダで型情報が提供され、DenoやエディタでそのままIntelliSenseが効きます - 複数レジストリ対応 - npmだけでなく、JSR(
/jsr/)、GitHubリポジトリ(/gh/)からも取得できます - 豊富なビルドオプション - クエリパラメータでtree shakingやターゲット指定などを細かく制御できます
インストール
esm.shはCDNサービスなので、インストール作業は一切不要です。HTMLファイルとブラウザさえあれば始められます。
<script type="module">
import confetti from "https://esm.sh/canvas-confetti@1.9.3";
confetti();
</script>
このHTMLをブラウザで開くだけで、紙吹雪が舞います。package.jsonもnode_modulesも必要ありません。
esm.shのサンプルを動かす
esm.shの核心は、https://esm.sh/{パッケージ名}@{バージョン}というURLをimport文にそのまま書けば、そのパッケージがESモジュールとして読み込めることです。下のサンプルでは、軽量なID生成ライブラリnanoidをesm.sh経由でバージョン固定インポートし、ボタンを押すたびにnanoid()関数でランダムなIDを生成して画面に表示します。桁数を変える入力欄も用意したので、まずは触って挙動を確かめてみてください。
import { nanoid } from "https://esm.sh/nanoid@6.0.1";
// 既定では21文字のランダムなIDを生成する
const id = nanoid();
// 引数で桁数を指定することもできる
const shortId = nanoid(10);
console.log(id, shortId);
実際に動かせるものが下です。入力欄の桁数を変えると、生成されるIDの長さがその場で変わります。
npm installもbundlerの設定も行わず、<script type="module">の中でhttps://esm.sh/nanoid@6.0.1をそのままimportしているだけなのに、nanoidパッケージのnanoid()関数が問題なく呼び出せています。URL中のバージョン番号(@6.0.1)を書き換えれば別のリリースを試せますし、桁数の入力欄を変えればnanoid(size)に渡す引数が変わり、生成されるIDの長さがその場で変化します。これがesm.shの最も基本的で、最も強力な使い方です。
基本的な使い方
バージョン指定
URLのパスでsemverによるバージョン指定ができます。本番で使う場合はバージョンを固定するのがおすすめです。
// 最新版(開発時のみ推奨)
import React from "https://esm.sh/react";
// バージョン固定
import React from "https://esm.sh/react@19.2.0";
// メジャーバージョンのみ指定
import React from "https://esm.sh/react@19";
// サブモジュールのインポート
import { renderToString } from "https://esm.sh/react-dom@19.2.0/server";
Import Mapsとの組み合わせ
URLを毎回書くのは冗長なので、実際の開発ではimport mapsと組み合わせるのが定番です。ベア指定子(reactのような通常のパッケージ名)で書けるようになります。
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.2.0",
"react-dom/client": "https://esm.sh/react-dom@19.2.0/client"
}
}
</script>
<script type="module">
import { createRoot } from "react-dom/client";
// 通常のnpmプロジェクトと同じ書き味で開発できます
</script>
便利なクエリパラメータ
esm.shの真骨頂は、クエリパラメータによる柔軟なビルド制御です。
// 必要なエクスポートだけを取得(tree shaking)
import { debounce } from "https://esm.sh/lodash-es@4.17.21?exports=debounce";
// 開発モードでビルド(Reactの詳細なエラーメッセージが出ます)
import React from "https://esm.sh/react@19.2.0?dev";
// ビルドターゲットを指定
import dayjs from "https://esm.sh/dayjs@1.11.13?target=es2022";
// Web Workerとしてロード
import workerFactory from "https://esm.sh/monaco-editor@0.52.0/esm/vs/editor/editor.worker?worker";
特に?exportsは効果が大きく、lodashのような大きなライブラリから関数ひとつだけを取り出す場合に転送量を大幅に削減できます。
実践的なユースケース
ビルドなしでReact + TSXを動かす
esm.shが提供するesm.sh/tsxという約1KBのスクリプトを読み込むと、HTML内に書いたTSXをそのまま実行できます。コンパイルはesm.shのサーバー側で行われ、結果はキャッシュされます。ポイントは、import mapsでreactとreact-dom/clientをesm.sh経由のURLに解決しておき、<script type="module" src="https://esm.sh/tsx">を読み込むだけで<script type="text/babel">内のJSXがそのまま動くことです。
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.2.8",
"react-dom/client": "https://esm.sh/react-dom@19.2.8/client"
}
}
</script>
<script type="module" src="https://esm.sh/tsx"></script>
<script type="text/babel">
import { useState } from "react";
import { createRoot } from "react-dom/client";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>クリック数: {count}</button>;
}
createRoot(document.getElementById("root")).render(<Counter />);
</script>
実際に動かせるものが下です。ボタンを押すたびにuseStateのカウントが増え、画面がその場で再描画されます。
このファイルをブラウザで開くだけで、状態を持つReactコンポーネントが動きます。useState(0)の初期値をuseState(10)に書き換えれば、開いた瞬間のクリック数表示が10からスタートするようになります。プロトタイピングや社内ツール、技術検証にはこれで十分というケースも多いのではないでしょうか。
依存関係の共有とバージョン制御
複数のライブラリが同じ依存を持つ場合、?externalで外部化してimport mapsに解決を委ねることで、Reactの二重ロードのような問題を防げます。preactと、preactのVNodeをHTML文字列に変換するpreact-render-to-stringを組み合わせる場合が典型例です。?external=preactを付けずにいると、preact-render-to-stringが独自にpreactをバンドルしてしまい、import mapsで解決した方のpreactとは別インスタンス扱いになって描画に失敗します。
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/preact@10.29.8",
"preact-render-to-string": "https://esm.sh/preact-render-to-string@6.7.0?external=preact"
}
}
</script>
<script type="module">
import { h } from "preact";
import render from "preact-render-to-string";
const vnode = h("button", { class: "btn" }, "クリックしてね");
const html = render(vnode); // '<button class="btn">クリックしてね</button>'
</script>
実際に動かせるものが下です。入力欄の文字を書き換えると、h()で組み立てたVNodeがpreact-render-to-stringのrender()でHTML文字列に変換され、その文字列がそのまま画面に反映されます。
https://esm.sh/*パッケージ名のようにアスタリスクを付けると、すべての依存を一括で外部化することもできます。もし?external=preactを外してこのサンプルを動かすと、preact-render-to-stringが独自にバンドルしたpreactと、こちらでimportしたpreactが別インスタンスになり、render()がエラーを返すようになります。
esm.sh独自クエリオプションで転送量を抑える
「便利なクエリパラメータ」で紹介した?exportsを、実際の入力フォームで使ってみましょう。lodash-esからdebounce関数だけを取り出してesm.sh経由でインポートし、検索キーワードの入力を間引く処理を実装します。
import { debounce } from "https://esm.sh/lodash-es@4.17.21?exports=debounce";
const search = debounce((keyword) => {
console.log("検索実行:", keyword);
}, 300);
document.getElementById("keyword").addEventListener("input", (e) => {
search(e.target.value);
});
実際に動かせるものが下です。入力欄に連続してタイプしている間はdebounceによって実行が抑えられ、タイプが止まってから300ミリ秒後に初めて実行回数が増えます。
debounce(fn, 300)の300を1000のように大きくすれば、実行されるまでの間隔がさらに空くのが分かるはずです。?exports=debounceによってlodash-es全体(未圧縮で数百KB)ではなく、debounce関数とその依存だけが転送されるため、ページの読み込みも軽量に保てます。
Denoでnpmパッケージを使う
esm.shはDenoフレンドリーな設計で、Node.jsの組み込みモジュール(fs、osなど)への依存も解決してくれます。型定義も自動で配信されるため、エディタ補完も効きます。
// main.ts
import chalk from "https://esm.sh/chalk@5.4.1";
console.log(chalk.green("DenoからnpmパッケージをESMで利用できます"));
deno run main.ts
GitHubリポジトリから直接インポート
npmに公開されていないパッケージでも、/gh/プレフィックスでGitHubから直接取得できます。タグやコミットハッシュの指定も可能です。
import tslib from "https://esm.sh/gh/microsoft/tslib@2.8.0";
まとめ
esm.shは、URLひとつでnpmパッケージをESモジュールとして利用できるノービルドCDNです。本記事のポイントを振り返ります。
- インストール・ビルド不要で、HTMLとブラウザだけで開発を始められます
- import mapsと組み合わせれば、通常のnpmプロジェクトに近い書き味になります
?exportsや?externalなどのクエリパラメータで、転送量や依存解決を細かく制御できますesm.sh/tsxを使えばTSXすらビルドなしで動かせます- Deno・JSR・GitHubにも対応した、モダンなエコシステムのハブ的存在です
大規模な本番アプリケーションではViteなどのビルドツールに軍配が上がる場面も多いですが、プロトタイピング、技術検証、小規模ツール、教育用途では、esm.shの手軽さは圧倒的です。まずはHTMLファイルをひとつ作って、お気に入りのライブラリをURLでインポートしてみてください。「ビルドしない開発」の快適さに驚くはずです。