Aller au contenu principal

Performances

Les développeurs posent fréquemment des questions à proppos des stratégies à adopter pour optimiser les performances des applications Electron. Les ingénieurs logiciels, les utilisateurs et les développeurs de frameworks ne sont pas toujours d’accord sur ce que signifie « performances » et quelle en est la définition. Ce document décrit certaines des manières privilégiées par les mainteneurs d'Electron pour réduire la quantité des ressources mémoire, CPU, et disque utilisées tout en s'assurant que l'application est très réactive aux saisies de l'utilisateur et s'exécute aussi rapidement que possible. Par ailleurs, ces stratégies devront assurer un niveau élevé de sécurité à votre application.

Le bon sens et les manières de créer des sites Web performants avec JavaScript s’appliquent pour la plupart aux applications Electron. Dans une certaine mesure, les ressources évoquant les façons de créer des applications Node.js performantes s’appliquent également, mais comprenez bien que la signification du terme « performances » est différente selon que l'on est avec un backend Node.js ou avec une application coté client.

La liste qui vous est fournie un peu plus bas l'est à titre utilitaire et est, tout comme notre liste de contrôle de sécurité , non exhaustive. Il est d'ailleurs surement possible de créer une application Electron s'avérant lente tout en suivant toutes les étapes décrites ci-dessous. Electron est une plate-forme de développement puissante qui vous permet, en tant que développeur, de faire plus ou moins ce que vous voulez. Toute cette liberté signifie que les performances sont, pour leur majeure partie, de votre responsabilité.

Mesurer, Mesurer et encore Mesurer

La checklist contient un certain nombre d’étapes assez simples et faciles à mettre en œuvre. Cependant, la création de la version la plus performante de votre application vous imposera bien plus que de suivre simplement un certain nombre d’étapes. Au lieu de cela, vous devrez examiner de très près tout le code en cours d’exécution dans votre application en l'analysant et le mesurant soigneusement. Vous devres donc chercher par exemple où sont les goulots d'étranglement ? Quelles opérations prennent le plus de temps lorsque l’utilisateur clique sur un bouton? Quels objets occupent le plus de mémoire alors que l’application tourne simplement au ralenti?

À maintes reprises, nous avons vu que la stratégie la plus efficace pour créer une application Electron performante consiste à profiler le code en cours d’exécution, à trouver le morceau le plus couteux en ressources et à l’optimiser. La répétition plusieurs fois de suite de ce processus laborieux et apparemment sans fin augmentera considérablement les performances de de votre application. L’expérience acquise avec des applications majeures telles que Visual Studio Code ou Slack a montré que cette pratique est de loin la stratégie la plus efficace pour améliorer les performances.

Pour en savoir plus sur la façon d'analyser et de profiler le code de votre application, familiarisez-vous avec les Outils de développement Chrome. Pour une analyse avancée ou on examine plusieurs processus simultanément, envisagez l’outil Chrome Tracing.

Lectures Recommandées

Checklist : recommandations pour optimiser les performances

Il y a des chances que votre application soit un peu plus légère, plus rapide et généralement moins gourmande en ressources si vous essayez de suivre ces conseils.

  1. Ne pas inclure des modules de manière inconsidérée
  2. Ne pas charger ni exécuter du code plus tôt que nécessaire
  3. Eviter de bloquer le processus principal
  4. Eviter de bloquer les processus de rendu
  5. Eviter les polyfills inutiles
  6. Faire la chasse aux requêtes réseau inutiles ou bloquantes
  7. Regroupez votre code

1. Ne pas inclure des modules de manière inconsidérée

Avant d’ajouter un module Node.js à votre application, examinez bien ledit module. Combien de de dépendances contient ce module ? De quel type de ressources a-t-il besoin pour simplement être appelé dans une déclaration require() ? Vous constaterez peut-être ainsi que la taille du module le plus de téléchargé sur le registre NPM ou le plus étoilé sur GitHub n’est en fait pas du tout la plus petite parmi ceux disponibles.

Pourquoi ?

Le raisonnement derrière cette recommandation est mieux illustré par un exemple réel. Durant les tout débuts d'Electron, une détection fiable de la connectivité réseau était un problème, il en résulta que de nombreuses applications utilièrent un module exposant une simple méthode isOnline().

Ce module détectait votre connectivité réseau en essayant d’atteindre un certain nombre de points de terminaison bien connus. Pour la liste de ces points de terminaison, cela dépendait d'un module différent, qui contenait également une liste de ports bien connus. Cette dépendance reposait elle-même sur un module contenant des informations sur les ports sous la forme d'un fichier JSON avec plus de 100.000 lignes de contenu. Chaque fois que le module était chargé (généralement dans une instruction require('module') ), il chargeait toutes ses dépendances et finissait par lire et analyser ce fichier JSON. Mais l’analyse de plusieurs milliers de lignes de JSON est une opération très coûteuse. Sur une machine lente, cela peut prendre des secondes.

Dans de nombreux contextes de serveur, le temps de démarrage est pratiquement insignifiant. Un de serveur Node.js ayant besoin des informations sur tous les ports est probablement en fait « plus performant » s’il charge toutes les informations requises en mémoire chaque fois que le serveur démarre et pourra ainsi traiter les requêtes plus rapidement. Le module abordé dans cet exemple n'est pas un « mauvais » module. Cependant, les applications Electron ne devraient pas avoir à charger, analyser et stocker en mémoire des informations dont elles n'ont pas réellement besoin.

En bref, un module apparemment excellent, écrit principalement pour les serveurs Node.js fonctionnant sous Linux peut être une mauvaise nouvelle pour les performances de votre application. Dans cet exemple particulier , la bonne solution était de n’utiliser aucun module et d’utiliser à la place les contrôles de connectivité inclus dans les versions ultérieures de Chromium.

Comment ?

Lorsque vous envisagez d'utiliser un module, nous vous recommandons de vérifier :

  1. La taille des dépendances incluses
  2. Les ressources nécessaires pour le charger (require())
  3. Les ressources requises pour effectuer l’action qui vous intéresse

La génération d’un profil CPU et d’un profil de mémoire de tas pour le chargement d’un module peut être effectuée avec une seule commande sur la ligne de commande. Dans l’exemple ci-dessous, nous examinons le module populaire request.

node --cpu-prof --heap-prof -e "require('request')"

L’exécution de cette commande aboutit à un fichier .cpuprofile et un fichier .heapprofile dans le répertoire où vous l’avez exécuté. Les deux fichiers peuvent être analysés en détails à l’aide des outils de développement de Chrome et en utilisant respectivement les onglets Performance et Memory.

Profil de performance de CPU

Profil de performance de la mémoire du tas

Dans cet exemple et sur la machine de l'auteur, nous avons observé que le chargement de request prenait près d'une demi-seconde, alors que node-fetch prenait lui, moins de 50ms et utilisait énormément moins de mémoire.

2. Ne pas charger ni exécuter du code plus tôt que nécessaire

Si vous avez des opérations de configuration coûteuses, envisagez de les reporter. Inspectez tout ce qui est exécuté juste après le démarrage de l’application. Au lieu de déclencher toutes les opérations immédiatement, envisagez de les échelonner dans une séquence plus en rapport avec le parcours de l’utilisateur.

Dans un développement traditionnel avec Node.js, nous avons l’habitude de mettre toutes nos déclarations require() en haut. Si vous écrivez actuellement votre application Electron de cette façon et que vous utilisez des modules de taille importante dont vous n’avez pas immédiatement besoin, appliquez la stratégie précédante et reportez leur chargement à un moment plus opportun.

Pourquoi ?

Le chargement des modules est une opération étonnamment coûteuse, en particulier sur Windows. Lorsque votre application démarre, elle ne doit pas obliger les utilisateurs à attendre des opérations qui ne sont pas nécessaires à ce moment précis.

Cela peut sembler évident, mais de nombreuses applications ont tendance à effectuer une grande quantité de travail immédiatement après le lancement de l’application - comme la vérification des mises à jour, le téléchargement de contenu utilisé dans un flux ultérieur ou l’exécution d’opérations d’E/S disque lourdes .

Prenons Visual Studio Code comme exemple. Lorsque vous ouvrez un fichier, il affiche immédiatement le fichier sans aucune mise en surbrillance de code, priorisant ainsi votre capacité à interagir avec le texte. Une fois ce travail effectué, il passera alors à la mise en surbrillance du code.

Comment ?

Nous allons pour exemple supposer que votre application analyse des fichiers au format fictif .foo . Pour ce faire, elle s’appuie sur le module foo-parser tout aussi fictif . Dans un développement Node.js traditionnel, vous ecririez du code chargeant les dépendances sans attendre:

parser.js
const fs = require('fs')
const fooParser = require('foo-parser')

class Parser {
constructor () {
this.files = fs.readdirSync('.')
}

getParsedFiles () {
return fooParser.parse(this.files)
}
}

const parser = new Parser()

module.exports = { parser }

Dans l’exemple ci-dessus, nous effectuons beaucoup de travail exécuté dès que que le fichier est chargé. Mais avons-nous besoin d’obtenir les fichiers analysés tout de suite? Ne pourrions-nous pas faire ce travail un peu plus tard, lorsque getParsedFiles() est réellement appelé?

parser.js
// "fs" est surement déja chargé donc son `require()` ne coûte surement pas cher
const fs = require('fs')

class Parser {
async getFiles () {
// Accéder au disque seulement lorsque `getFiles` est apellé et pas avant.
//Assurez-vous également que nous ne bloquons pas d’autres opérations en utilisant
// la version asynchrone.
this.files = this.files || await fs.readdir('.')

retourne ceci. iles
}

async getParsedFiles () {
// Notre outil fictif foo-parser est un module conséquent et coûteux à charger, donc
// diffèrez cette tâche jusqu'à ce que nous ayons réellement besoin d'analyser les fichiers.
Étant donné que 'require()' est livré avec un cache de module, l’appel à 'require()'
// ne sera coûteux qu’une seule fois - les appels ultérieurs de 'getParsedFiles()'
// seront plus rapides.
const fooParser = require('foo-parser')
const files = await this.getFiles()

return fooParser.parse(files)
}
}

// Cette opération est maintenant beaucoup plus légère que dans l'exemple précédant
const parser = new Parser()

module.exports = { parser }

En bref, allouez les ressources en « juste à temps » plutôt que de les allouer toutes au démarrage de votre application.

3. Eviter de bloquer le processus principal

Le processus principal d’Electron (parfois appelé « processus du navigateur ») est spécial: il est le processus parent à tous les autres processus de votre application et est le processus principal avec lequel le système d’exploitation interagit. Il gère les fenêtres, les interactions et la communication entre différents composants de votre application. Il héberge également le thread d’interface utilisateur .

Vous ne devez en aucun cas bloquer ce processus et le thread d’interface utilisateur par des opérations de longue durée. Le blocage du thread d’interface utilisateur signifie que l’ensemble de votre application se figera jusqu’à ce que le processus principal soit prêt à poursuivre le traitement.

Pourquoi ?

Le processus principal et son thread d’interface utilisateur sont essentiellement la tour de contrôle des principales opérations à l’intérieur de votre application. Lorsque le système d’exploitation informe votre application d’un clic de souris, il passe par le processus principal avant d’atteindre votre fenêtre. Si votre fenêtre rend une animation fluide, elle devra échanger avec le processus GPU - une fois de plus en passant par le processus principal.

Electron et Chromium prennent soin d'attribuer des nouveaux threads aux E/S de disque lourdes et aux opérations liées au processeur afin d'éviter de bloquer le thread d’interface utilisateur. Vous devez en faire de même.

Comment ?

La puissante architecture multi-processus d’Electron est là pour vous aider en ce qui concerne les tâches de longue durée, mais comprend également quelques pièges coté performances.

  1. Pour les tâches de longue durée lourdes pour le CPU , utilisez soit des threads de worker, ou envisagez de les déplacer vers la BrowserWindow ou (en dernier recours) génèrez un processus dédié.

  2. Évitez autant que possible, d’utiliser l’IPC synchrone et le module @electron/remote . Bien qu’il existe des cas d’utilisation légitimes, il est beaucoup trop facile de bloquer sans le savoir le thread de l’interface utilisateur.

  3. Évitez d’utiliser des opérations d’E/S bloquantes dans le processus principal. Bref, chaque fois que des modules de base de Node.js (comme fs ou child_process) offrent une version synchrone ou une version asynchrone vous préférez la variante asynchrone et non-bloquante. .

4. Eviter de bloquer les processus de rendu

Étant donné qu’Electron est livré avec une version courante de Chromium, vous pouvez utiliser les fonctionnalités les plus récentes et les plus performantes offertes par la plate-forme Web pour différer ou se débarasser des opérations lourdes afin que votre application soit fluide et réactive.

Pourquoi ?

Votre application a probablement beaucoup de JavaScript à exécuter dans le processus de rendu. L’astuce consiste à exécuter les opérations le plus rapidement possible sans réduire les ressources nécessaires au bon fonctionnement en ce qui concerne le défilement, les animations à 60ips et la réactivité aux saisies de l'utilisateur.

L'orchestration du flux des opérations dans le code de votre moteur de rendu est particulièrement utile si les utilisateurs se plaignent que parfois l'application fonctionne par acoups.

Comment ?

De manière générale, tous les conseils prodigués pour créer des applications web performantes pour les navigateurs modernes s'appliquent également aux moteurs de rendu d'Electron. Les deux principaux outils à votre disposition sont actuellement requestIdleCallback() pour les petites opérations et Web Workers pour les opérations de longue durée.

requestIdleCallback() permet aux développeurs de mettre en file d’attente une fonction à exécuter dès que le processus entre dans une période d’inactivité. Il vous permet d' d’effectuer un travail de faible priorité ou en arrière-plan sans affecter l’expérience utilisateur. Pour plus d’informations sur son utilisation, consultez sa documentation sur MDN.

Les Web Workers sont un outil puissant pour exécuter du code dans un thread séparé. Il y a cependant quelques réserves à considérer - consultez la documentation multithreading d’Electron et la documentation MDN concernant les Web Workers. Ils constituent une solution idéale pour toute opération nécessitant beaucoup de puissance CPU pendant une longue période.

5. Eviter les polyfills inutiles

L’un des grands avantages d’Electron est de savoir exactement quel moteur va analyser vos JavaScript, HTML et CSS. Si vous réutilisez du code qui a été écrit pour le Web en général, assurez-vous de ne pas coder de polyfill pour des fonctionnalités incluses dans Electron.

Pourquoi ?

Lors de la création d’une application Web pour l’Internet d’aujourd’hui, les environnements les plus anciens dictent les fonctionnalités que vous pouvez ou non utiliser. Même si Electron prend en charge les très performants filtres et animations CSS, un navigateur plus ancien ne le fera pas. Là où vous pouviez utiliser WebGL, vos développeurs ont peut-être choisi une solution plus gourmande en ressources pour prendre en charge les téléphones plus anciens.

En ce qui concerne JavaScript, vous avez peut-être inclus des bibliothèques de boîte à outils comme jQuery pour les sélecteurs DOM ou des polyfills comme le regenerator-runtime pour prendre en charge async/await.

Il est rare qu’un polyfill basé sur JavaScript soit plus rapide que l’équivalent natif d'Electron. Ne ralentissez pas votre application Electron en livrant votre propre version des fonctionnalités standard de la plate-forme Web.

Comment ?

Raisonner en supposant que les polyfills sont inutiles dans les versions en cours d’Electron. Si vous avez des doutes, vérifiez caniuse.com et vérifiez si la version de Chromium utilisée dans votre version Electron prend en charge la fonctionnalité souhaitée.

Examinez, en plus et avec attention, les bibliothèques que vous utilisez. Sont-elles vraiment nécessaires ? jQuery, par exemple, a eu un tel succès que plusieurs de ses fonctionnalités font maintenant partie du jeu de fonctionnalités standards de JavaScript.

Si vous utilisez un transpileur/compilateur comme TypeScript, examinez sa configuration et assurez-vous de cibler la dernière version d’ECMAScript prise en charge par Electron.

6. Faire la chasse aux requêtes réseau inutiles ou bloquantes

Évitez d'aller récupérer des ressources sur Internet si elles ne sont modifiées que très rarement et qu'elles peuvent facilement être livrées avec votre application.

Pourquoi ?

De nombreux utilisateurs d'Electron commencent avec une application complètement web qu'ils transforment en une application de bureau. En tant que développeurs Web, nous avons l’habitude de charger des ressources à partir d’une variété de réseaux de diffusion de contenu(cdn). Mais, maintenant que vous allez livrer une application de bureau, essayez dans la mesure du possible, de « couper le cordon » et évitez ainsi à vos utilisateurs d'attendre des ressources qui ne changent jamais et qui pourraient facilement être incluses dans votre application.

Les Google Fonts sont un exemple typique. De nombreux développeurs utilisent l'impressionnante collection de polices gratuites de Google, disponible sur cdn. L'utilisation est très simple : On inclus quelques lignes de CSS et Google s'occupera du reste.

Lors de la création d’une application Electron, vos utilisateurs attendront moins si vous téléchargez les polices et les regroupez avec votre application.

Comment ?

Dans un monde idéal, votre application n’aurait pas du tout besoin du réseau pour fonctionner. Pour y parvenir, vous devez comprendre quelles ressources sont téléchargées par votre application - et connaitre leur taille.

Pour ce faire, ouvrez les outils de développement. Naviguez vers l'onglet Réseau et cochez l'option Désactiver le cache. Puis, rechargez votre moteur de rendu. À moins que votre application n’interdise de tels rechargements, vous pouvez généralement déclencher un rechargement en appuyant sur Cmd + R ou Ctrl + R lorsque les devTools ont le focus.

Les devTools enregistreront minutieusement toutes les requêtes sur le réseau. Dans un premier temps, faites le point sur toutes les ressources en cours de téléchargement, en se concentrant d'abord sur les plus gros fichiers . Y a-t-il des images, des polices ou des fichiers multimédias qui ne changent pas et pourraient être inclus dans votre bundle ? Si c’est le cas, incluez-les.

Il vous faut ensuite activer la Limitation du Réseau. Recherchez dans la liste déroulante qui affiche par défaut Aucune limitation et sélectionnez un préréglage plus lent tel que 3G Rapide. Rechargez votre moteur de rendu et observez si votre application attend inutilement certaines ressources. . Dans de nombreux cas, une application attendra qu’une demande réseau se termine sans avoir réellement besoin de la ressource concernée.

À titre d’astuce, le chargement à partir d’Internet de ressources que vous souhaiterez peut-être modifier est une stratégie efficace car dans ce cas il ne vous sera pas obligatoire d'expédier une mise à jour de l’application. Pour un contrôle plus avancé de la façon dont les ressources sont chargées, envisagez l'utilisation des Service Workers.

7. Regroupez votre code

Comme nous l'avons déjà souligné dans «Ne pas charger ni exécuter du code plus tôt que nécessaire», l'appel à require() est une opération coûteuse. Il vaut mieux regrouper le code de votre application dans un seul fichier, si vous êtes en mesure de le faire,.

Pourquoi ?

Le développement moderne en JavaScript implique généralement de nombreux fichiers et modules. Bien que cela ne pose pas de problème pour le développement avec Electron, nous vous recommandons fortement de regrouper tout votre code en un seul fichier pour vous assurer que le temps perdu par l'appel à require() n'interviendra qu'une seule fois lors du chargement de l'application.

Comment ?

Il existe de nombreux empaqueteurs (bundlers) JavaScript et il est préférable pour ne facher personne de ne pas en recommander un plutôt qu'un autre. Nous vous recommandons toutefois d’utiliser un bundler capable de gérer l’environnement unique d’Electron qui doit gérer à la fois les environnements de Node.js et du navigateur.

Au moment de la rédaction de cet article, les choix appréciés incluent Webpack, Parcelet rollup.js.

8. Call Menu.setApplicationMenu(null) when you do not need a default menu

Electron will set a default menu on startup with some standard entries. But there are reasons your application might want to change that and it will benefit startup performance.

Pourquoi ?

If you build your own menu or use a frameless window without native menu, you should tell Electron early enough to not setup the default menu.

Comment ?

Call Menu.setApplicationMenu(null) before app.on("ready"). This will prevent Electron from setting a default menu. See also https://github.com/electron/electron/issues/35512 for a related discussion.