いがにんのぼやき

WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

Vue3のコンポーネントファイルサイズを検証する

Vue2のときはコンポーネントを切りすぎるとファイルサイズが増大すると言われていた
ではVue3になった今、実際どのくらいファイルサイズが変わるのか、検証してみる

まずはインストール

% npm init vite vue3-component-file-size -- --template vue
npx: 6個のパッケージを3.688秒でインストールしました。
✔ Select a framework: › vue
✔ Select a variant: › vue-ts

package.json

{
  "name": "vue3-component-file-size",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.25"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2.3.1",
    "typescript": "^4.5.4",
    "vite": "^2.9.5",
    "vite-plugin-compression": "^0.5.1",
    "vue-tsc": "^0.34.7"
  }
}

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()]
})

このようなファイルが生成される

<template>
  <div>
    <h1>{{ h1 }}</h1>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const h1 = ref('App')
</script>

このVueのコードをエンドポイントのApp.vueとして定義、各コンポーネントをimportすることでファイルサイズの変化を確認する
確認時は npm run build で確認。devビルドではなくminifyされた状態で確認

% npm run build   

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 9 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.530e5712.js   51.28 KiB / gzip: 20.64 KiB

この時点でのファイルサイズは51.28 KiB、gzipで20.64 KiB

npm run preview でサーバーを立ち上げてSafariでファイルサイズを確認するとヘッダーが304 B、本文が21.16 KBとなっている
ビルド時の表示がKiB表記になっているが大体近しい数字になっている。少しだけずれているがまあ誤差の範囲として無視する

このファイルサイズがどのくらい増えるかを確認していく

scriptやstyleのありなしでもファイルサイズが変わるので以下の4つのコンポーネントを作ってみた

templateとscriptのコンポーネント

コンポーネント
TemplateAndScript.vue

<template>
  <div>
    <h2 @click="handleClick">Component{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click ${props.id}`)
}
</script>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndScript :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndScript from './components/TemplateAndScript.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build  

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 10 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.fcfb82ce.js   51.45 KiB / gzip: 20.72 KiB
  • 51.28 KiB → 51.45 KiB = 0.17 KiB = 174.08 byte
  • gzipで20.64 KiB → 20.72KiB = 0.08 KiB = 81.92 byte

templateとscriptとstyleのコンポーネント

コンポーネント
TemplateAndScriptAndStyle.vue

<template>
  <div>
    <h2 :class="$style.title" @click="handleClick">Component{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click ${props.id}`)
}
</script>

<style module>
.title {
  color: red;
}
</style>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndScriptAndStyle :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndScriptAndStyle from './components/TemplateAndScriptAndStyle.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 12 modules transformed.
dist/index.html                  0.42 KiB
dist/assets/index.cdfc0d7e.css   0.03 KiB / gzip: 0.05 KiB
dist/assets/index.d9a18fc7.js    51.64 KiB / gzip: 20.82 KiB
  • 51.28 KiB → 51.64 KiB = 0.36 KiB = 368.64 byte
  • gzipで20.64 KiB → 20.82 KiB = 0.18 KiB = 184.32 byte

ここでindex.htmlにも変化があり、かつCSSファイルが追加された
これはHTMLに <link rel="stylesheet" href="/assets/index.cdfc0d7e.css"> が追加されて、App.vueの描画に必要となるCSSファイルが出力されているだけ
今回の例だとCSSの中身は ._title_y99vv_2{color:red} となっている

templateのみのコンポーネント

コンポーネント
Template.vue

<template>
  <div>
    <h2>Title</h2>
  </div>
</template>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <Template :id="1" />
  </div>
</template>

<script setup lang="ts">
import Template from './components/Template.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 11 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.05c6e5e0.js   51.49 KiB / gzip: 20.73 KiB
  • 51.28 KiB → 51.49 KiB = 0.21 KiB = 215.04 byte
  • gzipで20.64 KiB → 20.73KiB = 0.09 KiB = 92.16 byte

templateとstyleのコンポーネント

コンポーネント
TemplateAndStyle.vue

<template>
  <div>
    <h2 :class="$style.title">Title</h2>
  </div>
</template>

<style module>
.title {
  color: red;
}
</style>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndStyle :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndStyle from './components/TemplateAndStyle.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 12 modules transformed.
dist/index.html                  0.42 KiB
dist/assets/index.cdfc0d7e.css   0.03 KiB / gzip: 0.05 KiB
dist/assets/index.3ee1a737.js    51.58 KiB / gzip: 20.78 KiB
  • 51.28 KiB → 51.58 KiB = 0.3 KiB = 307.2 byte
  • gzipで20.64 KiB → 20.78 KiB = 0.14 KiB = 143.36 byte

結果

コンポーネントを追加した場合、以下のようにファイルサイズが追加される形となった

templateとscript templateとscriptとstyle templateのみ templateとstyle
174.08 byte, gzip: 81.92 byte 368.64 byte, gzip: 184.32 byte 215.04 byte, gzip: 92.16 byte 307.2 byte, gzip: 143.36 byte

こうみると大体1コンポーネントを追加するたびに多い場合、最低360byte(gzipなら180byte)から追加される形になる模様
このファイルサイズなら結構細かくコンポーネントを切っても問題ないように思う
なんかVue2とかはもっとファイルサイズが大きかった気がする

おまけ

Dynamic import

読み込み側をDynamic importに変えてみる

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <div>

    </div>
    <TemplateAndScriptAndStyle v-if="visible" :id="1" />
  </div>
</template>

<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue';

const h1 = ref('App')

const TemplateAndScriptAndStyle = defineAsyncComponent(() => import('./components/TemplateAndScriptAndStyle.vue'))
const visible = ref(false)

function importComponent() {
  visible.value = true
}
</script>
% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 13 modules transformed.
dist/index.html                                      0.36 KiB
dist/assets/TemplateAndScriptAndStyle.5352c308.js    0.41 KiB / gzip: 0.32 KiB
dist/assets/TemplateAndScriptAndStyle.ceb24f11.css   0.03 KiB / gzip: 0.05 KiB
dist/assets/index.78270881.js                        53.41 KiB / gzip: 21.56 KiB
  • 51.28 KiB → 53.41 KiB = 2.13 KiB = 2181.12 byte
  • gzipで20.64 KiB → 21.56 KiB = 0.92 KiB = 942.08 byte

Dynamic importで0.41 KiBのJSファイルと0.03 KiBのCSSが追加される形になった

TSX

TSXで記述した場合にサイズは変わるのか

import { FunctionalComponent } from 'vue'

export const FC: FunctionalComponent<{ id: number }> = (props, ctx) => {
    function handleClick() {
        alert(`click ${props.id}`)
    }
    return <>
            <div onClick={handleClick}>
                <h2>Title{props.id}</h2>
            </div>
        </>
}
<template>
  <div>
    <h1>{{ h1 }}</h1>
    <FC :id="1" />
  </div>
</template>

<script setup lang="ts">
import { FC } from './components/TSX'
import { ref } from 'vue';

const h1 = ref('App')
</script>
% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 10 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.639095d2.js   51.47 KiB / gzip: 20.73 KiB
  • 51.28 KiB → 51.47 KiB = 0.19 KiB = 194.56 byte
  • gzipで20.64 KiB → 20.73 KiB = 0.09 KiB = 92.16 byte

templateとscriptで実装した時とほとんど変わらない。10~20byteだけ変わるが誤差

2つのコンポーネントを読み込む場合

最初の検証はコンポーネントを1つだけ追加する形だった
そこに加えてもう一つコンポーネントを追加した場合のファイルサイズも確認してみる

通常

1つ目のコンポーネント。templateとscriptの時と同じ

<template>
  <div>
    <h2 @click="handleClick">Title{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click ${props.id}`)
}
</script>

2つ目のコンポーネント。1つ目のコンポーネントの文字列を少し変えただけ

<template>
  <div>
    <h2 @click="handleClick">Title2{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click2 ${props.id}`)
}
</script>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndScript :id="1" />
    <TemplateAndScript2 :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndScript from './components/TemplateAndScript.vue';
import TemplateAndScript2 from './components/TemplateAndScript2.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 11 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.07aa95e9.js   51.62 KiB / gzip: 20.73 KiB

1つしか読み込まなかった時と比べると51.45 KiB → 51.62 KiB(gzipで20.72 KiB → 20.73 KiB)とコンポーネントを追加しているのに全然ファイルサイズが増えていない!
つまり最初にコンポーネントを追加した場合数百byte追加されるが、2つ目以降は100~200byteくらいに収まり、しかもgzipだと10byteくらいになる

TSX

import { FunctionalComponent } from 'vue'

export const FC: FunctionalComponent<{ id: number }> = (props, ctx) => {
    function handleClick() {
        alert(`click ${props.id}`)
    }
    return <>
            <div onClick={handleClick}>
                <h2>Title{props.id}</h2>
            </div>
        </>
}

export const FC2: FunctionalComponent<{ id: number }> = (props, ctx) => {
    function handleClick() {
        alert(`click2 ${props.id}`)
    }
    return <>
            <div onClick={handleClick}>
                <h2>Title2{props.id}</h2>
            </div>
        </>
}
<template>
  <div>
    <h1>{{ h1 }}</h1>
    <FC :id="1" />
    <FC2 :id="1" />
  </div>
</template>

<script setup lang="ts">
import { FC, FC2 } from './components/TSX'
import { ref } from 'vue';

const h1 = ref('App')
</script>
% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 10 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.0746aaec.js   51.67 KiB / gzip: 20.75 KiB

TSXもほぼ変わらない

結論

Vueコンポーネントを切りすぎて問題になる、ということはほぼないだろう
これからは設計的に綺麗になるなら小さなコンポーネントも積極的に分割を行っていきたい