2023.04.10

次世代バンドルツールの競争の今 Turbopack vs Vite

こんにちは。グループ研究開発本部 次世代システム研究室のH.Oです。
生産的なアプリケーション開発において欠かせないものの一つにフロントエンドのJavaScriptバンドルツールがあります。

これまで、長年に渡ってJavaScriptのバンドルツールはwebpackがデファクトスタンダードとなり、Next.jsやNuxt.jsなどフレームワークにもデフォルトで組み込まれていました。その高機能性と安定性から現在も多くのWebサービスで利用されています。
一方で近年、webpackに続く次世代バンドルツールの開発競争が大きな注目を集めています。その中で特にwebpackに取って代わる勢いを見せているものにViteTurbopackがあります。これらは、開発するアプリケーションの肥大化に伴って処理が遅くなってしまうwebpackの問題点を解決することが主要目的となっています。

今回はViteとTurbopackを現在の動向を三つの観点で比較しながら、フロントエンドプロジェクトの開発体験の向上に繋がる方法を考察したいと思います。

アジェンダ

  • 内部の仕組みの比較
  • Webpackからの移行についての比較
  • 両者の主張

結論ファースト

  • 次世代のwebpackとも言える新しいバンドルツールの中で特に注目を浴びているのがViteとTurbopackである。
  • 現実的に開発されているアプリケーションのモジュール数を考えた場合、両者の性能を比較してTurbopackがViteよりも非常に優れている、と結論づける根拠がない。
  • 現状ではTurbopackはNext.js 13に付随する試験的なもの。Viteの方が様々なJavaScriptのフレームワークに対応しており、汎用性が高い。
  • 両者ともproduction buildの処理速度を改善することに対しては課題を抱えており、現状使用する大きなメリットは主にLocalでの開発となる。
  • Webpackを置き換えるというモチベーション自体は注目すべきである。開発の生産性が上がることは期待できる上、将来的な脱Webpackの動きに備えるのであれば、最初はユーザーの触らない管理画面のレポジトリなどで試験的に導入を進めていくのが良いと考える。

内部の仕組みの比較

Vite

Vue.jsの開発者であるEvan You氏が開発する新しいJavaScriptビルドツールです。2020年のリリース以降、近年の人気が急上昇しています。
直近の評価では2022年のState of JavaScript 2022Jamstack Community Survey Results 2022において、注目度が上昇傾向にあることがわかります。

内部の仕組みの要点をまとめます。画像は公式Documentから引用したものを使用しています。

開発サーバー

Webpackに代表される従来のバンドルツールは、全てのファイルを一つに統合する必要がありましたが、Viteはアプリケーションのモジュールを「依存関係」と「ソースコード」の二種類に分解して処理します。
  • 「依存関係」は開発中あまり変更されないプレーンなJSファイルを指し、esbuildを用いた事前バンドルを行います。esbuildはGoで実装されているビルドツールで、Go言語の強みでもある並列処理を駆使した高速な動作が特徴となっています。
  • 「ソースコード」は変更を必要とするJSX/CSS/Vue or Svelteのコンポーネントファイルなどを指します。
ルーティングによるコード分割を行うことで、全てのソースコードを同時に読む必要を無くしています。

従来のバンドラの仕事の一部をブラウザに渡す形で、Viteはブラウザのリクエストに応じて、ソースコードを変換して提供し、現在の画面で使われるもののみ、コードを動的インポートして処理することで、高速な動作を可能にしています。


従来のバンドル
(引用元:https://vitejs.dev/guide/why.html#slow-server-start


Viteの開発サーバー
(引用元:https://vitejs.dev/guide/why.html#slow-server-start

 

プロダクション

Production ModeではRollupを利用した従来通りのバンドルをしています。理由としては、importがネストされていると、ネットワークのラウンドトリップが増加してしまうので、プロダクションでは、バンドルされていないESMをリリースするのは非効率となるためです。開発サーバー側で用いているesbuildは、アプリケーションのバンドル用の機能が開発中なのでProduction Modeでは使用していません。当面はRollupを使いつつも、将来的にはesbuildを使う予定としています。

Turbopack

Webpackの開発者であるTobias Koppers氏が中心となりVercel社が開発している新しいバンドルツールで、2022年10月にα版がリリースされました。
Next.jsで開発されたアプリケーションのバンドルやビルドのスピードが遅いという問題を解決し、Webpackを置き換えることが狙いとなっています。

大きなコンセプト

一言で表現すると、Turbo engine × Webpack = Turbopackとなります。

Turbo engineは 漸増計算を可能にする再利用可能なRustベースのlibraryで、キャッシュ、Invalidation, インクリメンタルビルドなどの共通のタスクをこなすエンジンです。

WebpackはJSベースで、尚且つ漸増計算に対応しづらい面がありました。そのため、性能を向上させるためにWebpackを引き続き拡張していくことは難しいと判断し、Rustベースに移行し、Turbo engineを内蔵するTurborepoとWebpackで得られた知見を統合することで、Turbo engineとTurbopackの相互運用性の実現を目指すと発表しています。

Turbopackは、Viteの革新性を評価しつつも、アプリケーションの規模が大きくなった際にブラウザへのネットワークリクエストが大きく増加する点を、ローカルサーバーも含めた起動時間に影響を及ぼす問題点として指摘しています。

これを踏まえたTurbopackの大きな二つの特徴に
  • 関数レベルのcache
  • リクエストに応じたコンパイル
があります。

関数レベルのcache

公式ドキュメントに掲載されている画像を元に説明します。
開発用サーバー上で動作している上記のファイル構成を考えます。
まず、api.tsとsdk.tsの2つのファイルに対してreadFileを呼び出し、それらのファイルをバンドルし、concatで連結して、最終的にfullBundleを得る、という流れになります。この時、それぞれの関数呼び出しの結果は後でキャッシュに保存されます。
(引用元:https://turbo.build/pack/docs/core-concepts#function-level-caching

sdk.tsファイルを保存すると、Turbopackはファイルシステムのイベントを受けとり、readFile(“sdk.ts”)を再計算する必要があることを認識します。
sdk.tsの結果は変更されたため、再度バンドル、concatする必要が生じますが、その際、変更されていないapi.tsについてはキャッシュからその結果を読み取り、それをconcatに渡します。これにより、変更していないファイルを再度読み取り、再バンドルするといった無駄を省くことができます。
実際のバンドラーは数千のファイルを読み取ることを考慮すると、関数レベルのキャッシュによって膨大な処理を節約できることがわかります。

キャッシュの永続化、保存方法

Turboエンジンは現在、キャッシュをメモリに格納しており、プロセスを実行する間はキャッシュは続きます。
将来的には、このキャッシュを永続化するべく開発が進行中です。まずはファイルシステムに保存して永続化し、続いてVercel Remote Cacheを使用してチーム内で関数レベルのキャッシュを共有することができるようにする予定と発表されており、このアプローチが実現すればTurbopackは高速にアプリケーションの漸増計算ができるようになります。これを応用することでビルドも従来より高速に行うことができるようになる可能性もあり、今後の動向に目が離せません。

リクエストに応じたコンパイル

最初のNext.jsは開発モードでapp 全体をbundleしようとしていました。現在では開発サーバーでrequestされたページのみをバンドルするよう性能を改善させています。しかし、クライアントとサーバーのすべてのモジュール、ダイナミックにインポートされたモジュール、参照されたCSSおよび画像など、リクエストされたところとは直接関係のないものをバンドルしている点で非効率な部分が残っています。Turbopackは、リクエストされたコードのみをコンパイルし、ブラウザがHTMLを要求した場合は、HTMLのみをコンパイルし、HTMLで参照されているものはコンパイルしないようにします。
この時Native ESMを使用すると、サーバーへの多くのリクエストを生成してしまうため、リクエストレベルのコンパイルを使用することで、ブラウザへのリクエスト数を減らしてコンパイル速度を向上させています。

Webpackからの移行について

Turbopack

現状ではTurbopackを利用する方法は、Next.js v13でProjectを作成して試すのが唯一の手段となっています。

将来的にはWebpackのユーザーにとってフレンドリーな設定ができるAPIにするとしつつも、完全な1対1の互換性は保証しないとしており、あくまで別物を開発しているという立場を示しています。

Vite

プロジェクトの管理画面のフロントエンドを想定し、WebpackからViteへの移行を実践します。

Docker環境を前提に、create-react-app に react-adminをinstallした、管理画面を構築する最小構成のプロジェクトを考えます。
localhost:3000でアクセスできるようにします

ファイル構造は下記の通りになります。
.
├── Dockerfile
├── README.md
├── index.html
├── package.json
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── admin
│   │   └── index.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   └── setupTests.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite-env.d.ts
├── vite.config.ts
└── yarn.lock
viteをインストールします。
npm install vite

package.jsonでコマンドを書き換えます。
"dev": "vite --host",
"build": "tsc --noEmit && vite build",
"preview": "vite preview"
この時、Docker環境でViteを利用する際はvite –hostの–hostが必須となります。

クライアントでの型を定義するファイルを追加します。

vite-env.d.ts
/// <reference types="vite/client" />

tsconfig.jsonを修正します。
{
  "compilerOptions": {
    "target": "es2020",
    "lib": [
      "dom",
      "dom.iterable",
@@ -13,7 +13,7 @@
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
@@ -22,5 +22,6 @@
  },
  "include": [
    "src"
  ]
  ],
  "references": [{ "path": "./tsconfig.node.json" }]
}

tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}
Viteの開発サーバーはindex.htmlをアプリケーションのエントリポイントとしており、rootにindex.htmlを追加する必要があります。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Admin</title>
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
    />
  </head>
  <body>
    <div id="root"></div>
    <script>window.global = window;</script>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>
vite.config.jsに設定を書き込みます。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  build: {
    outDir: "build", // CRAに合わせて指定
  },
  server: {
    port: 3000,
    host: true,
  }, 
})

DockerfileのENTRYPOINTをVite用のコマンドに修正します。
ENTRYPOINT ["yarn", "dev"]
bandle時にbabelではなくSWCを利用するため、@vitejs/plugin-react-swcをインストールしています
また、Viteは開発サーバーのportが番号がデフォルトで5173となっていますが、vite.config.jsで自由に番号を設定できます。
react-scriptsをuninstallします

react-scriptsにはEsLintを含め、様々な設定が同梱されていたため、Viteに移行する際にはEsLintの必要なpackageのinstall、設定が必要になります。

今回はローカル環境での最小構成を考えましたが、実際のプロジェクトを移す際は、パスの解決の仕方、環境変数の登録の仕方にも違いがあるため、別途対応が必要になります。

両者の主張 〜本当にTurbopackはViteの10倍速いのか〜

Vercel側の主張

Turbopack側は、ViteがNative ESM、及びesbuildに依存している点に注目しています。

Viteは内部的にesbuildを多くのtaskで使っており、esbuildのコードは高速のbuildのために極めて最適化されている一方で、HMRを有しておらず、キャッシュを使用しないことを考慮すると、Rust製のbundlerがesbuildよりも良いパフォーマンスになると考えているため。

Turbopackの開発モードは、受信したリクエストに基づいてアプリのインポートとエクスポートの最小グラフを構築し、必要な最小限のコードのみをバンドルするため、最初に起動したときに非常に高速になります。ページをレンダリングするために必要なコードのみを計算し、単一のチャンクでブラウザに送信するため、大規模なアプリケーションの場合には、ネイティブESMよりも著しく高速になる、としています。

Evan You氏の主張

一方でTurbopackのリリース当時、Evan You 氏が、Turbopackの主張する「TurbopackはViteの10倍高速」という内容について反論とともに検証を試みた結果を発表しています。
Evan You氏の反論
結論としては、「Viteより10倍高速, webpackの700倍高速」という主張は、以下の時に成り立つとしています。
  • 比較している場面はホットリロードのパフォーマンスである。
  • Viteは、Nextが用いているSWC変換を使用しない。
  • 想定しているアプリケーションの規模は30000個以上のモジュールを含む規模であり、現実的ではない。
  • ベンチマークは、ホットアップデートされたモジュールが評価される時間のみを測定し、エンドユーザーが実際に意識するであろう、変更が実際に適用されるEnd to Endの時間を測定していない。
  • webpackでのバンドルについてはbabelでなくSWCを用いている
また、ViteとTurbopackの性能比較の仕方が厳密な同条件ではなかったこと等も踏まえると、現実的な範囲内でのモジュール数のプロジェクトにおいて、TurbopackとViteに圧倒的な差があるとは言い難いと言えます。

当時はVite3.2とNext13で比較されていましたが、現在はVite4がリリースされています。Vite4とNext13でも同様の比較が可能です。
Vite4で検証する場合は、プロジェクトの作成時に、babel依存ではなくSWCを選択するoptionが追加されたためvite-plugin-swc-react-refreshをインストールする必要はありません。後から追加する場合は、vite-plugin-swc-react-refreshではなく、vite-plugin-react-swcを追加します。
また、genFiles.jsとwatch.jsを書き換えることで、TypeScriptのプロジェクトでも計測可能になります。

まとめ

今回は、次世代のWebpackと目され、注目を浴びている二つのバンドルツールViteとTurbopackを取り上げ、三つの観点から比較検討しました。
まずは、筆者の考える双方のメリット・デメリットをまとめます。
  • Vite
    • pros
      • 開発サーバーの処理速度の向上によって、業務効率化ができること
      • 各種JSフレームワークでの開発やTypeScript対応も、比較的少ない手数で可能で、事例も集まっている。
    • cons
      • 開発サーバー側とプロダクション側で、依存しているパッケージが異なるため、(前者はesbuild,後者はRollup)内部的に重大なバグが生じた際の対応が難しいのではないかという懸念がある
      • Webpackベースで製作されている既存のアプリケーションをViteに移行する際のリスクがある。
      • テストフレームワークの利用に課題がある。(Vite環境で高速に動作するVitestを使用する必要がある。)
  • Turbopack
    • pros
      • Webpackの開発者が開発しており、Next.js v13からデフォルトで搭載されている。Webpackを置き換えるようなツールとなるのか、今後の動向は注視する必要がある。
    •  cons
      • 現在はα版
      • production modeが開発中
      • 現在異なる言語で実装されているTurborepo, Turbopackを一つのツールチェーンにする予定があるなど、開発動向に大きな動きがあることが予想されるため、安定するまでに時間を要する。
以上を考慮した上で以下を今回の考察の結論とします。
  • Production Buildに関しては、Vite, Turbopack双方とも主張する高速化・最適化が現実化されていないため、Production modeでの運用を考慮すると、現時点で大きなメリットがあるとは言えない。
  • Turbopackは現時点での導入は現実的ではないですが、Webpackの開発者が中心のチームで開発を進めていることも合わせると、今後も動向を注視する必要がある。
  • 一方でWebpackを置き換えるという意識は今後浸透していくと見られるので、積極的な利用していくのが良いと考えます。そのための第一歩として現時点で可能性がある方法はユーザーが触らない管理画面のプロジェクトなどでViteを試験的に導入することではないかと考える。
引き続き、今後の動向にも注目し、調査を続けていきたいと考えています。

参考リンク

https://ja.vitejs.dev/
https://turbo.build/pack
https://2022.stateofjs.com/ja-JP/libraries/build-tools/
https://jamstack.org/survey/2022/#frameworks-by-usage-and-satisfaction
https://github.com/yyx990803/vite-vs-next-turbo-hmr/discussions/8
https://vitest.dev/

最後に

グループ研究開発本部 次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネット上の高度なアプリケーション開発を行うエンジニア・アーキテクトを募集しています。募集職種一覧 からご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事