vue-i18n のパフォーマンス最適化

kazuya kawaguchi
12 min readSep 22, 2017

--

この記事はこちらの日本語版です。

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-i18nv7.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
// ...

VueVueI18n をインポート(ES2015 import / CommonJS require) して Vue.useVueI18n をインストールし、VueI18n インスタンスを生成します。ここまでは、Vue アプリケーションでの国際化対応と同じです。

vue-i18n-extensions からコンパイラモジュールを返す module 関数をインポートします。この関数は、Vue コンパイラ内で翻訳するコンパイラモジュールを返します。このため、引数には VueI18n インスタンスの指定が必要です。

vue-i18n-extensions からインポートした module 関数に VueI18n インスタンスを指定して実行することで、Vue コンパイラにフッキング可能なコンパイラモジュールを取得することができます。

それを使用して、事前翻訳する方法は現時点では以下の方法があります:

  1. vue-template-compiler
  2. 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-loadervue-template-compiler を内部で利用しており、上記のように compilerModules にコンパイラモジュールを指定することで、ローレベルな部分は vue-loader が処理してくれます。

vue-loaderwebpack を利用した Vue アプリケーションの構築は、Vue.js 公式にテンプレートを公開しているため、vue-cli で簡単に土台をセットアップすることができます。事前翻訳を利用する場合は、こちらを推奨します。

vue-loaderrollup-plugin-vue による事前翻訳の example は以下にあるので参照してください。

事前翻訳の仕組み

vue-i18n がサポートする事前翻訳は、以下の図のような Vue コンパイラのフッキングの仕組みを利用して実現します:

「Vue.js Extend with Compiler」より

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 に設定されたディレクティブの値を取得して翻訳。翻訳結果を VNodeDatadomProps のキー textContent に設定。

Vue の Virtual DOM の patch において domProps が存在する場合、その domProps のデータが DOM 要素に属性値として適用されるようになっています。

Vue コアでサポートしているディレクティブは様々なものがありますが、この特性を利用したディレクティブとして v-text があります。

vue-i18n の翻訳結果はテキストです。このため、v-text と同じ実装方法によって事前翻訳を実現しています。

おわりに

$t の翻訳における Vue コンポーネントのレンダリングパフォーマンス問題を解消するため、翻訳パフォーマンスをチューニングできるオプションを紹介しました。

これらオプションにより、よりハイパフォーマンスな Vue アプリケーションを構築することが可能になります。

今回サポートする翻訳パフォーマンスをチューニングできるオプションについて計測できる example を公開しています。example で以下の gif 動画のように測定することができます。

パフォーマンス測定の様子

ぜひ、自分で確認してみましょう!👀

--

--

kazuya kawaguchi

nickname kazupon. software engineer & emojineer. vue.js core team member. vuejs-jp users group organizer 😺. wasm love ❤️, i18n enthusiast 🌐.