メインコンテンツまでスキップ
Version: Next

最適化とベストプラクティス

neq_assign

親コンポーネントから props を受け取った際、changeメソッドが呼ばれます。 これはコンポーネントの状態を更新することができるのに加え、コンポーネントが props が変わった際に再レンダリングするかどうかを決める ShouldRenderという真偽値を返すことができます。

再レンダリングはコストがかかるもので、もし避けられるのであれば避けるべきです。 一般的なルールとして props が実際に変化した際にのみ再レンダリングすれば良いでしょう。 以下のコードブロックはこのルールを表しており、props が前と変わったときにtrueを返します。

use yew::ShouldRender;

#[derive(PartialEq)]
struct ExampleProps;

struct Example {
props: ExampleProps,
};

impl Example {
fn change(&mut self, props: ExampleProps) -> ShouldRender {
if self.props != props {
self.props = props;
true
} else {
false
}
}
}

しかし我々は先に進んでいけます! この 6 行のボイラープレードはPartialEqを実装したものにトレイトとブランケットを用いることで 1 行のコードへと落とし込むことができます。 こちらにてyewtilクレートのNewAssignトレイトを見てみてください。

効果的にスマートポインタを使う

注意: このセクションで使われている用語がわからなければ Rust book は スマートポインタについての章 があり、非常に有用です。

再レンダリングの際に props を作るデータを大量にコピーしないために、スマートポインタを用いてデータ自体ではなくデータへの参照だけを コピーできます。 props や子コンポーネントで関連するデータに実データではなく参照を渡すと、子コンポーネントでデータを変更する必要がなければ データのコピーを避けることができます。 その際、Rc::make_mutによって変更したいデータの変更可能な参照を得ることができます。

これにより、props が変更されたときにコンポーネントが再レンダリングされるかどうかを決めるかでComponent::changeに更なる恩恵があります。 なぜなら、データの値を比較する代わりに元々のポインタのアドレス (つまりデータが保管されている機械のメモリの場所) を比較できるためです。 2 つのポインターが同じデータを指す場合、それらのデータの値は同じでなければならないのです。 ただし、その逆は必ずしも成り立たないことに注意してください! もし 2 つのポインタが異なるのであれば、そのデータは同じである可能性があります。 この場合はデータを比較するべきでしょう。

この比較においてはPartialEqではなくRc::ptr_eqを使う必要があります。 PartialEqは等価演算子==を使う際に自動的に使われます。 Rust のドキュメントにはRc::ptr_eqについてより詳しく書いてあります

この最適化はCopyを実装していないデータの型に対して極めて有効なものです。 もしデータを簡単に変更できるのであれば、スマートポインタに取り換える必要はありません。 しかしVecHashMapStringなどのような重たいデータの構造体に対してはスマートポインタを使うことで パフォーマンスを改善することができるでしょう。

この最適化は値がまだ一度も子によって更新されていない場合に極めて有効で、親からほとんど更新されない場合においてもかなり有効です。 これにより、Rc<_>sが純粋なコンポーネントに対してプロパティの値をラップする良い一手となります。

View 関数

コードの可読性の理由からhtml!の部分を関数へと移植するのは意味があります。 これは、インデントを減らすのでコードを読みやすくするだけでなく、良いデザインパターンを産むことにも繋がるのです。 これらの関数は複数箇所で呼ばれて書くべきコード量を減らせるため、分解可能なアプリケーションを作ることができるのです。

純粋なコンポーネント

純粋なコンポーネントは状態を変化せず、ただ中身を表示してメッセージを普通の変更可能なコンポーネントへ渡すコンポーネントのことです。 View 関数との違いとして、純粋なコンポーネントは式の構文({some_view_function()})ではなく コンポーネントの構文(<SomePureComponent />)を使うことでhtml!マクロの中で呼ばれる点、 そして実装次第で記憶され (つまり、一度関数が呼ばれれば値は"保存"され、 同じ引数でもう一度呼ばれても値を再計算する必要がなく最初に関数が呼ばれたときの保存された値を返すことができる)、 先述のneq_assignロジックを使う別々の props で再レンダリングを避けられる点があります。

Yew は純粋な関数やコンポーネントをサポートしていませんが、外部のクレートを用いることで実現できます。

関数型コンポーネント (a.k.a フック)

関数型コンポーネントはまだ開発中です! 開発状況についてはプロジェクトボードに詳しく書いてあります。

キー付き DOM ノード

ワークスペースでコンパイル時間を減らす

間違いなく Yew を使う上での最大の欠点はコンパイルに時間がかかる点です。 プロジェクトのコンパイルにかかる時間はhtml!マクロに渡されるコードの量に関係しています。 これは小さなプロジェクトにはそこまで問題ないようですが、大きなアプリではコードを複数クレートに分割することでアプリに変更が加られた際に コンパイラの作業量を減らすのが有効です。

一つ可能なやり方として、ルーティングとページ洗濯を担当するメインのクレートを作り、それぞれのページに対して別のクレートを作ることです。 そうして各ページは異なるコンポーネントか、Htmlを生成する大きな関数となります。 アプリの異なる部分を含むクレート同士で共有されるコードはプロジェクト全体で依存する分離したクレートに保存されます。 理想的には 1 回のコンパイルでコード全てを再ビルドせずメインのクレートかどれかのページのクレートを再ビルドするだけにすることです。 最悪なのは、"共通"のクレートを編集して、はじめに戻ってくることです: 共有のクレートに依存している全てのコード、恐らく全てのコードをコンパイルすることです。

もしメインのクレートが重たすぎる、もしくは深くネストしたページ (例えば別ページのトップでレンダリングされるページ) で速く繰り返したい場合、クレートの例を用いてメインページの実装をシンプルにしたりトップで動かしているコンポーネントをレンダリングできます。

バイナリサイズを小さくする

  • Rust のコードを最適化する
  • cargo.toml ( release profile を定義 )
  • wasm-optを用いて wasm のコードを最適化する

注意: バイナリサイズを小さくするのについてはRust Wasm Bookに詳しく書いてあります。

Cargo.toml

Cargo.toml[profile.release]のセクションに設定を書き込むことでリリースビルドを小さくすることが可能です。

[profile.release]
# バイナリに含むコードを少なくする
panic = 'abort'
# コードベース全体での最適化 ( 良い最適化だがビルドが遅くなる)
codegen-units = 1
# サイズの最適化( よりアグレッシブに )
opt-level = 'z'
# サイズの最適化
# opt-level = 's'
# プログラム全体の分析によるリンク時最適化
lto = true

wasm-opt

更にwasmのコードのサイズを最適化することができます。

The Rust Wasm Book には Wasm バイナリのサイズを小さくすることについてのセクションがあります: Shrinking .wasm size

  • wasm-packでデフォルトのwasmのコードをリリースビルド時に最適化する
  • wasm-optによって直接wasmファイルを最適化する
wasm-opt wasm_bg.wasm -Os -o wasm_bg_opt.wasm

yew/examples/にある例を小さなサイズでビルドする

注意: wasm-packは Rust と Wasm のコードへの最適化を組み合わせます。wasm-bindgenはこの例では Rust のサイズ最適化を用いていません。

使用したツールサイズ
wasm-bindgen158KB
wasm-bindgen + wasm-opt -Os116KB
wasm-pack99 KB

参考文献: