Vue+Typescript+FirebaseでChrome拡張を作っているのですが、バンドルサイズが大きいよと警告が出たので対応しようと思います。
使っている技術
- Chrome Extension
- Vue+Vuetify
- TypeScript
- Firebase
- Authentication
- Cloud Firestore
ワーニングの内容
npm run build
(cross-env NODE_ENV=production webpack --hide-modules
)で本番用のコードを出力してみたところ、バンドルサイズが推奨サイズ(244 KB)を超えているというワーニングが出ました。
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
/fonts/_/node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot (842 KiB)
/fonts/_/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2 (276 KiB)
/fonts/_/node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff (390 KiB)
/fonts/_/node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf (842 KiB)
options/options.js (578 KiB)
popup/popup.js (1.17 MiB)
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
popup/popup (1.17 MiB)
popup/popup.js
options/options (578 KiB)
options/options.js
バンドル構成を可視化
webpack-bundle-analyzer
をインストールして、
npm install --save-dev webpack-bundle-analyzer
webpack.config.js
にpluginを追加します。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const config = {
plugins: [
new BundleAnalyzerPlugin(),
]
}
あとは、npm run build
を実行するだけで、自動的にブラウザが立ち上がり、バンドルの構成要素を確認できます。
どうやら、firebase
vuetify
@mdi/font
vue
を減らせば良さそうです。
そもそも、バンドルにモジュールが含められるメカニズムは?
バンドルからモジュールを削除するためには、どうやったらバンドルにモジュールが含められるかを知っておきたいですが、 自分が知る範囲で、以下の二つのメカニズムがあるようです。
エントリーポイントから依存関係
webpackはwebpack.config.js
のentry
で指定したエントリーポイントから依存関係を解析して、依存関係にあるモジュールをバンドルに含めるようです。
これをバンドルから除外したい場合、以下のような対応を取ることになります。
- 実際には使ってないケース
- 単純に
import
文やrequire()
文を削除する - こんなのないよと思うかもしれませんが、削除し忘れたimport文や、バンドルに含めるためだけのimport文など、結構ありますw
- 単純に
- 実際に使っているケース
-
webpack.config.js
のexternals
で含めないモジュールを明示的に除外設定する
-
バンドル時に実行されるPlugin
Pluginの中で、バンドルにモジュールを含めるようなことをするものもあります。
その場合、Plugin自体を削除したり、動作を変えたりすることになります。
バンドルから除外
エントリーポイントから依存関係
vue・vuetify・@mdi/font
vue関係の3つのライブラリをCDNから読み込み、バンドルから除外するようにします。
CDNからの読み込み
Chrome Extensionなので、アイコンをクリックした時に呼び出されるpopup.html
がエントリーポイントになるのですが、そこでCDNから vue・vuetify・@mdi/font を読み込むようにしました。
vueとvuetifyはmin.jsファイルが提供されているので、NODE_ENV === production
の場合はそちらを読み込むようにしてあります。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
+ <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
+ <link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.1.45/css/materialdesignicons.min.css" rel="stylesheet">
+ <link href="https://cdn.jsdelivr.net/npm/vuetify@2.2.27/dist/vuetify.min.css" rel="stylesheet">
</head>
<body>
<div style="width: 500px;">
<div id="app">/div>
</div>
+ <% if (NODE_ENV === 'production') { %>
+ <script defer src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
+ <script defer src="https://cdn.jsdelivr.net/npm/vuetify@2.2.27/dist/vuetify.min.js"></script>
+ <% } else { %>
+ <script defer src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script>
+ <script defer src="https://cdn.jsdelivr.net/npm/vuetify@2.2.27/dist/vuetify.js"></script>
+ <% } %>
<script defer src="popup.js"></script>
</body>
</html>
バンドルに含めない設定
vueとvuetifyは実装の中で依存しているので、webpack.config.js
のexternalsでバンドルに含めないように指定します。keyにモジュール名(=node_modulesの下のフォルダ名)、valueにグローバル変数名を指定します。グローバル変数名は一回動かしてみて、Chromeのconsoleで確認しました。
+ externals: {
+ vue: 'Vue',
+ vuetify: 'Vuetify',
+ }
この設定を入れることによって、バンドルファイルpopup.js
上は以下のように、node_modulesの下のソースコードではなく、単純にグローバル変数をモジュールとして定義してくれるようになります。
[function(e, r) {
e.exports = Vue
}
, function(e, r) {
e.exports = Vuetify
}
, function(e, r) {
(中略)
]
ライブラリ利用箇所の修正
vuetifyの初期化を行なっているvuetify.ts
の修正を行います。
- node_modulesの
vuetify/lib
ではなく、externalsで指定したvuetify
に変更 -
バンドルに含めるためにimportしていた
@mdi/font
を削除import Vue from 'vue' -import Vuetify from 'vuetify/lib' +import Vuetify from 'vuetify' -import '@mdi/font/css/materialdesignicons.css' Vue.use(Vuetify) export default new Vuetify({ icons: { iconfont: 'mdi', }, })
開発時も参照しないライブラリを削除
vue
やvuetify
は開発時に参照しているので削除できませんが、@mdi/font
はもともとバンドルに含めるためだけにinstallしてたので削除します。
npm uninstall @mdi/font --save
Chrome拡張で外部スクリプトを読み込めるようにする
普通のアプリではこの設定は不要ですが、Chrome拡張の場合はアプリ内で不正なjavascriptコードが実行されないように、content_security_policyによって実行可能なjavascriptコードを制限する仕組みがあるので、この設定をmanifest.json
に記載します。
- "content_security_policy": "script-src 'self' ; object-src 'self'"
+ "content_security_policy": "script-src 'self' https://cdn.jsdelivr.net/npm/vue@2.6.11/ https://cdn.jsdelivr.net/npm/vuetify@2.2.27/; object-src 'self'"
ちなみに、vue-web-extension
を使っている場合は、webpack.config.js
でこの設定を上書きするようになっているので、そちらを修正します。
firebase
firebaseも同様にCDNから読み込み、バンドルから除外していきます。
CDNからの読み込み
こちらもpopup.html
を修正します。min.jsが存在しないっぽいので、こちらを参考にそのまま読み込みます。ちなみにdefer
をつけると非同期でスクリプトを読み込み、DOMContentLoadedの直前でスクリプトを実行してくれるらしいのですが、こちらの方がChromeのperformanceでチェックしたところ少しだけ早く初期画面が表示されるようでしたので、defer
を設定してあります。
+ <script defer src="https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js"></script>
+ <script defer src="https://www.gstatic.com/firebasejs/7.14.2/firebase-auth.js"></script>
+ <script defer src="https://www.gstatic.com/firebasejs/7.14.2/firebase-firestore.js"></script>
<script defer src="popup.js"></script>
</body>
バンドルに含めない設定
webpack.config.jsのexternalsを修正します。探すといろんな記事でいろんな指定方法が紹介されてますが、結局以下の設定で問題なく除外できました。
externals: {
vue: 'Vue',
vuetify: 'Vuetify',
+ firebase: 'firebase',
}
- トップレベルのモジュール名(@はユーザ名を表すnamespeceであることを示しているだけなので不要)を指定する
- firebase/authやfirebase/firestoreは、firebaseオブジェクトを通じて呼び出すので個別設定は不要
- グローバル変数名は
firebase
を指定する
ライブラリ利用箇所の修正
もともと、node_modulesの下のfirebase/app
をimportしてましたが、externalsで指定したfirebase
に変更します。
その上で、firebase/auth
とfirebase/firestore
はグローバル変数のfirebase
オブジェクトを利用することになるので、ここのimport文は削除します。
-import * as firebase from 'firebase/app'
+import * as firebase from 'firebase'
-import 'firebase/auth'
-import 'firebase/firestore'
const firebaseConfig: { [key: string]: string } = {
}
firebase.initializeApp(firebaseConfig)
export default firebase
Chrome拡張で外部スクリプトを読み込めるようにする
こちらもChrome拡張で上記のスクリプトを実行できるように、content_security_poricyの設定をmanifest.json
に記載します。ちなみに、指定したURLの最後の/
は必要です。
- "content_security_policy": "script-src 'self' https://cdn.jsdelivr.net/npm/vue@2.6.11/ https://cdn.jsdelivr.net/npm/vuetify@2.2.27/; object-src 'self'"
+ "content_security_policy": "script-src 'self' https://www.gstatic.com/firebasejs/ https://cdn.jsdelivr.net/npm/vue@2.6.11/ https://cdn.jsdelivr.net/npm/vuetify@2.2.27/; object-src 'self'"
結果確認
ここまでの対応で、エントリーポイントからの依存関係については一通り除外できたと思いますので、npm run build
で本番用のコードを出力してみました。すると、バンドルサイズが推奨サイズ(244 KB)を超えているというワーニングは出なくなりました。
options/options.js (48.3 KiB)
popup/popup.js (145 KiB)
になったので、まあ良さそうです。
で、バンドルの構成は以下のようになりました。
よく見ると、vuetifyのcomponentが残っています。たぶんこれはvuetify-loader
のVuetifyLoaderPlugin
を使っているためと考えられますので、次はこれを対応しようと思います。
バンドル時に実行されるPlugin
VuetifyLoaderPlugin
このプラグインはvuetifyのUIコンポーネントを利用する箇所を自動的に検知して、自動的にUIコンポーネントのimportを行ってくれるPluginでかなり便利なので導入していましたが、CDN化した後はそもそもこのimportが不要になります。
ちなみに、importの処理自体は、loader.js
の中の以下のあたりで実行されているようです。
function install (install, content, imports) {
if (imports.length) {
let newContent = '/* vuetify-loader */\n'
newContent += `import ${install} from ${loaderUtils.stringifyRequest(this, '!' + runtimePaths[install])}\n`
newContent += imports.map(i => i[1]).join('\n') + '\n'
newContent += `${install}(component, {${imports.map(i => i[0]).join(',')}})\n`
// Insert our modification before the HMR code
const hotReload = content.indexOf('/* hot reload */')
if (hotReload > -1) {
content = content.slice(0, hotReload) + newContent + '\n\n' + content.slice(hotReload)
} else {
content += '\n\n' + newContent
}
}
return content
}
Pluginの削除
なので、単純にwebpack.config.js
から該当のPluginを削除します。
const { version } = require('./package.json');
-const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
(中略)
plugins: [
new BundleAnalyzerPlugin(),
- new VuetifyLoaderPlugin(),
さらに、package.jsonからも削除します。
npm uninstall vuetify-loader --save
結果確認
改めて、npm run build
でバンドルファイルを出力しなおすと、さらにバンドルサイズが削減できていました。
options/options.js (16.5 KiB)
popup/popup.js (21.2 KiB)
バンドルの構成はこんな感じです。
vuetifyのUI コンポーネント関係もなくなりました。
これにて対応完了!!!