En esta página

Consideraciones y sugerencias sobre el rendimiento

Consideraciones sobre la temporización

Como desarrollador de aplicaciones, normalmente te esfuerzas para que el motor de renderizado alcance una frecuencia de refresco constante de 60 fotogramas por segundo. Dependiendo de su hardware y requisitos, el número puede ser diferente, pero 60 FPS es muy común. 60 FPS significa que hay aproximadamente 16 milisegundos entre cada fotograma en los que se puede realizar el procesamiento, que incluye el procesamiento necesario para cargar las primitivas de dibujo en el hardware gráfico.

En la práctica, esto significa que el desarrollador de la aplicación debería

  • utilizar la programación asíncrona y basada en eventos siempre que sea posible
  • utilizar subprocesos de trabajo para realizar un procesamiento significativo
  • nunca girar manualmente el bucle de eventos
  • nunca gastar más de un par de milisegundos por fotograma en funciones de bloqueo.

Si no lo haces, te saltarás fotogramas, lo que tendrá un efecto drástico en la experiencia del usuario.

Nota: Un patrón tentador, pero que nunca debería utilizarse, es crear tu propio QEventLoop o llamar a QCoreApplication::processEvents() para evitar el bloqueo dentro de un bloque de código C++ invocado desde QML. Esto es peligroso, porque cuando se introduce un bucle de eventos en un manejador de señales o binding, el motor QML continúa ejecutando otros bindings, animaciones, transiciones, etc. Esos enlaces pueden causar efectos secundarios que, por ejemplo, destruyan la jerarquía que contiene tu bucle de eventos.

Perfiles

El consejo más importante es: utilice la herramienta QML Profiler incluido en Qt Creator. Saber dónde se gasta el tiempo en una aplicación le permitirá centrarse en las áreas problemáticas que realmente existen, en lugar de en las áreas problemáticas que potencialmente existen. Consulte Qt Creator: Profiling QML Applications para obtener más información.

Determinar qué bindings se ejecutan con más frecuencia o en qué funciones pasa más tiempo la aplicación le permitirá decidir si necesita optimizar las áreas problemáticas o rediseñar algunos detalles de implementación de la aplicación para mejorar el rendimiento. Si se intenta optimizar el código sin realizar un perfil, es probable que se obtengan mejoras de rendimiento muy pequeñas en lugar de significativas.

Código JavaScript

La mayoría de las aplicaciones QML contienen código JavaScript en forma de expresiones de vinculación de propiedades, funciones y controladores de señales. Por lo general, esto no supone ningún problema. Gracias a herramientas avanzadas como el compiladorQt Quick , las funciones y los enlaces sencillos pueden ser muy rápidos. Sin embargo, hay que tener cuidado de que no se activen accidentalmente procesos innecesarios. La página QML Profiler puede mostrar copiosos detalles sobre la ejecución de JavaScript y lo que la desencadenó.

Conversión de tipos

Un coste importante del uso de JavaScript es que, en algunos casos, cuando se accede a una propiedad de un tipo QML, se crea un objeto JavaScript con un recurso externo que contiene los datos C++ subyacentes (o una referencia a ellos). En la mayoría de los casos, esto es bastante económico, pero en otros puede resultar bastante caro. Hay que tener cuidado cuando se manejan tipos de valores grandes y complicados o tipos de secuencias. Éstos tienen que ser copiados por el motor QML cada vez que se cambian de lugar o se asignan a una propiedad diferente. Cuando esto se convierta en un cuello de botella, considere la posibilidad de utilizar tipos de objeto en su lugar. Las listas de tipos de objeto no tienen el mismo problema que las listas de tipos de valor porque las listas de tipos de objeto se implementan utilizando QQmlListProperty.

La mayoría de las conversiones entre tipos de valor simples son baratas. Sin embargo, hay excepciones. La creación de una url a partir de una cadena puede implicar la construcción de una instancia de QUrl, lo cual es costoso.

Resolución de propiedades

La resolución de propiedades lleva tiempo. Aunque las búsquedas suelen optimizarse para que se ejecuten mucho más rápido en las siguientes ejecuciones, siempre es mejor evitar hacer todo el trabajo innecesario, si es posible.

En el siguiente ejemplo, tenemos un bloque de código que se ejecuta a menudo (en este caso, es el contenido de un bucle explícito; pero podría ser una expresión vinculante comúnmente evaluada, por ejemplo) y en él, resolvemos el objeto con el id "rect" y su propiedad "color" múltiples veces:

// 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");
    }
}

Cada vez que se recupera rect.color, el motor QML tiene que:

  • Asignar una envoltura de tipo de valor en el montón de JavaScript.
  • Ejecutar el getter de la propiedad color de Rectangle.
  • Copiar el QColor resultante en la envoltura de tipo de valor.

No tenemos que hacer esto 4 veces. En su lugar, podemos resolver la base común una sola vez en el bloque:

// 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");
    }
}

Este simple cambio supone una mejora significativa del rendimiento. Tenga en cuenta que el código anterior se puede mejorar aún más (ya que la propiedad que se busca nunca cambia durante el procesamiento del bucle), elevando la resolución de la propiedad fuera del bucle, como se indica a continuación:

// 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");
    }
}

Enlaces de propiedades

Una expresión de vinculación de propiedades se reevaluará si cambia alguna de las propiedades a las que hace referencia. Por ello, las expresiones de vinculación deben ser lo más sencillas posible.

Si tiene un bucle en el que realiza algún procesamiento, pero sólo es importante el resultado final del procesamiento, a menudo es mejor actualizar un acumulador temporal que después asigna a la propiedad que necesita actualizar, en lugar de actualizar incrementalmente la propia propiedad, con el fin de evitar desencadenar la reevaluación de las expresiones de vinculación durante las etapas intermedias de la acumulación.

El siguiente ejemplo ilustra este punto:

// 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];
        }
    }
}

El bucle del controlador onCompleted hace que el enlace de la propiedad "text" se reevalúe seis veces (lo que provoca que cualquier otro enlace de propiedad que dependa del valor de text, así como el controlador de señal onTextChanged, se reevalúen cada vez y presenten el texto cada vez). Esto es claramente innecesario en este caso, ya que realmente sólo nos importa el valor final de la acumulación.

Se podría reescribir de la siguiente manera:

// 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;
    }
}

Consejos de secuencia

Como ya se ha mencionado, las secuencias de tipos de valores deben manejarse con cuidado.

En primer lugar, los tipos de secuencia muestran un comportamiento diferente en dos escenarios distintos:

  • si la secuencia es un Q_PROPERTY de un QObject (llamaremos a esto una secuencia de referencia),
  • si la secuencia es devuelta por una función Q_INVOKABLE de un QObject (lo llamaremos secuencia de copia).

Una secuencia de referencia se lee y escribe a través de QMetaObject cada vez que cambia, ya sea en el código JavaScript o en el objeto original. Como optimización, las secuencias de referencia (así como los tipos de valores de referencia) pueden cargarse perezosamente. El contenido real sólo se recupera cuando se utilizan por primera vez. Esto significa que cambiar el valor de cualquier elemento de la secuencia desde JavaScript resultará en:

  • Posiblemente se lea el contenido de QObject (si se ha cargado perezosamente).
  • Cambiar el elemento en el índice especificado en esa secuencia.
  • Volver a escribir toda la secuencia en QObject.

Una secuencia de copia es mucho más sencilla, ya que la secuencia real se almacena en los datos de recurso del objeto JavaScript, por lo que no se produce ningún ciclo de lectura/modificación/escritura (en su lugar, los datos de recurso se modifican directamente).

Por lo tanto, las escrituras en elementos de una secuencia de referencia serán mucho más lentas que las escrituras en elementos de una secuencia de copia. De hecho, escribir en un único elemento de una secuencia de referencia de N elementos es equivalente en coste a asignar una secuencia de copia de N elementos a esa secuencia de referencia, por lo que normalmente es mejor modificar una secuencia de copia temporal y luego asignar el resultado a una secuencia de referencia, durante el cálculo.

Supongamos la existencia (y registro previo en el espacio de nombres "Qt.example") del siguiente tipo C++:

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;
};

El siguiente ejemplo escribe en elementos de una secuencia de referencia en un bucle cerrado, lo que resulta en un mal rendimiento:

// 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");
    }
}

La lectura y escritura de la propiedad QObject en el bucle interno causada por la expresión "qrealListProperty[j] = j" hace que este código sea muy subóptimo. En su lugar, sería mejor algo funcionalmente equivalente pero mucho más rápido:

// 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");
    }
}

Otro patrón común que debería evitarse son los bucles de lectura-modificación-escritura donde cada elemento es leído, modificado y escrito de nuevo en la propiedad de la secuencia. Al igual que en el ejemplo anterior, esto provoca lecturas y escrituras de la propiedad QObject en cada iteración:

// 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");
    }
}

En su lugar, crear una copia manual de la secuencia, modificar la copia, y luego asignar el resultado de nuevo a la propiedad:

// 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");
    }
}

En segundo lugar, se emite una señal de cambio para la propiedad si cambia algún elemento de la misma. Si tiene muchas vinculaciones a un elemento concreto en una propiedad de secuencia, es mejor crear una propiedad dinámica que esté vinculada a ese elemento, y utilizar esa propiedad dinámica como símbolo en las expresiones de vinculación en lugar del elemento de secuencia, ya que sólo provocará la reevaluación de las vinculaciones si cambia su valor.

Este es un caso de uso inusual que la mayoría de los clientes nunca deberían encontrar, pero vale la pena ser consciente de ello, en caso de que se encuentre haciendo algo como esto:

// 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");
    }
}

Tenga en cuenta que, aunque en el bucle sólo se modifique el elemento de índice 2, los tres enlaces se volverán a evaluar, ya que la granularidad de la señal de cambio es que ha cambiado toda la propiedad. Por lo tanto, añadir un enlace intermedio a veces puede ser beneficioso:

// 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");
    }
}

En el ejemplo anterior, sólo se reevaluará el enlace intermedio cada vez, lo que supone un aumento significativo del rendimiento.

Consejos de tipo valor

Las propiedades de tipovalor (font, color, vector3d, etc) tienen una propiedad QObject similar y cambian la semántica de notificación a las propiedades de tipo secuencia. Como tales, los consejos dados anteriormente para las secuencias también son aplicables a las propiedades de tipo valor. Aunque suelen ser menos problemáticos con los tipos de valor (ya que el número de subpropiedades de un tipo de valor suele ser mucho menor que el número de elementos de una secuencia), cualquier aumento en el número de vinculaciones que se reevalúen innecesariamente tendrá un impacto negativo en el rendimiento.

Consejos generales de rendimiento

Las consideraciones generales de rendimiento de JavaScript derivadas del diseño del lenguaje también son aplicables a QML. Las más importantes son

  • Evite utilizar eval() en la medida de lo posible
  • No elimine propiedades de objetos

Elementos comunes de la interfaz

Elementos de texto

El cálculo de las disposiciones de texto puede ser una operación lenta. Considere la posibilidad de utilizar el formato PlainText en lugar de StyledText siempre que sea posible, ya que así se reduce la cantidad de trabajo que debe realizar el motor de maquetación. Si no puede utilizar PlainText (porque necesita incrustar imágenes o utilizar etiquetas para especificar rangos de caracteres que deben tener cierto formato (negrita, cursiva, etc.) en lugar de todo el texto), entonces debería utilizar StyledText.

Sólo debe utilizar AutoText si el texto puede ser (pero probablemente no lo sea) StyledText, ya que este modo incurrirá en un coste de análisis. El modo RichText no debería utilizarse, ya que StyledText proporciona casi todas sus características a una fracción de su coste.

Imágenes

Las imágenes son una parte vital de cualquier interfaz de usuario. Por desgracia, también son una gran fuente de problemas debido al tiempo que tardan en cargarse, la cantidad de memoria que consumen y la forma en que se utilizan.

Carga asíncrona

Las imágenes son a menudo bastante grandes, por lo que es conveniente asegurarse de que la carga de una imagen no bloquea el hilo de la interfaz de usuario. Establezca la propiedad "asíncrono" del elemento QML Image en true para permitir la carga asíncrona de imágenes desde el sistema de archivos local (las imágenes remotas siempre se cargan de forma asíncrona) cuando esto no tenga un impacto negativo en la estética de la interfaz de usuario.

Los elementos de imagen con la propiedad "asíncrona" establecida en true cargarán las imágenes en un subproceso de trabajo de baja prioridad.

Tamaño explícito de la fuente

Si tu aplicación carga una imagen grande pero la muestra en un elemento de tamaño pequeño, establece la propiedad "sourceSize" al tamaño del elemento que se está renderizando para asegurarte de que la versión a escala más pequeña de la imagen se mantiene en memoria, en lugar de la grande.

Ten en cuenta que cambiar el "sourceSize" hará que la imagen se recargue.

Evitar la composición en tiempo de ejecución

Recuerde también que puede evitar realizar el trabajo de composición en tiempo de ejecución proporcionando el recurso de imagen precompuesto con su aplicación (por ejemplo, proporcionando elementos con efectos de sombra).

Evite suavizar las imágenes

Habilite image.smooth sólo si es necesario. Es más lento en algunos equipos, y no tiene ningún efecto visual si la imagen se muestra en su tamaño natural.

Pintar

Evite pintar la misma zona varias veces. Utilice Elemento como elemento raíz en lugar de Rectángulo para evitar pintar el fondo varias veces.

Posicionar elementos con anclajes

Es más eficiente utilizar anclas que fijaciones para posicionar elementos relativos entre sí. Considere este uso de anclajes para posicionar rect2 relativo a 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
}

Esto se consigue más eficientemente usando anclas:

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
}

El posicionamiento con anclajes (asignando expresiones de anclaje a las propiedades x, y, anchura y altura de los objetos visuales, en lugar de utilizar anclajes) es relativamente lento, aunque permite la máxima flexibilidad.

Si el diseño no es dinámico, la forma más eficaz de especificarlo es mediante la inicialización estática de las propiedades x, y, anchura y altura. Las coordenadas de los elementos son siempre relativas a su padre, por lo que si se desea un desplazamiento fijo respecto a la coordenada 0,0 de su padre no se deben utilizar anclas. En el siguiente ejemplo los objetos Rectángulo hijos están en el mismo lugar, pero el código de anclas mostrado no es tan eficiente en recursos como el código que utiliza posicionamiento fijo mediante inicialización estática:

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
    }
}

Modelos y Vistas

La mayoría de las aplicaciones tendrán al menos un modelo que alimente de datos a una vista. Los desarrolladores de aplicaciones deben tener en cuenta algunos aspectos semánticos para obtener el máximo rendimiento.

Modelos C++ personalizados

A menudo es conveniente escribir un modelo personalizado en C++ para utilizarlo con una vista en QML. Aunque la implementación óptima de cualquier modelo de este tipo dependerá en gran medida del caso de uso que deba cumplir, a continuación se indican algunas directrices generales:

  • Ser lo más asíncrono posible
  • Realizar todo el procesamiento en un subproceso de trabajo (de baja prioridad).
  • Agrupar las operaciones de backend para minimizar la E/S y el IPC (potencialmente lentos).

Es importante tener en cuenta que se recomienda utilizar un subproceso trabajador de baja prioridad para minimizar el riesgo de matar de hambre al subproceso GUI (lo que podría resultar en un peor rendimiento percibido). Además, recuerde que los mecanismos de sincronización y bloqueo pueden ser una causa importante de un rendimiento lento, por lo que se debe tener cuidado para evitar bloqueos innecesarios.

Tipo ListModel QML

Qt Qml Models proporciona un tipo ListModel que puede utilizarse para introducir datos en ListView. Resulta útil para la creación rápida de prototipos, pero no es adecuado para grandes cantidades de datos. Utilice un QAbstractItemModel adecuado cuando sea necesario.

Rellenar dentro de un subproceso de trabajo

ListModel Los elementos se pueden rellenar en un subproceso de trabajo (de baja prioridad) en JavaScript. El desarrollador debe llamar explícitamente a sync() en ListModel desde dentro de WorkerScript para que los cambios se sincronicen con el subproceso principal. Consulte la documentación de WorkerScript para obtener más información.

Tenga en cuenta que el uso de un elemento WorkerScript dará lugar a la creación de un motor de JavaScript independiente (ya que el motor de JavaScript es por hilo). Esto incrementará el uso de memoria. Sin embargo, varios elementos de WorkerScript utilizarán el mismo subproceso, por lo que el impacto en la memoria de utilizar un segundo o tercer elemento de WorkerScript es insignificante una vez que la aplicación ya utiliza uno. Sin embargo, por otro lado, los scripts de trabajador adicionales no se ejecutan en paralelo.

No utilice roles dinámicos

El elemento ListModel asume que los tipos de roles dentro de cada elemento en un modelo dado son estables para propósitos de optimización. Si el tipo puede cambiar dinámicamente de elemento a elemento, el rendimiento del modelo será mucho peor.

Por lo tanto, la tipificación dinámica está desactivada por defecto; el desarrollador debe establecer específicamente la propiedad booleana dynamicRoles del modelo para activar la tipificación dinámica (y sufrir la consiguiente degradación del rendimiento). Le recomendamos que no utilice la tipificación dinámica a menos que sea absolutamente necesario.

Vistas

Las vistas delegadas deben ser lo más sencillas posible. El delegado debe tener el QML suficiente para mostrar la información necesaria. Cualquier funcionalidad adicional que no sea inmediatamente necesaria (por ejemplo, si muestra más información al hacer clic) no debe crearse hasta que sea necesario (véase la próxima sección sobre inicialización perezosa).

La siguiente lista es un buen resumen de las cosas que hay que tener en cuenta al diseñar un delegado:

  • Cuantos menos elementos haya en un delegado, más rápido se podrán crear y, por tanto, más rápido se podrá desplazar la vista.
  • Mantén al mínimo el número de enlaces en una delegación; en particular, utiliza anclas en lugar de enlaces para el posicionamiento relativo dentro de una delegación.
  • Evite utilizar elementos de ShaderEffect dentro de las delegaciones.
  • No active nunca el recorte en un delegado.

Puede establecer la propiedad cacheBuffer de una vista para permitir la creación asíncrona y el almacenamiento en búfer de delegados fuera del área visible. Se recomienda utilizar cacheBuffer para las vistas delegadas que no sean triviales y que probablemente no se creen en un solo fotograma.

Hay que tener en cuenta que cacheBuffer guarda los delegados adicionales en memoria. Por lo tanto, el valor derivado de la utilización de cacheBuffer debe equilibrarse con el uso adicional de memoria. Los desarrolladores deben utilizar la evaluación comparativa para encontrar el mejor valor para su caso de uso, ya que el aumento de la presión de memoria causada por la utilización de un cacheBuffer puede, en algunos casos raros, causar una reducción de la velocidad de fotogramas al desplazarse.

Para obtener mejoras de rendimiento adicionales, considere la posibilidad de habilitar la reutilización de elementos en las vistas. Consulte Reutilización de elementos para ListView y Reutilización de elementos para TableView y TreeView para obtener más información.

Efectos visuales

Qt Quick incluye varias funciones que permiten a los desarrolladores y diseñadores crear interfaces de usuario excepcionalmente atractivas. La fluidez y las transiciones dinámicas, así como los efectos visuales, pueden utilizarse con gran efecto en una aplicación, pero hay que tener cierto cuidado al utilizar algunas de las funciones en QML, ya que pueden tener implicaciones en el rendimiento.

Animaciones

En general, al animar una propiedad se reevalúan los enlaces que hacen referencia a ella. Normalmente, esto es lo que se desea, pero en otros casos puede ser mejor desactivar la vinculación antes de realizar la animación, y luego reasignar la vinculación una vez que la animación se haya completado.

Evite ejecutar JavaScript durante la animación. Por ejemplo, debe evitarse ejecutar una expresión compleja de JavaScript para cada fotograma de una animación de la propiedad x.

Los desarrolladores deben tener especial cuidado al utilizar animaciones de script, ya que éstas se ejecutan en el hilo principal (y, por tanto, pueden hacer que se omitan fotogramas si tardan demasiado en completarse).

Partículas

El módulo Qt Quick Particles permite integrar efectos de partículas en las interfaces de usuario. Sin embargo, cada plataforma tiene diferentes capacidades de hardware gráfico, y el módulo Particles es incapaz de limitar los parámetros a lo que tu hardware puede soportar. Cuantas más partículas intentes renderizar (y cuanto más grandes sean), más rápido tendrá que ser tu hardware gráfico para poder renderizar a 60 FPS. Afectar a más partículas requiere una CPU más rápida. Por lo tanto, es importante probar todos los efectos de partículas en tu plataforma de destino con cuidado, para calibrar el número y el tamaño de las partículas que puedes renderizar a 60 FPS.

Hay que tener en cuenta que un sistema de partículas puede desactivarse cuando no se utiliza (por ejemplo, en un elemento no visible) para evitar hacer simulaciones innecesarias.

Consulta la Guía de rendimiento del sistema de partículas para obtener información más detallada.

Control de la vida útil de los elementos

Al dividir una aplicación en componentes simples y modulares, cada uno de ellos contenido en un único archivo QML, puede conseguir un tiempo de inicio de la aplicación más rápido y un mejor control sobre el uso de la memoria, así como reducir el número de elementos activos pero invisibles en su aplicación.

Inicialización perezosa

El motor QML hace algunas cosas complicadas para asegurarse de que la carga e inicialización de los componentes no hace que se omitan fotogramas. Sin embargo, no hay mejor manera de reducir el tiempo de inicio que evitar hacer el trabajo que no necesitas hacer, y retrasar el trabajo hasta que sea necesario. Esto puede conseguirse utilizando Loader.

Usando Loader

El Loader es un elemento que permite la carga y descarga dinámica de componentes.

  • Utilizando la propiedad "active" de un Loader, la inicialización puede retrasarse hasta que sea necesaria.
  • Utilizando la versión sobrecargada de la función "setSource()", se pueden suministrar valores iniciales de las propiedades.
  • Establecer la propiedad Loader asynchronous a true también puede mejorar la fluidez mientras se instancia un componente.

Destruir elementos no utilizados

Los elementos que son invisibles porque son hijos de un elemento no visible (por ejemplo, la segunda pestaña en un widget de pestañas, mientras que la primera pestaña se muestra) deben ser inicializados perezosamente en la mayoría de los casos, y eliminados cuando ya no estén en uso, para evitar el coste continuo de dejarlos activos (por ejemplo, renderizado, animaciones, evaluación de propiedades, etc).

Un elemento cargado con un elemento Loader puede ser liberado restableciendo la propiedad "source" o "sourceComponent" del Loader, mientras que otros elementos pueden ser liberados explícitamente llamando a destroy() sobre ellos. En algunos casos, puede ser necesario dejar el elemento activo, en cuyo caso debe hacerse invisible como mínimo.

Véase la próxima sección sobre Renderizado para más información sobre elementos activos pero invisibles.

Renderizado

El gráfico de escena utilizado para el renderizado en Qt Quick permite renderizar interfaces de usuario altamente dinámicas y animadas de forma fluida a 60 FPS. Sin embargo, hay algunas cosas que pueden disminuir drásticamente el rendimiento de la renderización, y los desarrolladores deben tener cuidado de evitar estas trampas siempre que sea posible.

Recorte

El recorte está desactivado por defecto y sólo debe activarse cuando sea necesario.

El recorte es un efecto visual, NO una optimización. Aumenta (en lugar de reducir) la complejidad del renderizador. Si el recorte está activado, un elemento recortará su propia pintura, así como la pintura de sus hijos, a su rectángulo delimitador. Esto impide que el renderizador pueda reordenar libremente el orden de dibujo de los elementos, lo que da como resultado un recorrido del gráfico de la escena subóptimo en el mejor de los casos.

Recortar dentro de un delegado es especialmente malo y debe evitarse a toda costa.

Sobredibujo y elementos invisibles

Si tienes elementos que están totalmente cubiertos por otros elementos (opacos), es mejor establecer su propiedad "visible" a false o se dibujarán innecesariamente.

Del mismo modo, los elementos que son invisibles (por ejemplo, la segunda pestaña de un widget de pestañas, mientras se muestra la primera pestaña) pero que necesitan ser inicializados en el momento del inicio (por ejemplo, si el coste de instanciar la segunda pestaña lleva demasiado tiempo para poder hacerlo sólo cuando se activa la pestaña), deberían tener su propiedad "visible" establecida a false, para evitar el coste de dibujarlos (aunque como se ha explicado anteriormente, seguirán incurriendo en el coste de cualquier animación o evaluación de bindings, ya que siguen activos).

Translúcido vs Opaco

El contenido opaco es generalmente mucho más rápido de dibujar que el translúcido. La razón es que el contenido translúcido necesita mezclarse y que el renderizador puede optimizar mejor el contenido opaco.

Una imagen con un píxel translúcido se trata como totalmente translúcida, aunque sea mayoritariamente opaca. Lo mismo ocurre con una BorderImage con bordes transparentes.

Sombreadores

El tipo ShaderEffect permite colocar código GLSL en línea en una aplicación Qt Quick con muy poca sobrecarga. Sin embargo, es importante tener en cuenta que el programa de fragmentos debe ejecutarse para cada píxel de la forma renderizada. Cuando se despliega en hardware de gama baja y el shader cubre una gran cantidad de píxeles, se debe mantener el fragment shader a unas pocas instrucciones para evitar un rendimiento pobre.

Los shaders escritos en GLSL permiten escribir transformaciones complejas y efectos visuales, sin embargo deben usarse con cuidado. El uso de ShaderEffectSource hace que una escena sea prerrenderizada en un FBO antes de que pueda ser dibujada. Esta sobrecarga puede resultar bastante cara.

Asignación y recogida de memoria

La cantidad de memoria que será asignada por una aplicación y la forma en que esa memoria será asignada son consideraciones muy importantes. Aparte de las preocupaciones obvias sobre las condiciones fuera de memoria en dispositivos con memoria limitada, la asignación de memoria en el montón es una operación bastante costosa computacionalmente, y ciertas estrategias de asignación pueden resultar en una mayor fragmentación de los datos a través de las páginas. JavaScript utiliza un montón de memoria gestionada que se recoge automáticamente, lo que tiene algunas ventajas, pero también algunas implicaciones importantes.

Una aplicación escrita en QML utiliza memoria tanto del montón de C++ como de un montón de JavaScript gestionado automáticamente. El desarrollador de la aplicación debe conocer las sutilezas de cada una de ellas para maximizar el rendimiento.

Consejos para desarrolladores de aplicaciones QML

Los consejos y sugerencias contenidos en esta sección son sólo orientativos y pueden no ser aplicables en todas las circunstancias. Asegúrese de comparar y analizar cuidadosamente su aplicación utilizando métricas empíricas, con el fin de tomar las mejores decisiones posibles.

Instanciar e inicializar componentes de forma perezosa

Si tu aplicación consiste en múltiples vistas (por ejemplo, múltiples pestañas) pero sólo una es necesaria en un momento dado, puedes utilizar la instanciación perezosa para minimizar la cantidad de memoria que necesitas tener asignada en un momento dado. Consulte la sección anterior sobre Inicialización perezosa para obtener más información.

Destruir objetos no utilizados

Si carga componentes perezosamente, o crea objetos dinámicamente durante una expresión JavaScript, a menudo es mejor destroy() manualmente en lugar de esperar a que lo haga la recolección automática de basura. Consulte la sección anterior sobre Control de la vida útil de los elementos para obtener más información.

No invoque manualmente al recolector de basura

En la mayoría de los casos, no es aconsejable invocar manualmente al recolector de basura, ya que bloqueará el hilo GUI durante un periodo de tiempo considerable. Esto puede dar lugar a fotogramas omitidos y animaciones entrecortadas, que deben evitarse a toda costa.

Hay algunos casos en los que invocar manualmente el recolector de basura es aceptable (y esto se explica con más detalle en una próxima sección), pero en la mayoría de los casos, invocar el recolector de basura es innecesario y contraproducente.

Evita definir múltiples tipos implícitos idénticos

Si un elemento QML tiene una propiedad personalizada definida en QML, se convierte en su propio tipo implícito. Esto se explica con más detalle en una próxima sección. Si se definen varios tipos implícitos idénticos en Component, se desperdiciará algo de memoria. En ese caso, suele ser mejor definir explícitamente un nuevo componente que pueda reutilizarse. En tal caso, considere la posibilidad de definir un componente en línea utilizando la palabra clave component.

Definir una propiedad personalizada puede ser a menudo una optimización beneficiosa para el rendimiento (por ejemplo, para reducir el número de enlaces necesarios o reevaluados), o puede mejorar la modularidad y la capacidad de mantenimiento de un componente. En estos casos, se recomienda utilizar propiedades personalizadas. Sin embargo, si el nuevo tipo se utiliza más de una vez, debe dividirse en su propio componente (en línea o archivo .qml) para ahorrar memoria.

Reutilización de componentes existentes

Si está pensando en definir un nuevo componente, conviene comprobar que no existe ya en el conjunto de componentes de su plataforma. De lo contrario, estará forzando al motor QML a generar y almacenar datos de tipo para un tipo que es esencialmente un duplicado de otro componente preexistente y potencialmente ya cargado.

Utilice tipos singleton en lugar de scripts de biblioteca pragma

Si utiliza una secuencia de comandos de biblioteca pragma para almacenar datos de instancia de toda la aplicación, considere la posibilidad de utilizar un tipo singleton de QObject en su lugar. Esto debería mejorar el rendimiento y hará que se utilice menos memoria JavaScript en la pila.

Asignación de memoria en una aplicación QML

El uso de memoria de una aplicación QML puede dividirse en dos partes: su uso de la pila de C++ y su uso de la pila de JavaScript. Parte de la memoria asignada en cada una de ellas será inevitable, ya que la asigna el motor QML o el motor JavaScript, mientras que el resto depende de decisiones tomadas por el desarrollador de la aplicación.

El heap de C++ contendrá:

  • la sobrecarga fija e inevitable del motor QML (estructuras de datos de implementación, información de contexto, etc.);
  • datos compilados por componente e información de tipo, incluidos metadatos de propiedades por tipo, que el motor QML genera o carga desde la caché de disco en función de los módulos y componentes que cargue la aplicación;
  • datos C++ por objeto (incluidos los valores de propiedad) más una jerarquía de metaobjetos por elemento, en función de los componentes que instancie la aplicación;
  • cualquier dato asignado específicamente por las importaciones QML (bibliotecas).

El montón de JavaScript contendrá:

  • la sobrecarga fija e inevitable del propio motor JavaScript (incluidos los tipos JavaScript incorporados);
  • la sobrecarga fija e inevitable de nuestra integración de JavaScript (funciones de constructor para tipos cargados, plantillas de funciones, etc.);
  • información de diseño por tipo y otros datos de tipo internos generados por el motor de JavaScript en tiempo de ejecución, para cada tipo (véase la nota siguiente, relativa a los tipos);
  • datos JavaScript por objeto (propiedades "var", funciones JavaScript y manejadores de señales, y expresiones de enlace no optimizadas);
  • variables asignadas durante la evaluación de expresiones.

Además, habrá una pila de JavaScript asignada para su uso en el subproceso principal y, opcionalmente, otra pila de JavaScript asignada para su uso en el subproceso WorkerScript. Si una aplicación no utiliza un elemento WorkerScript, no se incurrirá en esa sobrecarga. La pila de JavaScript puede tener un tamaño de varios megabytes, por lo que las aplicaciones escritas para dispositivos con limitaciones de memoria pueden ser más útiles si se evita el elemento WorkerScript.

Tenga en cuenta que tanto el motor QML como el motor JavaScript generarán automáticamente sus propias cachés de datos de tipo sobre los tipos observados. Cada componente cargado por una aplicación es un tipo distinto (explícito), y cada elemento (instancia de componente) que define sus propias propiedades personalizadas en QML es un tipo implícito. Los motores JavaScript y QML consideran que cualquier elemento (instancia de un componente) que no defina ninguna propiedad personalizada es del tipo definido explícitamente por el componente, en lugar de su propio tipo implícito.

Considere el siguiente ejemplo:

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"
    }
}

En el ejemplo anterior, los rectángulos r0 y r1 no tienen propiedades personalizadas, por lo que los motores JavaScript y QML consideran que ambos son del mismo tipo. Es decir, r0 y r1 se consideran ambos del tipo Rectangle definido explícitamente. Los rectángulos r2, r3 y r4 tienen propiedades personalizadas y se consideran de tipos diferentes (implícitos). Observe que r3 y r4 se consideran de tipos diferentes, aunque tienen la misma información de propiedad, simplemente porque la propiedad personalizada no se declaró en el componente del que son instancias.

Si r3 y r4 fueran instancias de un componente RectangleWithString, y esa definición de componente incluyera la declaración de una propiedad de cadena llamada customProperty, entonces r3 y r4 se considerarían del mismo tipo (es decir, serían instancias del tipo RectangleWithString, en lugar de definir su propio tipo implícito).

Consideraciones detalladas sobre la asignación de memoria

A la hora de tomar decisiones sobre la asignación de memoria o el rendimiento, es importante tener en cuenta el impacto del rendimiento de la caché de la CPU, la paginación del sistema operativo y la recolección de basura del motor de JavaScript. Las posibles soluciones deben evaluarse cuidadosamente para asegurarse de que se elige la mejor.

Ningún conjunto de directrices generales puede sustituir a una sólida comprensión de los principios subyacentes de la informática combinada con un conocimiento práctico de los detalles de implementación de la plataforma para la que el desarrollador de aplicaciones está desarrollando. Además, ningún cálculo teórico puede sustituir a un buen conjunto de puntos de referencia y herramientas de análisis a la hora de tomar decisiones de compensación.

Fragmentación

La fragmentación es un problema de desarrollo de C++. Si el desarrollador de la aplicación no está definiendo ningún tipo C++ o plugins, puede ignorar esta sección.

Con el tiempo, una aplicación asignará grandes porciones de memoria, escribirá datos en esa memoria, y posteriormente liberará algunas porciones de la misma una vez que haya terminado de utilizar algunos de los datos. Esto puede dar lugar a que la memoria "libre" se encuentre en trozos no contiguos, que no pueden ser devueltos al sistema operativo para que otras aplicaciones los utilicen. También repercute en las características de acceso y almacenamiento en caché de la aplicación, ya que los datos "vivos" pueden estar repartidos en muchas páginas diferentes de la memoria física. Esto, a su vez, podría obligar al sistema operativo a hacer swap, lo que puede provocar E/S del sistema de archivos, que es, comparativamente hablando, una operación extremadamente lenta.

La fragmentación puede evitarse utilizando asignadores de pool (y otros asignadores de memoria contigua), reduciendo la cantidad de memoria que se asigna en un momento dado mediante una gestión cuidadosa de los tiempos de vida de los objetos, limpiando y reconstruyendo periódicamente las cachés, o utilizando un tiempo de ejecución gestionado por memoria con recolección de basura (como JavaScript).

Recogida de basura

JavaScript dispone de un sistema de recogida de basura. La memoria que se asigna al montón de JavaScript (a diferencia del montón de C++) es propiedad del motor de JavaScript. El motor recogerá periódicamente todos los datos no referenciados en el montón de JavaScript.

Implicaciones de la recolección de elementos no utilizados

La recolección de elementos no utilizados tiene ventajas e inconvenientes. Significa que la gestión manual de la vida útil de los objetos es menos importante. Sin embargo, también significa que una operación potencialmente duradera puede ser iniciada por el motor JavaScript en un momento que está fuera del control del desarrollador de la aplicación. A menos que el desarrollador de la aplicación considere cuidadosamente el uso del montón de JavaScript, la frecuencia y duración de la recolección de basura puede tener un impacto negativo en la experiencia de la aplicación. Desde Qt 6.8, el recolector de basura es incremental, lo que significa que incurrirá en interrupciones más cortas, pero potencialmente más frecuentes.

Invocación manual del recolector de basura

Una aplicación escrita en QML requerirá (muy probablemente) que se realice la recolección de basura en algún momento. Aunque el motor JavaScript activará automáticamente la recolección de elementos no utilizados según su propio calendario, a veces es mejor que el desarrollador de la aplicación decida cuándo invocar manualmente el recolector de elementos no utilizados (aunque no suele ser el caso).

Es probable que el desarrollador de la aplicación sea quien mejor sepa cuándo una aplicación va a estar inactiva durante periodos de tiempo considerables. Si una aplicación QML utiliza mucha memoria de montón de JavaScript, lo que provoca ciclos de recolección de elementos no utilizados regulares y molestos durante tareas especialmente sensibles al rendimiento (por ejemplo, desplazamiento de listas, animaciones, etc.), el desarrollador de la aplicación puede estar bien servido para invocar manualmente al recolector de elementos no utilizados durante períodos de actividad nula. Los periodos de inactividad son ideales para realizar la recolección de basura ya que el usuario no notará ninguna degradación de la experiencia de usuario (cuadros saltados, animaciones entrecortadas, etc.) que resultaría de invocar al recolector de basura mientras la actividad está ocurriendo.

El recolector de basura puede ser invocado manualmente llamando a gc() dentro de JavaScript. Esto provocará que se realice un ciclo de recolección completo y no incremental, que puede tardar entre unos cientos y más de mil milisegundos en completarse, por lo que debe evitarse en la medida de lo posible.

Memoria frente a rendimiento

En algunas situaciones, es posible compensar un mayor uso de memoria con un menor tiempo de procesamiento. Por ejemplo, almacenar en caché el resultado de una búsqueda de símbolos utilizada en un bucle cerrado en una variable temporal de una expresión de JavaScript supondrá una mejora significativa del rendimiento al evaluar esa expresión, pero implica asignar una variable temporal. En algunos casos, estas compensaciones son sensatas (como en el caso anterior, que casi siempre lo es), pero en otros puede ser mejor permitir que el procesamiento tarde un poco más para evitar aumentar la presión de memoria sobre el sistema.

En algunos casos, el impacto del aumento de la presión de memoria puede ser extremo. En algunas situaciones, sacrificar el uso de memoria por una supuesta ganancia de rendimiento puede resultar en un aumento de page-thrash o cache-thrash, causando una enorme reducción del rendimiento. Siempre es necesario evaluar cuidadosamente el impacto de las compensaciones para determinar qué solución es la mejor en una situación determinada.

Para obtener información detallada sobre el rendimiento de la caché y las compensaciones memoria-tiempo, consulta los siguientes artículos:

Optimización de arranque rápido e inicio

Basándonos en la experiencia del mundo real optimizando aplicaciones Qt Quick para un arranque rápido, considere las siguientes mejores prácticas:

  • Diseña tu aplicación para que arranque rápido desde el principio. Piense en lo que quiere que el usuario vea primero.
  • Utilice la QML Profiler para identificar cuellos de botella en el arranque.
  • Utilice la carga en cadena. Ejecute sólo tantos loaders como núcleos tenga en su CPU (por ejemplo, dos núcleos: dos cargadores ejecutándose al mismo tiempo).
  • El primer loader no debería ser asíncrono, para que algunos contenidos se muestren inmediatamente. Active después los cargadores asíncronos.
  • Conéctese a los servicios back-end sólo cuando sea necesario.
  • Crear módulos QML que se importen cuando sea necesario. Usando módulos y tipos cargados perezosamente puedes hacer que los servicios no críticos estén disponibles para tu aplicación cuando sea necesario.
  • Optimice sus imágenes PNG/JPG utilizando herramientas como optipng.
  • Optimiza tus modelos 3D reduciendo la cantidad de vértices y eliminando las partes que no son visibles.
  • Optimice la carga del modelo 3D utilizando glTF.
  • Limita el uso de clip y opacidad, ya que pueden afectar al rendimiento.
  • Mide las limitaciones de la GPU y tenlas en cuenta a la hora de diseñar la interfaz de usuario. Consulta Capturas de fotogramas y perfiles de rendimiento para obtener más información.
  • Utilice Qt Quick Compiler para precompilar los archivos QML.
  • Investiga si la vinculación estática es posible para tu arquitectura.
  • Intente utilizar enlaces declarativos en lugar de manejadores de señales imperativos.
  • Simplifique los enlaces de propiedades. En general, el código QML debe ser sencillo, divertido y legible. De este modo se conseguirá un buen rendimiento.
  • Sustituya los controles complejos por imágenes o shaders si el tiempo de creación es un problema.

No lo haga:

  • Exagerar con QML. Incluso si usas QML, no necesitas hacer absolutamente todo en QML.
  • Inicializa todo en tu main.cpp.
  • Crea grandes singletons que contengan todas las interfaces necesarias.
  • Crea delegados complejos para ListView u otras vistas.
  • Utilice clip a menos que sea absolutamente necesario.
  • Caer en la trampa común de sobreutilizar los cargadores. Loader es genial para la carga perezosa de cosas grandes como páginas de aplicaciones, pero introduce demasiada sobrecarga para cargar cosas simples. No es magia negra que acelera cualquier cosa y todo. Es un elemento extra con un contexto QML extra.

Estas prácticas ayudan a conseguir tiempos de arranque por debajo del segundo y experiencias de usuario fluidas, especialmente en dispositivos integrados.

© 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.