typescript + vite + reactを使ったChrome拡張機能の土台を作って検証してみた

Chrome拡張機能を`typeScript` + `webpack` + `create react app`で開発していますが、ビルドが遅いので、開発でストレスになっていた。

一方、`vite`がビルド速度が早いと巷で話題になっているので、まずは土台を作成し、本当に早いのか検証してみました。

## 使用環境

会社で使用しているMacなので、かなりハイエンドのものを使用していますが、ここまでのスペックじゃなくても問題なく利用はできると思います。

* MacBook Pro (16-inch, 2019)
  * OS: MacOSX 10.15.7
  * CPU: 2.4 GHz 8コアIntel Core i9
  * メモリ：64 GB 2667 MHz DDR4
* node v12.13.1

## ベースで使用したもの

`react-ts`というテンプレートから作成しています。

```
yarn create @vitejs/app --template react-ts .
```

## やったことまとめ

作成したものは、以下においてありますが、やった内容をまとめておきます。

https://github.com/faithnh/vite-template-for-chrome-extension

### `package.json`の変更

以下の変更を行っています

* `yarn build`時に、`tsc`に`--noEmit`オプションを追加（型検査のみで十分なので)
* パッケージを追加
  * chrome関係のものをtypescriptで利用するのに必要
    * `@types/chrome`
  * ビルドで必要
    * `rollup-plugin-copy`
    * `@types/node`

``` json
{
  "name": "vite_for_chrome_extension",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "react": "^17.0.0",
    "react-dom": "^17.0.0"
  },
  "devDependencies": {
    "rollup-plugin-copy": "3.4.0",
    "@types/node": "16.4.0",
    "@types/chrome": "0.0.148",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@vitejs/plugin-react-refresh": "^1.3.1",
    "typescript": "^4.3.2",
    "vite": "^2.4.3"
  }
}
```

### `vite.config.ts`を変更

主に、やっていることは以下の通り

* `main.js`やbackgroundスクリプトである`background.js`をビルドし、`dist`直下へ出力
* `public`ファイルを`dist/public`へコピー
* `manifest.json`を`dist`へコピー
* `index.html`などchrome拡張機能で利用する`html`ファイルを`dist`へコピー

``` typescript
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import copy from 'rollup-plugin-copy'

const { resolve } = require('path')

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    // src上にあるts, tsxファイルを一つのjsにまとめて、dist配下へ配置
    // ポップアップスクリプト本体であるmain.jsと、バックアップスクリプトbackground.jsのビルド設定を含めている
    // コンテンツスクリプトは含めていないが、必要であればここに追加していけばよい
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'src/main/main.tsx'),
        background: resolve(__dirname, 'src/background/background.ts'),
      },
      output: {
        entryFileNames: '[name].js',
      },
    },
  },

  plugins: [
    reactRefresh(),
    copy({
      verbose: true,
      hook: 'writeBundle',
      targets: [
        // publicファイル(アイコンなど) を dist/public へコピー
        {
          src: 'public/*',
          dest: 'dist/public'
        },
        // manifest.json を distへコピー
        {
          src: 'manifest.json',
          dest: 'dist',
        },
        // index.htmlなどchrome extensionで使用するhtmlファイルをdistへコピー
        {
          src: '*.html',
          dest: 'dist'
        }
      ],
    }),
  ]
})
```

### `src`配下にあるコンポーネントファイルを`src/main`へ移動

`vite-env.d.ts`を除くすべてのファイルを`src/main`へ移動させた。コンポーネント関係なので、分離してわかりやすくしたかっただけに過ぎません。

### `background.ts`を作成

background scriptを作成しておきます。とりあえず土台を作成したかったので、何もいれていません。

``` typescript
export {}
```

### `public`フォルダを作成

ここにはとりあえず、拡張機能で利用するファビコンやロゴなどを入れています

### `index.html`を作成

拡張機能アイコンを押したときに、呼び出す`index.html`をルート直下で作成しています。
ここで`main.js`を呼び出すようにしています。
なお、`main.js`のパスはビルド時にルート直下で作成されることを想定しているので、以下のようにしています。

``` html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="main.js"></script>
  </body>
</html>
```

### `manifest.json`を作成

chrome 拡張機能で必須なファイル `manifest.json` を作成しておきます。ロゴやバックグラウンドスクリプト`background.js`、ポップアップで表示させるファイル`index.html`を指定しています。
コンテンツスクリプトは今回含まれていませんが、`vite.config.js`でビルドできるようにすればすぐに対応できるようにしています。

``` json
{
  "manifest_version": 3,
  "name": "vite_for_chrome_extension",
  "version": "0.0.0.0",
  "action": {
    "name": "test",
    "default_popup":"index.html"
  },
  "web_accessible_resources" : [{
    "resources": [
      "index.html", "main.js"
    ],
    "matches": ["http://*/*", "https://*/*", "*://*/*"],
    "extension_ids": []
  }],
  "icons": {
    "32": "public/logo192.png",
    "64": "public/logo192.png",
    "128": "public/logo192.png",
    "512": "public/logo192.png"
  },
  "background" : {
    "service_worker" : "./background.js"
  },
  "content_scripts" : [],
  "permissions": [
    "tabs",
    "webNavigation",
    "clipboardWrite",
    "clipboardRead",
    "storage",
    "unlimitedStorage",
    "alarms",
    "scripting"
  ],
  "host_permissions" : [
    "http://*/*",
    "https://*/*",
    "*://*/*"
  ]
}
```

ここまで作成すれば、あとは、`yarn build`で拡張機能がビルドできるようになります。

## 結局早くビルドできるのか？

従来のものと比較して、本当に早くビルドできるのか試してみました。

従来のプロジェクトについては、比較のためにこちらを拝借しました

https://github.com/pixochi/create-react-chrome-extension-ts

実際にビルドしてみてかかった時間は以下のとおりです。

* vite： 平均 6.46s (6.81s, 6.37s, 6.21s)
* create-react-app: 平均 5.32s (7.22s, 4.35s, 4.38s)

なんと、`vite`のほうが遅いという残念な結果に。。。

なぜ遅いのか調べてみたところ、`vite build`の前に`tsc`を実行していてこれがビルド時間に足かせになっているようです（実際これを省略してみたところ、平均 2.56s (2.82s, 2.46s, 2.40s)で済み、問題なくビルドできているようです）。

ではなぜ`tsc`をいれているのかというと、[公式マニュアル](https://vitejs.dev/guide/features.html#typescript)によると、`vite`もといビルドに使っている`esbuild`では、あくまでトランスパイルしか行わず、型チェックは行っていないからだそうです。なので、型チェックは`tsc`でカバーしているようです。

一方、拡張機能では毎回ビルドすることになるので、当然型チェックは必要です。なので、`tsc`は外せられないと考えています。

### 規模が大きくなるとどうなるかも検証

何も入れていない状態だと、あまり意味がないのかなと思い、規模が大きくなったら本領が発揮されるという話もあるので、検証してみました。

たとえば、React Material UI ( https://material-ui.com/ ) という比較的重いモジュールを含めてみることにしました。バージョンは`4.12.2`とします。

その結果がこちらです。

* vite： 平均 10.64s (10.66s, 10.53s, 10.74s)
* vite(tscを除外): 平均 4.46s (4.60s, 4.36s, 4.43s)
* create-react-app: 平均 6.82s (6.77s, 6.87s, 6.81s)

これでも`vite`のほうが遅い結果に。。。`tsc`を除外したら早いので、ここでも`tsc`が足かせになっているようでした。

他にモジュールをいれてみたら結果は変わるかもしれませんが、キリがなさそうなので、ここで打ち切ることにしました。

### まとめ

`vite`で土台を作ってみたら早くなるのかなと検証していましたが、従来のものと比較して早くビルドされるわけではありませんでした

`tsc`がビルド時間の足かせになっていることはわかったものの、型チェックは必要なので外せません。なので、chrome拡張機能で採用するにはまだまだ課題がある印象でした。

今後改善してもっと早くなることがわかれば、またトライしてみたいです。