Tauri のインストーラーは小さいと言われますが、依存クレートや画像が増えると 1 桁大きくなることもあります。配布物の中身は、Rust の実行ファイル、その中に圧縮して埋め込まれるフロントエンド(frontendDist の中身)、bundle.resources で同梱したファイル、インストーラー形式ごとの追加分の 4 つです。どれが大きいかで打つ手が変わるので、削る前に分けて測ります。削る手段は アプリを軽量化する、画像は 画像を圧縮してサイズを減らす で扱います。
前提条件
プラグインや権限は不要です。Rust 側の内訳を見る cargo-bloat を一度だけ入れます。
cargo install cargo-bloat
比べるのは必ず npm run tauri build の出力です(本番用にアプリをビルドする)。--debug 付きのビルドは Rust の最適化がかからず、Tauri が勧める Vite の設定では最小化も止まるので、何倍も大きく出ます。
1. 3 つの大きさを並べる
フロントエンド(dist/)、実行ファイル、インストーラーの大きさを並べます。Windows なら次のとおりです。
# プロジェクトのルートで、tauri build の直後に実行する
$dist = (Get-ChildItem dist -Recurse -File | Measure-Object Length -Sum).Sum
"{0,8:N0} KB dist/" -f ($dist / 1KB)
Get-ChildItem src-tauri/target/release/*.exe, src-tauri/target/release/bundle/*/* -File |
ForEach-Object { "{0,8:N0} KB {1}" -f ($_.Length / 1KB), $_.Name }
macOS / Linux なら du -sh dist src-tauri/target/release/bundle/*/* と ls -lh src-tauri/target/release/<アプリ名> です。
読むときの要点は、フロントエンドがビルド時に brotli で圧縮されてから実行ファイルに入ることです。最小化した JS や CSS は 3 分の 1 程度になりますが、JPEG・PNG・WebP・woff2 フォントはもともと圧縮済みなのでほぼそのまま増えます。手元の例では 643KB の JS と 1.5MB の JPEG を足すと実行ファイルが約 1.7MB 増え、その 9 割が JPEG でした。
| 目立って大きいもの | 疑う原因 | 調べ方 |
|---|---|---|
dist/ | ライブラリ、画像、フォント、ソースマップ | 2 章 |
実行ファイル(dist/ の割に) | 依存クレート、Rust に埋め込んだ画像 | 3 章 |
| インストーラーだけ | WebView2 の導入方法、同梱ファイル、AppImage | 4 章 |
2. フロントエンドの内訳を調べる (TypeScript)
vite build の最後に出る表は出力ファイルごとの大きさで、JS・CSS には gzip 後の値も付きます。JS・CSS による実行ファイルの増え方は、この gzip の値より少し小さい程度です。ただし public/ のファイルはコピーされるだけなので表に出ず、どの npm パッケージが JS を膨らませているかも分かりません。ビルドの最後に集計する小さなプラグインで補います。
// vite.config.ts
import { defineConfig, type Plugin } from 'vite';
// ビルドの最後に、出力ファイルと「JS に入った量」をパッケージ別に大きい順で表示する
function sizeReport(top = 8): Plugin {
return {
name: 'size-report',
apply: 'build',
generateBundle(_options, bundle) {
const files: Array<[string, number]> = [];
const packages = new Map<string, number>();
for (const out of Object.values(bundle)) {
if (out.type === 'asset') {
const bytes = typeof out.source === 'string' ? out.source.length : out.source.byteLength;
files.push([out.fileName, bytes]);
continue;
}
files.push([out.fileName, out.code.length]);
for (const [id, mod] of Object.entries(out.modules)) {
// node_modules/名前 または node_modules/@scope/名前 を 1 つのパッケージとして数える
const hit = /node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(id);
const name = hit ? hit[1].replace('\\', '/') : '(自分のコード)';
packages.set(name, (packages.get(name) ?? 0) + mod.renderedLength);
}
}
const kb = (n: number) => `${(n / 1024).toFixed(1).padStart(8)} KB`;
console.log('\n[size-report] 出力ファイル(public/ からのコピーは含まない)');
for (const [name, size] of files.sort((a, b) => b[1] - a[1]).slice(0, top)) console.log(`${kb(size)} ${name}`);
console.log('[size-report] JS の内訳(最小化する前の長さ。比率の目安)');
for (const [name, size] of [...packages].sort((a, b) => b[1] - a[1]).slice(0, top)) console.log(`${kb(size)} ${name}`);
},
};
}
export default defineConfig({
plugins: [sizeReport()],
});
見落としやすいのは dist/ に紛れ込むファイルです。build.sourcemap: true のままの .map も、public/ に置いたままの元画像も、すべて実行ファイルに埋め込まれます。図で見たいなら rollup-plugin-visualizer も使えますが、beforeBuildCommand で毎回動くので分析するときだけ有効にし、レポートは dist/ の外に出します。
3. Rust の実行ファイルの内訳を調べる (Rust)
cargo bloat でクレート別に見る
src-tauri で実行します。--features tauri/custom-protocol は tauri build と条件を揃えるためで、付けないと devUrl がある構成ではフロントエンドが埋め込まれず、実際より小さく出ます。cargo bloat はデバッグ情報付きでビルドし直すので、--target-dir を分けないと target/release の実行ファイルが上書きされます。
cd src-tauri
cargo bloat --release --crates -n 10 --features tauri/custom-protocol --target-dir target/bloat
.text 列はコード部分(.text)に占める割合です。最後の行の「.text の大きさ」と「ファイルの大きさ」の差は主に埋め込まれたデータで、差が大きければ原因はクレートではなくフロントエンドや画像です。std・tauri・tokio が上位なのは普通で、目立つクレートがあれば --filter <クレート名> で関数別に見ます。LTO で関数がまたがって最適化されるので、割り当ては目安です。
依存クレートを cargo tree でたどる
cargo tree -i <クレート名> -e normal --target all # そのクレートを誰が引き込んでいるか
cargo tree -d -e normal,no-proc-macro # 同じクレートの複数バージョン
cargo tree -e features -i tokio --depth 1 # tokio のどの機能が有効か
cargo tree は既定では今の OS 向けの依存しか表示しないので、他 OS 専用のクレートを探すときは --target all を付けます。小さなクレートの重複は珍しくありませんが、大きなクレートが二重に入っているなら、プラグインのバージョンを揃えると 1 つにまとまることがあります。
埋め込まれたフロントエンドを一覧する
asset_resolver().iter() は、実行ファイルに入っているフロントエンドを埋め込んだ状態(圧縮後)のバイト列で返します。ファイルごとに実際に何バイト使っているかが分かります。tauri dev では埋め込まれないので、tauri build で作ったアプリで確かめます。
#[derive(serde::Serialize)]
struct EmbeddedAsset {
path: String,
bytes: usize,
}
/// 実行ファイルに埋め込まれたフロントエンドを、埋め込み後の大きさの順に返す
#[tauri::command]
fn embedded_assets(app: tauri::AppHandle) -> Vec<EmbeddedAsset> {
let mut list: Vec<EmbeddedAsset> = app
.asset_resolver()
.iter()
.map(|(path, bytes)| EmbeddedAsset { path: path.into_owned(), bytes: bytes.len() })
.collect();
list.sort_by(|a, b| b.bytes.cmp(&a.bytes));
list
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![embedded_assets])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
リリースビルドでは開発者ツールが開けないので、結果は画面に出します。
import { invoke } from '@tauri-apps/api/core';
type EmbeddedAsset = { path: string; bytes: number };
export async function showEmbeddedAssets() {
const list = await invoke<EmbeddedAsset[]>('embedded_assets');
const total = list.reduce((sum, a) => sum + a.bytes, 0);
const lines = list.slice(0, 15).map((a) => `${(a.bytes / 1024).toFixed(1).padStart(8)} KB ${a.path}`);
const pre = document.createElement('pre');
pre.textContent = [`合計 ${(total / 1024).toFixed(0)} KB(${list.length} ファイル)`, ...lines].join('\n');
document.body.append(pre);
}
4. インストーラーだけが大きいとき
- Windows:
bundle.windows.webviewInstallModeを確かめます。既定のdownloadBootstrapperはほとんど増えませんが、embedBootstrapperで約 1.8MB、offlineInstallerで約 127MB、fixedRuntimeで約 180MB 増えます。 - 同梱ファイル:
bundle.resourcesのファイルはそのまま入ります(画像やDBファイルを配布物に同梱する)。 - Linux: AppImage は依存ライブラリを丸ごと含むので、小さなアプリでも 70MB を超えます。
インストーラーは中身を圧縮するので実行ファイルより小さくなるのが普通で、同じくらいなら画像や動画のような縮まない中身が多いということです。
動作確認
最小構成のアプリ(opener プラグインのみ、Windows x64)で 1 章のスクリプトを実行した例です。
0 KB dist/
4,182 KB sizelab.exe
1,333 KB sizelab_0.1.0_x64-setup.exe
同じアプリで 3 章の cargo bloat を実行した結果です。実行ファイルの 8 割がコードで、上位は tauri・std・tokio です。
File .text Size Crate
20.7% 25.3% 860.7KiB tauri
19.9% 24.2% 824.6KiB std
11.9% 14.4% 492.2KiB tokio
4.0% 4.9% 167.8KiB sizelab_lib
3.8% 4.6% 156.3KiB tao
3.0% 3.7% 126.1KiB tauri_runtime_wry
1.7% 2.1% 71.0KiB serde_json
1.5% 1.8% 61.8KiB wry
1.4% 1.7% 56.7KiB hashbrown
1.2% 1.5% 50.1KiB muda
11.7% 14.2% 483.5KiB And 64 more crates. Use -n N to show more.
82.1% 100.0% 3.3MiB .text section size, the file size is 4.1MiB
dist/ に 643KB の JS と 1.5MB の JPEG を置いてビルドし、showEmbeddedAssets() を呼ぶと次のように出ます。JS は 3 分の 1 以下になり、JPEG はほとんど縮んでいません。
合計 1747 KB(3 ファイル)
1547.5 KB /photo.jpg
199.2 KB /app.min.js
0.1 KB /index.html
よくあるエラーと対処法
embedded_assetsが空の配列を返す:tauri devで動かしています。devUrlがあるとフロントエンドは埋め込まれません。- cargo bloat の数字が
tauri buildの実行ファイルと合わない:--releaseか--features tauri/custom-protocolが抜けています。 - 「warning: nothing to print.」:
cargo tree -iで指定したクレートが今の OS 向けの依存にありません。--target allを付けるか綴りを確かめます。 dist/が小さいのに実行ファイルだけ大きい: Rust のinclude_bytes!やinclude_image!で画像を埋め込んでいる可能性があります。include_image!は画素を展開した状態で入り、512px なら約 1MB です(perf-003)。
OS ごとの違いと注意点
- Windows: インストーラーは既定で WebView2 を含まないので小さく済みます。
- macOS:
.appはフォルダーなのでdu -shで測ります。universal-apple-darwinで作ると両アーキテクチャのコードが入るぶん大きくなります。 - Linux: 形式で桁が変わるので、同じ形式どうしで比べます。
