vue-i18n のパフォーマンス最適化
この記事はこちらの日本語版です。
TL;DR
-
vue-i18n
で翻訳パフォーマンスを改善するいくつかの方法を提供するよ- 翻訳パフォーマンスは、”コンパイラモジュール > カスタムディレクティブ
v-t
>$t
メソッド ” だよ
はじめに
vue-i18n
は Ruby on Rails の i18n にインスパイアされて開発された Vue.js 向けの国際化プラグインです。Ruby on Rails の I18n.t
と同様な $t
によるシンプルな翻訳の他に、現在では、Pluralization、DateTime/Number、そして Component Interpolation による様々な方法によって Vue アプリケーションの翻訳が可能です。
しかしながら、特に一番利用する $t
おいては、Vue のレンダリングパフォーマンスに影響を与える問題を持っています。それは、Vue コンポーネントにおいて再レンダリングが発生するたびに、毎回翻訳処理を実行してしまうという問題です。このため、パフォーマンスが要求される Vue アプリケーションにおいては良い結果をもたらしません。
vue-i18n
の v7.3 以降では、こうした問題を解決するために、ユーザーによって翻訳パフォーマンスをチューニングできるオプションをサポートします!
カスタムディレクティブ: v-t
Vue が提供するカスタムディレクティブ v-t
を利用して翻訳するオプションを提供します。
使い方
vue-i18n
にデフォルトでビルドインしているので、特に別途インストールする必要はありません。vue-i18n
インストール & Vue.use(VueI18n)
後、すぐに v-t
を利用して翻訳することができます。
以下は、v-t
を利用した翻訳の例です:
- 文字列構文
<!-- 文字列リテラル -->
<p v-t="'hello'"></p><!-- dataまた算出プロパティによるキーパスバインディング -->
<p v-t="path"></p>
- オブジェクト構文
<!-- 純粋なオブジェクトリテラル -->
<p v-t="{ path: 'hello', locale: 'ja', args: { name: 'kazupon' } }"></p><!-- dataまた算出プロパティによるバインディング -->
<p v-t="{ path: path, args: { name: nickName } }"></p>
v-t
詳細については以下のドキュメントを参照してください:
- Custom directive localization
- Directives
v-t
v-t
で上記のようにすると、カスタムディレクティブのバインド (bind
フック関数実行)時に翻訳処理を実行し、その結果を対象となる要素の textContent
に挿入します。
翻訳処理コスト削減の仕組み
$t
による翻訳は Vue 内部で実行される render
関数(テンプレートの場合はコンパイルされた)において実行していました。それに対して v-t
による翻訳は、Virtual DOM の patch においてフッキングされます。このため、翻訳処理の実行タイミングが異なります。
Vue のカスタムディレクティブでは、Vue コンポーネントにおいて再レンダリングが発生すると update
フック関数により何らかの処理が実行されます。v-t
では、$t
と同様にリアクティブな動的データによる翻訳のユースケースがあるため、update
フック関数においても翻訳処理をするようにしています。
Vue のカスタムディレクティブにおいて、update
などのフック可能な関数には、カスタムディレクティブにバインディングされた現在の値(binding.value
) と前の値 (binding.oldValue
) が渡ってくるようになっています。
v-t
ではこれらの値を比較することによって、値が同じなら return してフック関数の処理を途中で抜けます。このようして翻訳コストを減らすことで Vue アプリケーションのパフォーマンスを劣化させないようにしています。
サーバサイドレンダリング (SSR)
v-t
を利用した Vue アプリケーションを SSR に対応するには、それ向けに対応するための別途モジュール vue-i18n-extensions
をインストールする必要があります。
$ npm install --save-dev vue-i18n-extensions
インストール後サーバサイドにおいて、以下のようにレンダラを作成する createRenderer
関数の directives
オプションに指定します:
import { createRenderer } from 'vue-server-renderer'
import { directive as t } from 'vue-i18n-extensions'// something setup ...const renderer = createRenderer({ directives: { t } })// something rendering ...
SSR 向けに提供するカスタムディレクティブ v-t
のドキュメントも公開しているのでそちらを参照してください。
また v-t
の SSR をしている example もあるので実際に動かして試してみるとよいでしょう。😉
コンパイラモジュールによる事前翻訳
Vue が提供するコンパイラのフッキングの仕組みを利用して、カスタムディレクティブ v-t
の事前翻訳を可能にするコンパイラモジュールを提供します。
事前翻訳は、静的な国際化リソースを持つ Vue アプリケーションにおいてパフォーマンスを最大化できるため大変有用です。
使い方
事前翻訳を利用するには、以下のようなコードを実装する必要があります:
// Imports some modules
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { module } from 'vue-i18n-extensions'// Install VueI18n
Vue.use(VueI18n)// Create VueI18n instance
const i18n = new VueI18n({
// ...
})// Wrap with VueI18n instance
const i18nModule = module(i18n)
// Specify to `module` option of Vue compiler
// ...
Vue
と VueI18n
をインポート(ES2015 import
/ CommonJS require
) して Vue.use
で VueI18n
をインストールし、VueI18n
インスタンスを生成します。ここまでは、Vue アプリケーションでの国際化対応と同じです。
vue-i18n-extensions
からコンパイラモジュールを返す module
関数をインポートします。この関数は、Vue コンパイラ内で翻訳するコンパイラモジュールを返します。このため、引数には VueI18n
インスタンスの指定が必要です。
vue-i18n-extensions
からインポートした module
関数に VueI18n インスタンスを指定して実行することで、Vue コンパイラにフッキング可能なコンパイラモジュールを取得することができます。
それを使用して、事前翻訳する方法は現時点では以下の方法があります:
vue-template-compiler
vue-loader
/rollup-plugin-vue
以下は、vue-template-compiler
を利用した例です。
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { compile } from 'vue-template-compiler'
import { module } from 'vue-i18n-extensions'
Vue.use(VueI18n)
const i18n = new VueI18n({
// ...
})
const i18nModule = module(i18n)
const { ast, render } = compile(`<p v-t="'hello'"></p>`, { modules: [i18nModule] })
console.log(ast.i18n) // output -> 'hello'
console.log(render) // output -> `with(this){return _c('p',{domProps:{"textContent":_s("hello")}})}`// something do for `ast`, `render`
// ...
vue-template-compiler
はかなりローレベルであるため、それを利用して Vue アプリケーションを構築するにはかなりの労力を要します。このため、Vue.js 公式に提供するバンドリングツール向けの vue-loader
/ rollup-plugin-vue
を利用するのが一般的です。
vue-loader
を利用した場合は、webpack
の設定は以下のようになります:
const Vue = require('vue')
const VueI18n = require('vue-i18n')
const i18nExtensions = require('vue-i18n-extensions')
Vue.use(VueI18n)
const i18n = new VueI18n({
// ...
})
module.exports = {
module: {
rules: [{
test: /\.vue$/,
loader: 'vue',
options: {
compilerModules: [i18nExtensions.module(i18n)],
// other vue-loader options go here
loaders: {}
}
}]
}
}
vue-loader
は vue-template-compiler
を内部で利用しており、上記のように compilerModules
にコンパイラモジュールを指定することで、ローレベルな部分は vue-loader
が処理してくれます。
vue-loader
と webpack
を利用した Vue アプリケーションの構築は、Vue.js 公式にテンプレートを公開しているため、vue-cli
で簡単に土台をセットアップすることができます。事前翻訳を利用する場合は、こちらを推奨します。
vue-loader
と rollup-plugin-vue
による事前翻訳の example は以下にあるので参照してください。
事前翻訳の仕組み
vue-i18n
がサポートする事前翻訳は、以下の図のような Vue コンパイラのフッキングの仕組みを利用して実現します:
Vue コンパイラは、テンプレートに対して以下の流れでコンパイル処理を行います:
- HTML parser
テンプレートを解析し、AST Node Tree を生成 - Optimizer
生成された AST Node Tree を最適化 - Code Generator
最適化された AST Node Tree を元に render 関数郡 (render
/staticRenderFns
) を生成
上記の処理それぞれのフローにおいて、Vue コンパイラはフッキングをサポートしています。
vue-i18n-extensions
によって提供されるコンパイラモジュールは、フック関数で以下のように処理します。
transformNode
v-t を検出し、ディレクティブの値を取得して、AST Node にアタッチgenData
transformNode
で AST Node に設定されたディレクティブの値を取得して翻訳。翻訳結果をVNodeData
のdomProps
のキーtextContent
に設定。
Vue の Virtual DOM の patch において domProps
が存在する場合、その domProps
のデータが DOM 要素に属性値として適用されるようになっています。
Vue コアでサポートしているディレクティブは様々なものがありますが、この特性を利用したディレクティブとして v-text
があります。
おわりに
$t
の翻訳における Vue コンポーネントのレンダリングパフォーマンス問題を解消するため、翻訳パフォーマンスをチューニングできるオプションを紹介しました。
これらオプションにより、よりハイパフォーマンスな Vue アプリケーションを構築することが可能になります。
今回サポートする翻訳パフォーマンスをチューニングできるオプションについて計測できる example を公開しています。example で以下の gif 動画のように測定することができます。
ぜひ、自分で確認してみましょう!👀