パフォーマンスに関する考慮事項と提案
タイミングに関する考慮事項
アプリケーション開発者は通常、レンダリングエンジンが 1 秒あたり 60 フレームという安定したリフレッシュレートを達成できるよう努めます。 お使いのハードウェアや要件によってこの数値は異なる場合がありますが、60 FPS は非常に一般的です。60 FPS とは、各フレームの間に約 16 ミリ秒の処理時間が確保されていることを意味します。この処理時間には、描画プリミティブをグラフィックスハードウェアにアップロードするために必要な処理も含まれます。
実際には、これはアプリケーション開発者が次のことを行うべきであることを意味します。
- 可能な限り非同期かつイベント駆動型のプログラミングを採用する
- 大規模な処理にはワーカースレッドを使用する
- イベントループを手動でスピンさせてはならない
- ブロックする関数内では、1フレームあたり数ミリ秒以上を費やさない
これらを守らないと、フレームがスキップされ、ユーザー体験に深刻な影響を及ぼします。
注: QML から呼び出される C++ などのバックエンドコードブロック内でのブロッキングを回避するために、独自の `QEventLoop ` を作成したり、`QCoreApplication::processEvents()` を呼び出したりすることは 、魅力的に思えるかもしれませんが、決して使用してはなりません。 これは危険な行為です。なぜなら、シグナルハンドラやバインディングでイベントループに入ると、QML エンジンは他のバインディング、アニメーション、トランジションなどを実行し続けるからです。その結果、それらのバインディングによって、たとえばイベントループを含む階層構造が破壊されるといった副作用が生じる可能性があります。
プロファイリング
最も重要なヒントは、 QML ProfilerQt Creator に含まれているもの(またはQt Extension for Visual Studio Code のトレースビューア)を使用することです。アプリケーション内でどこに時間が費やされているかを把握することで、潜在的に存在する問題領域ではなく、実際に存在する問題領域に焦点を当てることができます。詳細については、Qt Creator: QML アプリケーションのプロファイリングおよびQt Extension for Visual Studio Code: QML コードのプロファイリングを参照してください。
どのバインディングが最も頻繁に実行されているか、あるいはアプリケーションのどの関数に最も多くの時間が費やされているかを特定することで、問題のある箇所を最適化すべきか、あるいはパフォーマンスを向上させるためにアプリケーションの実装詳細の一部を再設計すべきかを判断できるようになります。プロファイリングを行わずにコードの最適化を試みても、パフォーマンスの大幅な向上にはつながりにくく、ごくわずかな改善にとどまる可能性が高いです。
JavaScript コード
ほとんどの QML アプリケーションには、プロパティバインディング式、関数、シグナルハンドラの形で、何らかの JavaScript コードが含まれています。これは一般的に問題にはなりません。Qt Quick コンパイラなどの高度なツールのおかげで、単純な関数やバインディングは非常に高速に実行されます。ただし、不必要な処理が誤ってトリガーされないよう注意を払う必要があります。 QML Profiler は、JavaScriptの実行状況や、それが何によってトリガーされたかについて、詳細な情報を表示できます。
型変換
JavaScript を使用する際の大きなコストの一つは、QML 型のプロパティにアクセスする際に、基となる C++ データ(またはその参照)を含む外部リソースを持つ JavaScript オブジェクトが作成される場合があることです。ほとんどの場合、このコストはそれほど高くありませんが、状況によってはかなりの負荷となることもあります。 大規模で複雑な値型やシーケンス型を扱う際は注意が必要です。これらをその場で変更したり、別のプロパティに代入したりするたびに、QMLエンジンによってコピーされる必要があります。これがボトルネックになる場合は、代わりにオブジェクト型の使用を検討してください。 オブジェクト型のリストは、QQmlListProperty を使用して実装されているため、値型のリストとは異なる問題を抱えていません。
単純な値型間の変換のほとんどは低コストです。ただし、例外もあります。文字列からURLを作成する場合、QUrl インスタンスの生成が必要になることがあり、これはコストがかかります。
プロパティの解決
プロパティの解決には時間がかかります。通常、検索処理は後続の実行でははるかに高速になるよう最適化されていますが、可能であれば、不必要な処理を最初から避けるのが常に最善です。
次の例では、頻繁に実行されるコードブロック(この場合は明示的なループの内容ですが、例えば頻繁に評価されるバインディング式などでも同様です)があり、その中で ID が「rect」のオブジェクトとその「color」プロパティの解決を複数回行っています:
// bad.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which: string, value: real) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
printValue("red", rect.color.r);
printValue("green", rect.color.g);
printValue("blue", rect.color.b);
printValue("alpha", rect.color.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
}rect.color が取得されるたびに、QML エンジンは以下の処理を行わなければなりません:
これを4回も実行する必要はありません。代わりに、ブロック内で共通の基底部分を1回だけ解決すれば済みます:
// good.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which: string, value: real) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
var rectColor = rect.color; // resolve the common base.
printValue("red", rectColor.r);
printValue("green", rectColor.g);
printValue("blue", rectColor.b);
printValue("alpha", rectColor.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
}この単純な変更だけで、パフォーマンスが大幅に向上します。なお、ループ処理中に参照されるプロパティは決して変化しないため、上記のコードはさらに改善可能です。具体的には、次のようにプロパティの解決処理をループの外側に持ち出すことで実現できます:
// better.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which: string, value: real) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
var rectColor = rect.color; // resolve the common base outside the tight loop.
for (var i = 0; i < 1000; ++i) {
printValue("red", rectColor.r);
printValue("green", rectColor.g);
printValue("blue", rectColor.b);
printValue("alpha", rectColor.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
}プロパティバインディング
プロパティバインディング式は、参照しているプロパティのいずれかが変更されると再評価されます。そのため、バインディング式は可能な限りシンプルに保つ必要があります。
何らかの処理を行うループがあり、その処理の最終結果のみが重要である場合、蓄積の中間段階でバインディング式の再評価が引き起こされるのを避けるために、プロパティ自体を逐次更新するのではなく、一時的な累積変数を更新し、その後、更新が必要なプロパティにその値を代入する方が、多くの場合、より良い方法です。
以下の作り話のような例が、この点を説明しています:
// bad.qml
import QtQuick
Item {
id: root
width: 200
height: 200
property int accumulatedValue: 0
Text {
anchors.fill: parent
text: root.accumulatedValue.toString()
onTextChanged: console.log("text binding re-evaluated")
}
Component.onCompleted: {
var someData = [ 1, 2, 3, 4, 5, 20 ];
for (var i = 0; i < someData.length; ++i) {
accumulatedValue = accumulatedValue + someData[i];
}
}
}onCompletedハンドラ内のループにより、「text」プロパティのバインディングが6回再評価されます(その結果、textの値に依存する他のプロパティのバインディングやonTextChangedシグナルハンドラも毎回再評価され、表示用のテキストが毎回レイアウトされます)。 このケースでは、実際には累積の最終値のみが重要であるため、これは明らかに不要です。
次のように書き換えることができます:
// good.qml
import QtQuick
Item {
id: root
width: 200
height: 200
property int accumulatedValue: 0
Text {
anchors.fill: parent
text: root.accumulatedValue.toString()
onTextChanged: console.log("text binding re-evaluated")
}
Component.onCompleted: {
var someData = [ 1, 2, 3, 4, 5, 20 ];
var temp = accumulatedValue;
for (var i = 0; i < someData.length; ++i) {
temp = temp + someData[i];
}
accumulatedValue = temp;
}
}シーケンスに関するヒント
前述のように、値型のシーケンスは慎重に扱う必要があります。
まず、シーケンス型は以下の2つの異なるシナリオで異なる挙動を示します:
- シーケンスがQObject のQ_PROPERTY である場合(これを参照シーケンスと呼ぶ)、
- シーケンスがQObject のQ_INVOKABLE 関数から返される場合(これを「コピーシーケンス」と呼びます)。
参照シーケンスは、JavaScriptコード内または元のオブジェクトのいずれかで変更があるたびに、QMetaObject を介して読み書きされます。最適化の一環として、参照シーケンス(および参照型)は遅延読み込みされる場合があります。 実際のコンテンツは、それらが初めて使用される際にのみ取得されます。つまり、JavaScript からシーケンス内の要素の値を変更すると、次のような処理が行われます:
- QObject からコンテンツが読み込まれる可能性があります(遅延読み込みされている場合)。
- そのシーケンス内の指定されたインデックスにある要素を変更する。
- シーケンス全体をQObject に書き戻す。
コピーシーケンスは、実際のシーケンスが JavaScript オブジェクトのリソースデータに格納されているため、読み取り/変更/書き込みのサイクルが発生せず(代わりにリソースデータが直接変更される)、はるかに単純です。
したがって、参照シーケンスの要素への書き込みは、コピーシーケンスの要素への書き込みよりもはるかに遅くなります。 実際、N 要素の参照シーケンスの 1 つの要素への書き込みは、その参照シーケンスに N 要素のコピーシーケンスを代入することと同等のコストがかかるため、通常、計算中は一時的なコピーシーケンスを変更し、その結果を参照シーケンスに代入するほうが効率的です。
以下の C++ 型が存在し(かつ「Qt.example」名前空間に事前に登録されている)と仮定します:
class SequenceTypeExample : public QQuickItem
{
Q_OBJECT
Q_PROPERTY (QList<qreal> qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged)
public:
SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; }
~SequenceTypeExample() {}
QList<qreal> qrealListProperty() const { return m_list; }
void setQrealListProperty(const QList<qreal> &list) { m_list = list; emit qrealListPropertyChanged(); }
signals:
void qrealListPropertyChanged();
private:
QList<qreal> m_list;
};次の例では、タイトなループ内で参照シーケンスの要素への書き込みが行われており、パフォーマンスが著しく低下しています:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
qrealListProperty.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
qrealListProperty[j] = j;
}
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}"qrealListProperty[j] = j" という式によって引き起こされる内側のループ内でのQObject プロパティの読み取りと書き込みが、このコードのパフォーマンスを著しく低下させています。代わりに、機能的には同等でありながらはるかに高速な方法は次のようになります:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
var someData = [1.1, 2.2, 3.3]
someData.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
someData[j] = j;
}
qrealListProperty = someData;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}避けるべきもう一つの一般的なパターンは、各要素を読み取り、変更し、シーケンスのプロパティに書き戻す「読み取り-変更-書き込み」ループです。前の例と同様に、これにより各反復でQObject プロパティの読み取りと書き込みが発生します:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
qrealListProperty.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
qrealListProperty[j] = qrealListProperty[j] * 2;
}
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}代わりに、シーケンスの手動コピーを作成し、そのコピーを変更してから、結果をプロパティに割り当てるようにします:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 500; ++i) {
let data = [...qrealListProperty];
for (var j = 0; j < 100; ++j) {
data[j] = data[j] * 2;
}
qrealListProperty = data;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}第二に、シーケンス内のいずれかの要素が変更されると、そのプロパティに対して変更シグナルが発信されます。 シーケンスプロパティ内の特定の要素に対して多数のバインディングがある場合は、その要素にバインドされた動的プロパティを作成し、バインディング式内のシンボルとしてシーケンス要素の代わりにその動的プロパティを使用することをお勧めします。そうすることで、その値が変更された場合にのみバインディングの再評価が行われるようになります。
これは、ほとんどのクライアントが遭遇することのない珍しいユースケースですが、次のような処理を行う場合に備えて、知っておく価値があります:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
property int firstBinding: qrealListProperty[1] + 10;
property int secondBinding: qrealListProperty[1] + 20;
property int thirdBinding: qrealListProperty[1] + 30;
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
qrealListProperty[2] = i;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}ループ内でインデックス 2 の要素のみが変更される場合でも、変更シグナルの粒度はプロパティ全体が変更されたというものであるため、3 つのバインディングすべてが再評価されることに注意してください。そのため、中間バインディングを追加することが有益な場合もあります:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
property int intermediateBinding: qrealListProperty[1]
property int firstBinding: intermediateBinding + 10;
property int secondBinding: intermediateBinding + 20;
property int thirdBinding: intermediateBinding + 30;
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
qrealListProperty[2] = i;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}上記の例では、毎回中間バインディングのみが再評価されるため、パフォーマンスが大幅に向上します。
値型に関するヒント
値型プロパティ(font、color、vector3d など)は、シーケンス型プロパティと同様の「QObject 」プロパティおよび変更通知のセマンティクスを持っています。したがって、シーケンス型について上記で示したヒントは、値型プロパティにも適用されます。 通常、値型ではこの問題はそれほど深刻ではありません(値型のサブプロパティの数は、シーケンスの要素数よりもはるかに少ないため)が、不必要に再評価されるバインディングの数が増加すると、パフォーマンスに悪影響を及ぼします。
一般的なパフォーマンスに関するヒント
言語設計に起因する一般的な JavaScript のパフォーマンスに関する考慮事項は、QML にも当てはまります。特に顕著なのは以下の通りです:
- 可能な限り eval() の使用を避ける
- オブジェクトのプロパティを削除しない
一般的なインターフェース要素
テキスト要素
テキストレイアウトの計算には時間がかかる場合があります。 可能な限り、StyledText の代わりにPlainText 形式を使用することを検討してください。これにより、レイアウトエンジンにかかる負荷を軽減できます。PlainText を使用できない場合(画像を埋め込む必要がある場合や、テキスト全体ではなく特定の文字範囲に書式(太字、斜体など)を適用するためにタグを使用する必要がある場合など)は、StyledText を使用してください。
AutoText を使用するのは、テキストが (おそらくそうではないでしょうが)StyledText である可能性がある場合に限るべきです。このモードでは解析コストが発生するためです。RichText モードは使用すべきではありません。StyledText であれば、そのコストのほんの一部で、 モードの機能のほぼすべてを実現できるからです。
画像
画像は、あらゆるユーザーインターフェースにおいて不可欠な要素です。しかし残念ながら、読み込みに時間がかかること、消費するメモリ量、そして使用方法といった理由から、大きな問題の原因にもなっています。
非同期読み込み
画像は容量が大きいことが多いため、画像の読み込みによってUIスレッドがブロックされないようにすることが賢明です。 QMLのImage要素の「asynchronous」プロパティをtrue に設定することで、ユーザーインターフェースの見栄えに悪影響を及ぼさない範囲で、ローカルファイルシステムからの画像の非同期読み込みを有効にできます(リモート画像は常に非同期で読み込まれます)。
「asynchronous」プロパティがtrue に設定されているImage要素は、低優先度のワーカースレッドで画像を読み込みます。
明示的なソースサイズ
アプリケーションで大きな画像を読み込むものの、それを小さな要素内に表示する場合は、「sourceSize」プロパティをレンダリングされる要素のサイズに設定してください。これにより、大きな画像ではなく、縮小されたバージョンの画像がメモリ内に保持されるようになります。
sourceSizeを変更すると、画像が再読み込みされる点に注意してください。
実行時の合成は避ける
また、アプリケーションに事前に合成済みの画像リソースを用意することで(たとえば、影効果を適用した要素を用意するなど)、実行時の合成処理を回避できることも覚えておいてください。
画像のスムージングは避ける
image.smooth は、必要な場合にのみ有効にしてください。一部のハードウェアでは処理速度が低下するほか、画像が本来のサイズで表示される場合は視覚的な効果もありません。
描画
同じ領域を何度も描画することは避けてください。背景を何度も描画しないように、Rectangle ではなく Item をルート要素として使用してください。
アンカーを使用した要素の配置
アイテム同士を相対的に配置するには、バインディングよりもアンカーを使用する方が効率的です。バインディングを使用して rect2 を rect1 に対して配置する次の例を考えてみてください:
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
x: rect1.x
y: rect1.y + rect1.height
width: rect1.width - 20
height: 200
}アンカーを使用すると、これよりも効率的に実現できます:
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
height: 200
anchors.left: rect1.left
anchors.top: rect1.bottom
anchors.right: rect1.right
anchors.rightMargin: 20
}バインディングを用いた位置指定(アンカーを使用するのではなく、ビジュアルオブジェクトの x、y、width、height プロパティにバインディング式を割り当てる方法)は、最大限の柔軟性を確保できる反面、処理が比較的遅くなります。
レイアウトが動的でない場合、レイアウトを指定する最もパフォーマンスの高い方法は、x、y、width、height プロパティを静的に初期化することです。 項目の座標は常に親を基準としているため、親の座標 (0, 0) から固定のオフセット位置に配置したい場合は、アンカーを使用すべきではありません。次の例では、子 Rectangle オブジェクトは同じ位置にありますが、示されているアンカーを使用したコードは、静的初期化による固定配置を使用するコードほどリソース効率が良くありません。
Rectangle {
width: 60
height: 60
Rectangle {
id: fixedPositioning
x: 20
y: 20
width: 20
height: 20
}
Rectangle {
id: anchorPositioning
anchors.fill: parent
anchors.margins: 20
}
}モデルとビュー
ほとんどのアプリケーションには、ビューにデータを供給するモデルが少なくとも1つ存在します。最高のパフォーマンスを実現するためには、アプリケーション開発者が知っておくべきいくつかのセマンティクスがあります。
カスタム C++ モデル
QMLのビューで使用するために、C++などのバックエンド言語で独自のカスタムモデルを作成することが望ましい場合がよくあります。このようなモデルの最適な実装は、そのモデルが満たすべきユースケースに大きく依存しますが、一般的なガイドラインは以下の通りです:
- 可能な限り非同期処理を心がける
- すべての処理を(低優先度の)ワーカースレッドで行う
- (処理に時間がかかる可能性のある)I/OやIPCを最小限に抑えるよう、バックエンド操作をバッチ処理する
GUIスレッドがリソース不足に陥るリスク(その結果、体感パフォーマンスの低下を招く恐れがある)を最小限に抑えるため、低優先度のワーカースレッドを使用することが推奨される点に留意することが重要です。また、同期やロックの仕組みがパフォーマンス低下の大きな原因となり得るため、不必要なロックは避けるよう注意する必要があります。
ListModel QML タイプ
Qt Qml Models モジュールには、ListView にデータを供給するために使用できるListModel 型が用意されています。これは迅速なプロトタイピングには有用ですが、大量のデータには適していません。必要に応じて、適切なQAbstractItemModel を使用してください。
ワーカースレッド内でのデータ設定
ListModel JavaScript では、要素を(低優先度の)ワーカースレッド内で設定することができます。変更内容をメインスレッドに同期させるには、開発者はWorkerScript 内からListModel に対してsync() を明示的に呼び出す必要があります。詳細については、WorkerScript のドキュメントを参照してください。
WorkerScript 要素を使用すると、JavaScriptエンジンがスレッドごとに作成されるため、別のJavaScriptエンジンが生成されることに注意してください。これにより、メモリ使用量が増加します。 ただし、複数の `WorkerScript ` 要素はすべて同じワーカースレッドを使用するため、アプリケーションがすでに 1 つの `WorkerScript ` 要素を使用している場合、2 つ目や 3 つ目の ` ` 要素を使用しても、メモリへの影響はごくわずかです。その一方で、追加されたワーカースクリプトは並行して実行されません。
動的なロールの使用は避ける
ListModel 要素は、最適化の目的上、特定のモデル内の各要素におけるロールのタイプが安定していることを前提としています。要素ごとにタイプが動的に変化する場合、モデルのパフォーマンスは大幅に低下します。
そのため、動的型付けはデフォルトで無効になっています。動的型付けを有効にするには(それに伴うパフォーマンスの低下を承知の上で)、開発者がモデルのブール型プロパティ `dynamicRoles ` を明示的に設定する必要があります。絶対に必要な場合を除き、動的型付けを使用しないことをお勧めします。
ビュー
ビューデリゲートは、できるだけシンプルに保つべきです。デリゲート内のQMLは、必要な情報を表示するのに必要な分だけに留めてください。すぐには必要とされない追加機能(たとえば、クリック時にさらに多くの情報を表示するなど)は、必要になるまで実装すべきではありません(後述の「遅延初期化」のセクションを参照してください)。
以下のリストは、デリゲートを設計する際に留意すべき点をまとめたものです:
- デリゲート内の要素が少ないほど、その生成が速くなり、ひいてはビューのスクロールも速くなります。
- デリゲート内のバインディングの数は最小限に抑えてください。特に、デリゲート内での相対的な配置には、バインディングではなくアンカーを使用してください。
- デリゲート内でのShaderEffect 要素の使用は避けてください。
- デリゲートでクリッピングを有効にしないでください。
ビューのcacheBuffer プロパティを設定することで、表示領域外でのデリゲートの非同期作成およびバッファリングを許可することができます。cacheBuffer の利用は、複雑で1フレーム以内に作成される可能性が低いビューデリゲートに推奨されます。
cacheBuffer を設定すると、追加のデリゲートがメモリ内に保持されることに留意してください。したがって、cacheBuffer の利用によって得られるメリットと、追加のメモリ使用量とのバランスを考慮する必要があります。cacheBuffer の利用によってメモリ負荷が増加すると、ごくまれにスクロール時のフレームレートが低下する可能性があるため、開発者はベンチマークテストを行い、使用ケースに最適な値を見極める必要があります。
パフォーマンスをさらに向上させるには、ビューでのアイテムの再利用を有効にすることを検討してください。詳細については、「Reusing Items for ListView 」および「Reusing Items for TableView and TreeView 」を参照してください。
視覚効果
Qt Quick には、開発者やデザイナーが極めて魅力的なユーザーインターフェースを作成できる機能がいくつか含まれています。滑らかな動きやダイナミックな遷移、そして視覚効果はアプリケーションにおいて大きな効果を発揮しますが、QMLの機能の中にはパフォーマンスに影響を与える可能性があるものもあるため、使用には注意が必要です。
アニメーション
一般的に、プロパティにアニメーションを適用すると、そのプロパティを参照しているバインディングがすべて再評価されます。通常、これは望ましい動作ですが、場合によっては、アニメーションを実行する前にバインディングを無効にし、アニメーションが完了した後にバインディングを再割り当てした方が良いこともあります。
アニメーション実行中は JavaScript の実行を避けてください。たとえば、x プロパティのアニメーションの各フレームごとに複雑な JavaScript 式を実行することは避けるべきです。
スクリプトによるアニメーションはメインスレッドで実行されるため(したがって、完了までに時間がかかりすぎるとフレームがスキップされる可能性があるため)、開発者は特に注意を払う必要があります。
パーティクル
この Qt Quick Particles モジュールを使用すると、美しいパーティクルエフェクトをユーザーインターフェースにシームレスに組み込むことができます。しかし、プラットフォームごとにグラフィックスハードウェアの性能は異なり、Particles モジュールでは、お使いのハードウェアが問題なくサポートできる範囲にパラメータを制限することはできません。 レンダリングしようとするパーティクルの数が増えるほど(また、パーティクルのサイズが大きくなるほど)、60 FPSでレンダリングするには、より高速なグラフィックスハードウェアが必要になります。 より多くのパーティクルを処理するには、より高速なCPUが必要です。したがって、ターゲットプラットフォームですべてのパーティクルエフェクトを慎重にテストし、60 FPSでレンダリングできるパーティクルの数とサイズを調整することが重要です。
なお、不要なシミュレーションを避けるため、使用していない場合(たとえば、表示されていない要素上など)は、パーティクルシステムを無効にすることができます。
より詳細な情報については、『パーティクルシステムのパフォーマンスガイド』を参照してください。
要素の存続期間の制御
アプリケーションを、それぞれ単一のQMLファイルに収まるシンプルなモジュール型コンポーネントに分割することで、アプリケーションの起動時間を短縮し、メモリ使用量をより適切に制御できるほか、アプリケーション内でアクティブでありながら表示されていない要素の数を減らすことができます。
遅延初期化
QMLエンジンは、コンポーネントの読み込みや初期化によってフレームがスキップされないよう、いくつかの巧妙な処理を行っています。しかし、起動時間を短縮する最善の方法は、不要な処理を避け、必要な時までその処理を遅らせることです。これは、Loader のいずれかを使用することで実現できます。
Loaderの使用
Loaderは、コンポーネントの動的な読み込みおよびアンロードを可能にする要素です。
- Loaderの「active」プロパティを使用することで、初期化を必要な時まで遅らせることができます。
- 「setSource()」関数のオーバーロードされたバージョンを使用することで、初期のプロパティ値を指定できます。
- Loaderのasynchronous プロパティをtrueに設定すると、コンポーネントのインスタンス化中の動作の滑らかさが向上する場合もあります。
未使用の要素を破棄する
非表示の要素の子であるために表示されていない要素 (たとえば、最初のタブが表示されている間のタブウィジェットの 2 番目のタブなど)は、ほとんどの場合、遅延初期化を行い、使用されなくなった時点で削除して、それらをアクティブなままにしておくことによる継続的なコスト(たとえば、レンダリング、アニメーション、プロパティバインディングの評価など)を回避する必要があります。
Loader要素で読み込まれたアイテムは、Loaderの「source」または「sourceComponent」プロパティをリセットすることで解放できますが、その他のアイテムは、destroy()を呼び出すことで明示的に解放できます。場合によっては、アイテムをアクティブなままにしておく必要があるかもしれませんが、その場合は、少なくとも非表示にする必要があります。
アクティブでありながら非表示の要素に関する詳細については、次の「レンダリング」のセクションを参照してください。
レンダリング
でレンダリングに使用されるシーングラフは、 Qt Quick では、非常に動的なアニメーション付きユーザーインターフェースを60 FPSで滑らかにレンダリングできます。ただし、レンダリング性能を劇的に低下させる要因がいくつか存在するため、開発者は可能な限りこれらの落とし穴を避けるよう注意する必要があります。
クリッピング
クリッピングはデフォルトで無効になっており、必要な場合にのみ有効にする必要があります。
クリッピングは視覚効果であり、最適化ではありません。これにより、レンダラーの処理複雑度は(軽減されるどころか)増加します。 クリッピングが有効になっている場合、アイテムは自身の描画および子要素の描画を、そのバウンディング矩形に合わせて切り取ります。これにより、レンダラーが要素の描画順序を自由に並べ替えることができなくなり、最良の場合でもシーングラフのトラバーサルが最適とは言い難い状態になります。
デリゲート内でのクリッピングは特に悪影響が大きいため、絶対に避けるべきです。
オーバードローと非表示の要素
他の(不透明な)要素によって完全に覆い隠されている要素がある場合は、それらの「visible」プロパティをfalse に設定することをお勧めします。そうしないと、不必要に描画されてしまいます。
同様に、目に見えない(例えば、最初のタブが表示されている間のタブウィジェットの2番目のタブなど)が、起動時に初期化する必要がある要素(例えば、 2番目のタブのインスタンス化に時間がかかりすぎて、タブがアクティブになった時点でのみ実行できない場合など)、描画にかかるオーバーヘッドを避けるために、「visible」プロパティをfalse に設定する必要があります(ただし、前述の通り、それらは依然としてアクティブであるため、アニメーションやバインディングの評価にかかるコストは発生します)。
半透明と不透明
不透明なコンテンツは、一般的に半透明なコンテンツよりも描画がはるかに高速です。その理由は、半透明なコンテンツにはブレンディング処理が必要であるのに対し、レンダラーは不透明なコンテンツをより効率的に最適化できる可能性があるためです。
1つの半透明ピクセルを含む画像は、その大部分が不透明であっても、完全に半透明として扱われます。透明なエッジを持つBorderImage についても同様です。
シェーダー
ShaderEffect 型を使用することで、Qt Quick アプリケーション内にGLSLコードをインラインで配置することができ、オーバーヘッドを最小限に抑えることができます。ただし、レンダリングされる形状のすべてのピクセルに対してフラグメントプログラムを実行する必要があることに留意する必要があります。 ローエンドのハードウェアにデプロイする場合、シェーダーが大量のピクセルを処理することになるため、パフォーマンスの低下を避けるために、フラグメントシェーダーを数命令程度に抑える必要があります。
GLSLで記述されたシェーダーを使用すれば、複雑な変形や視覚効果を実装できますが、その使用には注意が必要です。ShaderEffectSource を使用すると、シーンが描画される前にFBOにプリレンダリングされることになります。この余分なオーバーヘッドは、かなりの負荷となる可能性があります。
メモリの割り当てと回収
アプリケーションによって割り当てられるメモリの量と、その割り当て方法は、非常に重要な考慮事項です。 メモリ容量に制約のあるデバイスにおけるメモリ不足という明らかな懸念はさておき、ヒープ上でのメモリ割り当ては計算負荷がかなり高い操作であり、特定の割り当て戦略によっては、ページ間のデータ断片化が進行する可能性があります。 JavaScriptでは、自動的にガベージコレクションが行われる管理されたメモリヒープが使用されており、これにはいくつかの利点がある一方で、重要な影響も伴います。
QML で記述されたアプリケーションは、C++ ヒープと、自動的に管理される JavaScript ヒープの両方のメモリを使用します。アプリケーション開発者は、パフォーマンスを最大化するために、それぞれの微妙な違いを把握しておく必要があります。
QMLアプリケーション開発者向けのヒント
このセクションに記載されているヒントや提案はあくまでガイドラインであり、すべての状況に当てはまるとは限りません。最適な判断を下すために、実測データを用いてアプリケーションのベンチマークと分析を慎重に行うようにしてください。
コンポーネントのインスタンス化と初期化は遅延行う
アプリケーションが複数のビュー(たとえば、複数のタブ)で構成されているものの、一度に必要となるのはそのうちの1つだけである場合、遅延インスタンス化を利用することで、任意の時点で割り当てる必要のあるメモリ量を最小限に抑えることができます。詳細については、前のセクション「遅延初期化」を参照してください。
未使用のオブジェクトを破棄する
コンポーネントを遅延読み込みする場合や、JavaScript 式の実行中に動的にオブジェクトを作成する場合は、自動ガベージコレクションに任せるのではなく、手動で `destroy() ` を実行する方が望ましい場合が多いです。詳細については、前のセクション「要素のライフタイムの制御」を参照してください。
ガベージコレクタを手動で呼び出さない
ほとんどの場合、ガベージコレクタを手動で呼び出すことは賢明ではありません。GUIスレッドが長時間にわたってブロックされてしまうためです。これにより、フレームのスキップやアニメーションのカクつきが発生する可能性があり、これは何としても避けるべきです。
ガベージコレクタを手動で呼び出すことが許容されるケースもいくつかありますが(これについては後のセクションで詳しく説明します)、ほとんどの場合、ガベージコレクタを呼び出すことは不必要であり、かえって逆効果となります。
同一の暗黙的型を複数定義することは避けてください
QML要素にQMLでカスタムプロパティが定義されている場合、その要素は独自の暗黙的型となります。これについては、後のセクションで詳しく説明します。Component に複数の同一の暗黙的型が定義されていると、メモリが無駄になります。 そのような状況では、通常、再利用可能な新しいコンポーネントを明示的に定義する方が良いでしょう。そのような場合は、component キーワードを使用してインラインコンポーネントを定義することを検討してください。
カスタムプロパティを定義することは、多くの場合、パフォーマンスの最適化(たとえば、必要となるバインディングや再評価されるバインディングの数を減らすため)に役立ちます。また、コンポーネントのモジュール性や保守性を向上させることもできます。そのような場合は、カスタムプロパティの使用が推奨されます。 ただし、新しい型を複数回使用する場合は、メモリを節約するために、それを独自のコンポーネント(インラインまたは.qmlファイル)に分割する必要があります。
既存のコンポーネントの再利用
新しいコンポーネントの定義を検討している場合は、そのコンポーネントがプラットフォームのコンポーネントセットにすでに存在していないか、念入りに確認することをお勧めします。そうしないと、QMLエンジンに、既存のコンポーネント(おそらくすでに読み込まれているもの)と実質的に重複する型のデータ生成と保存を強いることになってしまいます。
pragma libraryスクリプトの代わりにシングルトン型を使用する
アプリケーション全体のインスタンスデータを保存するためにpragmaライブラリスクリプトを使用している場合は、代わりにQObject のシングルトン型を使用することを検討してください。これにより、パフォーマンスが向上し、JavaScriptのヒープメモリ使用量が削減されます。
QMLアプリケーションにおけるメモリ割り当て
QMLアプリケーションのメモリ使用量は、ネイティブヒープの使用量とJavaScriptヒープの使用量の2つに分けられます。それぞれで割り当てられるメモリの一部は、QMLエンジンやJavaScriptエンジンによって割り当てられるため避けられませんが、残りの部分はアプリケーション開発者の判断に依存します。
ネイティブヒープには以下が含まれます:
- QMLエンジンに固有の、固定かつ避けられないオーバーヘッド(実装用データ構造、コンテキスト情報など);
- コンポーネントごとのコンパイル済みデータおよび型情報(型ごとのプロパティメタデータを含む)。これらは、アプリケーションによってどのモジュールやコンポーネントが読み込まれるかによって、QMLエンジンによって生成されるか、ディスクキャッシュから読み込まれます;
- アプリケーションがインスタンス化するコンポーネントに応じて、オブジェクトごとの C++ データ(プロパティ値を含む)および要素ごとのメタオブジェクト階層;
- QMLインポート(ライブラリ)によって特別に割り当てられたデータ。
JavaScriptヒープには以下が含まれます:
- JavaScriptエンジン自体に由来する固定的かつ避けられないオーバーヘッド(組み込みのJavaScript型を含む);
- 当社の JavaScript 統合に伴う固定的かつ避けられないオーバーヘッド(読み込まれた型用のコンストラクタ関数、関数テンプレートなど);
- 各型について、実行時に JavaScript エンジンによって生成される型ごとのレイアウト情報およびその他の内部型データ(型に関する以下の注を参照);
- オブジェクトごとの JavaScript データ(「var」プロパティ、JavaScript 関数およびシグナルハンドラ、最適化されていないバインディング式);
- 式評価中に割り当てられる変数。
さらに、メインスレッドで使用するために 1 つの JavaScript ヒープが割り当てられ、オプションで、WorkerScript スレッドで使用するために別の JavaScript ヒープが 1 つ割り当てられます。アプリケーションが `WorkerScript ` 要素を使用しない場合、そのオーバーヘッドは発生しません。 JavaScriptヒープのサイズは数メガバイトに達する場合があるため、メモリに制約のあるデバイス向けに作成されたアプリケーションでは、WorkerScript 要素の使用を避けることが最善の策となる場合があります。
なお、QMLエンジンとJavaScriptエンジンの両方は、監視対象の型に関する型データのキャッシュを自動的に生成します。アプリケーションによって読み込まれる各コンポーネントは個別の(明示的な)型であり、QMLで独自のカスタムプロパティを定義する各要素(コンポーネントのインスタンス)は暗黙的な型となります。 カスタムプロパティを一切定義していない要素(コンポーネントのインスタンス)は、JavaScript エンジンおよび QML エンジンによって、それ自体の暗黙的な型ではなく、コンポーネントによって明示的に定義された型を持つものとみなされます。
次の例を考えてみましょう:
import QtQuick
Item {
id: root
Rectangle {
id: r0
color: "red"
}
Rectangle {
id: r1
color: "blue"
width: 50
}
Rectangle {
id: r2
property int customProperty: 5
}
Rectangle {
id: r3
property string customProperty: "hello"
}
Rectangle {
id: r4
property string customProperty: "hello"
}
}前の例では、矩形 `r0 ` および `r1 ` にはカスタムプロパティが一切ないため、JavaScript および QML エンジンはこれらを同じ型であるとみなします。つまり、`r0 ` および `r1 ` は、どちらも明示的に定義された `Rectangle ` 型であるとみなされます。一方、矩形 `r2`、`r3 `、および `r4 ` はそれぞれカスタムプロパティを持っており、それぞれ異なる(暗黙的な)型であるとみなされます。 なお、r3 とr4 は、プロパティ情報が同一であるにもかかわらず、それらがインスタンスであるコンポーネント内でカスタムプロパティが宣言されていなかったという理由だけで、それぞれ異なる型であるとみなされます。
もし、r3 とr4 が両方ともRectangleWithString コンポーネントのインスタンスであり、そのコンポーネント定義にcustomProperty という名前の文字列プロパティの宣言が含まれていた場合、r3 とr4 は同じ型であるとみなされます(つまり、独自の暗黙的な型を定義するのではなく、RectangleWithString 型のインスタンスとなります)。
メモリ割り当てに関する詳細な考察
メモリ割り当てやパフォーマンスのトレードオフに関する決定を行う際は、CPUキャッシュのパフォーマンス、オペレーティングシステムのページング、およびJavaScriptエンジンのガベージコレクションが及ぼす影響を常に念頭に置くことが重要です。最適な解決策を選択するためには、候補となる解決策を慎重にベンチマークする必要があります。
いかなる一般的なガイドラインも、コンピュータサイエンスの基礎原理に対する確固たる理解と、アプリケーション開発者が開発を行っているプラットフォームの実装詳細に関する実践的な知識に取って代わることはできません。さらに、トレードオフの判断を行う際、いくら理論的な計算を行っても、優れたベンチマークや分析ツールに代わるものはありません。
フラグメンテーション
断片化はC++開発における問題です。アプリケーション開発者がC++型やプラグインを定義していない場合は、このセクションを無視しても問題ありません。
時間の経過とともに、アプリケーションは大量のメモリを割り当て、そのメモリにデータを書き込み、一部のデータの使用が終了すると、その一部を解放します。その結果、「空き」メモリが非連続なブロックに分散することになり、他のアプリケーションが使用できるようオペレーティングシステムに返すことができません。 また、「アクティブな」データが物理メモリの多くの異なるページに分散してしまうため、アプリケーションのキャッシュやアクセス特性にも影響を及ぼします。その結果、オペレーティングシステムがスワップを余儀なくされ、ファイルシステムのI/Oが発生する可能性があります。これは、比較すると極めて遅い操作です。
断片化は、プールアロケータ(およびその他の連続したメモリアロケータ)を利用すること、オブジェクトの存続期間を慎重に管理して一度に割り当てられるメモリ量を減らすこと、キャッシュを定期的にクリーンアップして再構築すること、あるいはガベージコレクションを備えたメモリ管理型ランタイム(JavaScriptなど)を利用することで回避できます。
ガベージコレクション
JavaScriptはガベージコレクション機能を備えています。ネイティブヒープではなくJavaScriptヒープ上に割り当てられたメモリは、JavaScriptエンジンによって管理されます。エンジンは定期的に、JavaScriptヒープ上の参照されていないすべてのデータを収集します。
ガベージコレクションの影響
ガベージコレクションには長所と短所があります。これにより、オブジェクトのライフタイムを手動で管理する必要性は低くなります。しかし、その一方で、アプリケーション開発者が制御できないタイミングで、JavaScriptエンジンによって長時間かかる可能性のある処理が開始されることもあります。 アプリケーション開発者が JavaScript ヒープの使用状況を慎重に検討しない限り、ガベージコレクションの頻度や実行時間は、アプリケーションのユーザー体験に悪影響を及ぼす可能性があります。Qt 6.8 以降、ガベージコレクタは増分方式を採用しており、これにより実行時間は短縮されますが、中断の頻度が増える可能性があります。
ガベージコレクタの手動起動
QMLで記述されたアプリケーションでは、(おそらく)ある段階でガベージコレクションを実行する必要が生じます。ガベージコレクションは JavaScript エンジンによって独自のスケジュールで自動的にトリガーされますが、場合によっては、アプリケーション開発者がガベージコレクタを手動で呼び出すタイミングを決定した方が良いこともあります(ただし、通常はそうではありません)。
アプリケーションがどの程度の期間アイドル状態になるかについては、アプリケーション開発者が最もよく把握しているはずです。 QMLアプリケーションがJavaScriptヒープメモリを大量に消費し、特にパフォーマンスが重要なタスク(リストのスクロールやアニメーションなど)の最中に、定期的かつ動作を妨げるガベージコレクションサイクルが発生する場合、アプリケーション開発者は、アクティビティがゼロの期間にガベージコレクタを手動で呼び出すことが有効な手段となる場合があります。 アイドル状態の期間は、ガベージコレクションを実行するのに理想的です。なぜなら、アクティビティが行われている最中にガベージコレクタを呼び出した場合に生じるような、ユーザー体験の低下(フレームのスキップ、アニメーションのカクつきなど)をユーザーが気付くことがないからです。
ガベージコレクタは、JavaScript内でgc() を呼び出すことで手動で実行できます。これにより、完全な(非増分型の)ガベージコレクションサイクルが実行されますが、完了までに数百ミリ秒から1,000ミリ秒以上かかる場合があるため、可能な限り避けるべきです。
メモリとパフォーマンスのトレードオフ
状況によっては、メモリ使用量の増加と引き換えに処理時間を短縮することが可能です。例えば、タイトなループ内で使用されるシンボル検索の結果を JavaScript 式内の一時変数にキャッシュすると、その式を評価する際のパフォーマンスが大幅に向上しますが、その際には一時変数の割り当てが必要となります。 場合によっては、こうしたトレードオフは合理的です(上記の例のように、ほとんどの場合合理的です)。しかし、システムへのメモリ負荷の増加を避けるために、処理時間をわずかに長くしてもよい場合もあります。
場合によっては、メモリ負荷の増加による影響が極めて深刻になることもあります。状況によっては、想定されるパフォーマンス向上のためにメモリ使用量とトレードオフを行うと、ページスラッシュやキャッシュスラッシュが増加し、パフォーマンスが大幅に低下する可能性があります。特定の状況においてどの解決策が最適かを判断するためには、トレードオフの影響を常に慎重にベンチマークで検証する必要があります。
キャッシュのパフォーマンスやメモリと時間のトレードオフに関する詳細な情報については、以下の記事を参照してください。
- Ulrich Drepper 氏による優れた記事「What Every Programmer Should Know About Memory(すべてのプログラマーがメモリについて知っておくべきこと)」:https://people.freebsd.org/~lstewart/articles/cpumemory.pdf。
- Agner Fog氏によるC++アプリケーションの最適化に関する優れたマニュアル:http://www.agner.org/optimize/。
高速ブートと起動の最適化
Qt Quick アプリケーションの高速起動に向けた最適化に関する実体験に基づき、以下のベストプラクティスを検討してください:
- アプリケーションは、最初から高速に起動するように設計してください。ユーザーに最初に何を見せたいかを考えてください。
- QML Profiler を使用して、起動時のボトルネックを特定してください。
- チェーンローディングを活用してください。loaders の起動数は、CPUのコア数と同じ数に抑えてください(例:コアが2つの場合、2つのローダーを同時に実行します)。
- 最初のloader は非同期にしないようにし、一部のコンテンツが即座に表示されるようにします。その後、非同期のローダーを起動します。
- バックエンドサービスへの接続は、必要な場合にのみ行います。
- 必要に応じてインポートされるQMLモジュールを作成してください。遅延読み込みされるモジュールや型を使用することで、重要度の低いサービスをアプリケーションが必要とする際にのみ利用可能にすることができます。
- optipng などのツールを使用して、PNG/JPG 画像を最適化してください。
- 頂点数を減らし、表示されない部分を削除することで、3Dモデルを最適化してください。
- glTF を使用して、3D モデルの読み込みを最適化してください。
- clip や opacity の使用は、パフォーマンスに影響を与える可能性があるため、制限してください。
- GPUの制限を測定し、UIを設計する際にそれを考慮に入れてください。詳細については、Frame Captures and Performance Profiling を参照してください。
- Qt Quick Compilerを使用して、QML ファイルを事前にコンパイルしてください。
- お使いのアーキテクチャで静的リンクが可能かどうかを調査してください。
- 命令型のシグナルハンドラではなく、宣言型のバインディングを心がけてください。
- プロパティのバインディングはシンプルに保ってください。一般的に、QML コードはシンプルで、楽しく、読みやすいものにしましょう。そうすれば、パフォーマンスも向上します。
- 生成時間が問題となる場合は、複雑なコントロールを画像やシェーダーに置き換えてください。
以下のことは避けてください:
- QMLを過度に使いすぎないこと。QMLを使用する場合でも、必ずしもすべてをQMLで行う必要はありません。
- main.cpp内ですべてを初期化しないこと。
- 必要なインターフェースをすべて含む巨大なシングルトンを作成しないこと。
- ListView やその他のビュー用に複雑なデリゲートを作成しないこと。
- どうしても必要な場合を除き、clipを使用しないでください。
- Loaderの使いすぎというよくある罠にはまらないようにしましょう。Loader はアプリケーションページのような大規模な要素の遅延読み込みには最適ですが、単純な要素を読み込むにはオーバーヘッドが大きすぎます。これは、あらゆるものを高速化する「黒魔術」ではありません。追加のQMLコンテキストを持つ、単なる追加要素に過ぎないのです。
これらの実践により、特に組み込みデバイスにおいて、1秒未満の起動時間とスムーズなユーザー体験を実現できます。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.