Best Practice für Performance in Erweiterungen

Einer der größten Vorteile von Firefox ist seine Erweiterbarkeit. Erweiterungen können beinahe jede beliebige Funktion erfüllen. Doch dieses Konzept hat einen Nachteil: Eine schlecht geschriebene Erweiterung kann sich stark negativ auf das Nutzungserlebnis auswirken, und auch zu allgemeinen Leistungseinbußen in Firefox führen. Der folgende Artikel soll eine Reihe von Vorschlägen liefern, um die Leistung und Geschwindigkeit Deiner Erweiterung zu erhöhen, und dadurch auch von Firefox selbst.

Die Startup Leistung Verbessern

Erweiterungen werden immer dann geladen und gestartet, wenn eine neues Browser Fenster geöffnet wird. Im Umkehrschluss bedeutet das aber, dass Deine Erweiterung einen direkten Einfluss darauf hat, wie lange ein Benutzer beim Laden einer neuen Seite warten muss. Es gibt mehrere Möglichkeiten, die Startzeit Deiner Erweiterung zu optimieren und somit die Verzögerung für den Endbenutzer zu minimieren.

Lade nur, was nötig ist

Lade keine Ressourcen beim Startup, die nicht direkt benötigt werden. Das sind Daten, die erst nach einer Benutzerinteraktion, etwa ein Klick auf einen Button, benötigt werden, oder Daten die nur bei bestimmten Einstellungen zum Tragen kommen. Auch wenn Deine Erweiterung Features anbietet, die nur funktionieren wenn der Benutzer sich in ein Service eingeloggt hat, lade die Ressourcen für diese Features erst beim tatsächlichen Login.

Nutze JavaScript Code Module

Du kannst Teile deiner Erweiterung in JavaScript code modules kapseln. Diese Module können zur Laufzeit bei Bedarf geladen werden und reduzieren somit den Ladeaufwand zum Programmstart.

Die JavaScript Code Module bieten hier einen Vorteil gegenüber XPCOM Modulen, die immer zu Beginn geladen werden.

Natürlich hängt es von der Komplexität der Erweiterung ab, ob eine Modularisierung des Codes sinnvoll ist.

Verschiebe alles, was verschoben werden kann

Die meisten Erweiterungen fangen das load event eines Fensters ab, um ihren Startup Code auszuführen. Hier sollte so wenig wie möglich getan werden. Das Browser Fenster wird so lange blockiert, bis der load Handler deiner Erweiterung abgeschlossen ist. Das bedeutet, je länger die Erweiterung dafür braucht, desto langsamer wirkt Firefox für den Benutzer.

Jede Operation, die nicht sofort ausgeführt werden muss, kann mittels einem nsITimer oder mit der window.setTimeout() Funktion für einen späteren Zeitpunkt geplant werden. Sogar kurze Verzögerungen in diesem Programmbereich können eine große Auswirkung auf die Ladegeschwindigkeit haben.

General Performance Tips

Vermeide Speicherlecks

Speicherlecks können die Leistung deiner Erweiterung stark reduzieren, weil sie dafür sorgen, dass der Garbage Collector und der Cycle Collector mehr Arbeit haben.

Sogenannte Zombiebereiche sind eine Form von Speicherlecks, die Du selbst sehr einfach entdecken und verhindern kannst. Lies dazu den Artikel zu Zombie compartments, speziell die Sektion Proactive checking of add-ons.

Im Artikel Common causes of memory leaks in extensions werden weitere Möglichkeiten, wie Du Zombiebereiche und andere Formen von Speicherlecks verhindern kannst, besprochen.

Neben der direkten Suche nach den oben genannten Lecks solltest Du auch allgemein ein Auge auf die Speichernutzung deines Addons haben und regelmäßig unter about:memory überprüfen. Als Beispiel sei bug 719601 genannt, bei dem ein "System Principal" JavaScript Bereich auf mehrere 100 MB an Speicher anwuchs, was sehr viel größer ist als im Regelfall.

Nutze JavaScript Module

JavaScript Module verhalten sich wie jeder andere JavaScript Code, mit dem feinen Unterschied, dass sie als Singletons agieren und daher von Firefox in den Cache abgelegt werden können. Dadurch können sie beim nächsten Start sehr viel effizienter geladen werden. Wann immer deine Erweiterung JavaScript Code von einem <script> Element lädt, solltest du überlegen, stattdessen ein JavaScript Modul zu nutzen. Weitere Information über JavaScript Module und ihre Verwendung werden im Artikel Using JavaScript Code Modules besprochen.

Vermeide Langsamen CSS Code

  • Lies den Leitfaden "writing efficient CSS"
  • Beachte, dass jeder Selektor, der auf viele unterschiedliche Knoten zutreffen könnte, eine Quelle von Ineffizienz darstellen kann, entweder während dem Matching oder beim Verarbeiten von Updates. Das ist speziell in letzterem Fall problematisch, wenn der Selektor dynamisch zutreffen könnte oder nicht. Ein unqualifiziertes ":hover" sollte vermieden werden wie die Pest.

Vermeide DOM Mutation Event Listeners

Durch das Hinzufügen eines DOM Mutation Listeners in einem Dokument werden die meisten DOM Mutation Optimierungen deaktiviert und die Performanz von weiteren Änderungen der DOM-Struktur des Dokuments wird stark herabgesetzt. Des weiteren kann dieser Effekt durch das Deaktivieren eines Mutation Listeners nicht wieder rückgängig gemacht werden. Die folgenden Events sollten daher strikt vermieden werden: DOMAttrModified, DOMAttributeNameChanged, DOMCharacterDataModified, DOMElementNameChanged, DOMNodeInserted, DOMNodeInsertedIntoDocument, DOMNodeRemoved, DOMNodeRemovedFromDocument, DOMSubtreeModified

Weitere Information zu diesen veralteten Events findest Du im Artikel Mutation events. Stattdessen sollten Mutation Observers benutzt werden.

Benutze Lazy Load für Services

Das JavaScript Modul XPCOMUtils bietet zwei Möglichkeiten für Lazy Loading:

  • defineLazyGetter() definiert eine Getter-Funktion für ein bestimmtes Objekt, die erst bei der erstmaligen Verwendung angelegt wird. Beispiele.
  • defineLazyServiceGetter() definiert eine Funktion für ein bestimmtes Objekt, die als Getter für ein Service fungiert. Das Service wird dabei erst aktiviert, wenn es zum ersten Mal benutzt wird. Lies den Quellcode für Beispiele.

Seit Firefox 4.0 werden viele übliche Services bereits in Services.jsm gecached.

Reduziere File I/O

TODO: Hier fehlen Beispiele, wie etwa Links zu Code, Bugs, oder Docs.

  • Wenn du mit Firefox 3.6 und darunter kompatibel sein willst, oder wenn du em:unpack benutzt, verwende chrome JARs!
  • Kombiniere dein CSS
  • Kombiniere deine Einstellungsseiten
  • Kombiniere Schnittstellen in eine einzelne .idl Datei, um xpt Dateien zu reduzieren
  • Kombiniere Toolbar Icons in eine einzelne Datei

Benutze die Richtige Kompressionsstufe für JAR und XPI Dateien

Daten von komprimierten Archiven zu lesen ist zeitaufwändig. Je stärker ein Archiv komprimiert ist, desto mehr Aufwand muss auch für das Lesen der darin befindlichen Daten erbracht werden. Daher sollten alle JAR Dateien in deiner Erweiterung mit Kompressionslevel 0 (keine Kompression) gepackt werden. Es mag kontraproduktiv klingen, aber dadurch wird zwar die Dateigröße der JAR Datei erhöht, die Größe der XPI Datei aber reduziert, weil dadurch die Möglichkeit gegeben ist, dass beim Komprimiered der XPI Datei Kompressionen der einzelnen im JAR enthaltenen Dateien stattfinden können. (Das kann als eine Art progressive Kompression bezeichnet werden).

Wenn deine Erweiterung nicht explizit em:unpack verwendet, wird das XPI file ab Firefox 4 nicht entpackt, sondern direkt genutzt. Aus diesem Grund ist eine niedrige Kompressionsstufe zu bevorzugen, wobei wir zu Kompressionslevel 1 raten. Selbst im Vergleich mit maximaler Kompression wird dadurch die Größe des Downloads nur geringfügig angehoben.

Benutze asynchrone I/O

Diese Regel kann nicht oft genug wiederholt werden: Benutze niemals synchrone I/O in einem GUI Thread.

  • Benutze keine synchronen XMLHttpRequests (XHR). Verwende stattdessen asynchrone Anfragen und zeige dem Benutzer ein Ladesymbol oder eine Nachricht, falls es zu Wartezeiten kommt.
  • Hilfsfunktionen für asynchrones Lesen und Kopieren von Dateien werden von NetUtils.jsm bereitgestellt.
  • Greife niemals synchron auf eine SQLite Datenbank zu. Benutze stattdessen die asynchrone API.

Unnötige Verwendung von onreadystatechange in XHR

Für die meisten Anwendungsfälle sind addEventListener(load/error) und/oder xhr.onload/.onerror völlig ausreichend und bieten den Vorteil, dass sie nur einmal aufgerufen werden, im Gegensatz zu onreadystatechange. In vielen Fällen wird onreadystatechange aus Kompatibilitätsgründen verwendet, wenn XHR in einer Webseite verwendet wird. Das ist oft auchreichend, um Ressourcen zu laden oder Fehler zu behandeln. Allerdings werden load/error Event Listener viel seltener aufgerufen als onreadystatechange, genauer gesagt nur einmal, und es ist nicht notwendig jedes mal den readyState  zu überprüfen oder herauszufinden, ob es sich um ein error Event handelt. onreadystatechange sollte nur benutzt werden, wenn es notwendig ist, eine Antwort noch während ihrem Einlangen zu behandeln.

Entferne Event Listeners

Entferne Event Listener, wenn sie nicht mehr benötigt werden. Es ist viel effizienter, Event Listener zu entfernen, als sie etwa durch Flags zu deaktivieren; denn bei zweiterem Ansatz muss bei jedem auftretenden Event die Flag abgefragt werden. Konstrukte wie function onMouseOver(evt) { if (is_active) { /* doSomeThing */ } } sollten also vermieden werden. Auch "Einmal-Events" sollten danach wieder deaktiviert werden:

 function init() {
   var largeArray;
   addEventListener('load', function onLoad() {
        removeEventListener('load', onLoad, true);
        largeArray.forEach();
 }, true);

Andernfalls kann es vorkommen, dass Closure Objekte des Listeners weiter referenziert werden (in obigem Beispiel die Variable largeArray). Der Listener wird dadurch weit über seine nötige Lebensdauer im Speicher gehalten.

Befülle Menüs nach Bedarf

Befülle Kontextmenüs (page, tabs, tools) nur nach Bedarf und reduziere Berechnungen auf ein Minimum, um die Reaktionsgeschwindigkeit der UI zu erhalten. Es ist nicht notwendig, bei jeder Änderung das gesamte Menü neu zu befüllen. Diese Aufgabe kann warten, bis der Benutzer das Menü tatsächlich verwenden will. Füge einen Listener für das "popupshowing" Event hinzu und erstelle/befülle das Kontextmenü dort.

Vermeide Maus-Bewegungs-Events

Vermeide die Verwendung von Mausbewegungsevents (enter, over, exit) oder minimiere zumindest die Berechnungen, die beim Auslösen eines solchen Events durchgeführt werden auf ein Minimum. Solche Events, besonders das mouseover Event, treten überlichweise sehr häufig auf. Es wird geraten, im Eventhandler nur neue Information zu speichern und die Berechnung erst dann auszuführen, wenn der Benutzer sie benötigt (zum Beispiel bei einem popupshowing Event). Vergiss auch nicht darauf, nicht mehr benötigte Event Listener auszuschalten (siehe oben).

Vermeide Polling

Benutze die nsIObserverService Funktion stattdessen. Jede Erweiterung darf via nsIObserverService eigene Benachrichtigungen versenden, aber die wenigsten benutzen diese Funktionalität. Auch viele andere Services bieten Funktionalität zur Beobachtung, etwa nslPrefBranch2.

aPNG/aGIF sind oft nicht zu Empfehlen

Animationen benötigen viel Ladezeit, weil eine große Anzahl an Bildern dekodiert werden muss (die Frames der Animation). Animierte Bilder werden häufig aus dem Cache entfernt, was dazu führt, dass sie immer wieder neu geladen werden müssen. Besonders anfällig dafür ist nsITree / <xul:tree>, das unter manchen Umständen gar kein Caching betreibt.

base64/md5/sha1 Implementierungen

Verwende keine eigenen base64/md5/sha1 Implementierungen. Die eingebauten Funktionen für base64 atob/btoa sind völlig ausreichend und können in overlay Scripts sowie in JavaScript Modulen verwendet werden. Hashes können mit nsICryptoHash, berechnet werden, das entweder einen String oder nsIInputStream akzeptiert.

Image sprites

Mehrere Bilder können in ein Sprite kombiniert werden. Siehe -moz-image-region. Die meisten XUL Widgets, die zum Anzeigen von Bildern verwendet werden können (inklusive <xul:button> und <xul:toolbarbutton>) erlauben auch die Verwendung von list-style-image. Vermeide die Benutzung der imagesrc/src Attribute für die einbettung von Bildern.

Verwende Chrome Workers

Für lange andauernde Berechnungen oder Datenverarbeitung kann ChromeWorker verwendet werden.