Erstellen Sie ein Node.js-Tool zum Aufzeichnen und Vergleichen von Google Lighthouse Berichten

Avatar of Luke Harrison
Luke Harrison am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

In diesem Tutorial zeige ich Ihnen Schritt für Schritt, wie Sie ein einfaches Tool in Node.js erstellen, um Google Lighthouse-Audits über die Befehlszeile auszuführen, die generierten Berichte im JSON-Format zu speichern und sie dann zu vergleichen, damit die Web-Performance im Zuge des Wachstums und der Entwicklung der Website überwacht werden kann.

Ich hoffe, dies kann als gute Einführung für jeden Entwickler dienen, der daran interessiert ist, programmatisch mit Google Lighthouse zu arbeiten.

Aber zuerst, für die Unkundigen...

Was ist Google Lighthouse?

Google Lighthouse ist eines der besten automatisierten Tools, die einem Webentwickler zur Verfügung stehen. Es ermöglicht Ihnen, eine Website schnell in einer Reihe von Schlüsselbereichen zu überprüfen, die zusammen ein Maß für ihre Gesamtqualität bilden können. Diese sind:

  • Leistung
  • Barrierefreiheit
  • Best Practices
  • SEO
  • Progressive Web App

Nach Abschluss des Audits wird ein Bericht generiert, der zeigt, was Ihre Website gut macht... und was nicht so gut, wobei Letzteres als Indikator dafür dienen soll, welche nächsten Schritte Sie zur Verbesserung der Seite unternehmen sollten.

Hier sieht ein vollständiger Bericht aus. 

Zusammen mit anderen allgemeinen Diagnostiken und Web-Performance-Metriken ist eine wirklich nützliche Funktion des Berichts, dass jeder der Schlüsselbereiche in farbcodierte Bewertungen zwischen 0 und 100 aggregiert wird.

Dies ermöglicht es Entwicklern nicht nur, die Qualität einer Website schnell einzuschätzen, ohne weitere Analyse, sondern auch Nicht-Technikern wie Stakeholdern oder Kunden, sie zu verstehen.

Zum Beispiel bedeutet dies, dass es viel einfacher ist, den Erfolg mit Heather aus dem Marketing zu teilen, nachdem Zeit für die Verbesserung der Website-Zugänglichkeit aufgewendet wurde, da sie die Anstrengung besser nachvollziehen kann, wenn sie sieht, wie die Lighthouse-Zugänglichkeitsbewertung um 50 Punkte ins Grüne steigt.

Aber gleichermaßen mag Simon, der Projektmanager, nicht verstehen, was Speed Index oder First Contentful Paint bedeutet, aber wenn er sieht, dass der Lighthouse-Bericht die Website-Performance-Bewertung tief im roten Bereich zeigt, weiß er, dass Sie noch Arbeit zu tun haben.

Wenn Sie Chrome oder die neueste Version von Edge verwenden, können Sie jetzt mit DevTools selbst ein Lighthouse-Audit durchführen. Hier ist, wie es geht.

Sie können ein Lighthouse-Audit auch online über PageSpeed Insights oder über beliebte Performance-Tools wie WebPageTest durchführen.

Heute sind wir jedoch nur an Lighthouse als Node-Modul interessiert, da dies uns ermöglicht, das Tool programmatisch zu verwenden, um Web-Performance-Metriken zu auditieren, aufzuzeichnen und zu vergleichen.

Lassen Sie uns herausfinden, wie.

Setup

Zunächst einmal, wenn Sie es noch nicht haben, benötigen Sie Node.js. Es gibt tausend verschiedene Möglichkeiten, es zu installieren. Ich benutze den Homebrew-Paketmanager, aber Sie können auch einen Installer direkt von der Node.js-Website herunterladen, wenn Sie möchten. Dieses Tutorial wurde mit Blick auf Node.js v10.17.0 geschrieben, wird aber höchstwahrscheinlich auch mit den meisten Versionen, die in den letzten Jahren veröffentlicht wurden, gut funktionieren.

Sie benötigen auch Chrome, da wir damit die Lighthouse-Audits ausführen werden.

Erstellen Sie als Nächstes ein neues Verzeichnis für das Projekt und wechseln Sie dann mit cd in der Konsole dorthin. Führen Sie dann npm init aus, um eine package.json-Datei zu erstellen. Zu diesem Zeitpunkt empfehle ich, einfach immer wieder die Eingabetaste zu drücken, um so viel wie möglich zu überspringen, bis die Datei erstellt ist.

Erstellen Sie nun eine neue Datei im Projektverzeichnis. Ich habe meine lh.js genannt, aber Sie können sie gerne nennen, wie Sie möchten. Diese Datei wird den gesamten JavaScript-Code für das Tool enthalten. Öffnen Sie sie in Ihrem Texteditor Ihrer Wahl und schreiben Sie vorerst eine console.log-Anweisung.

console.log('Hello world');

Stellen Sie dann sicher, dass Ihr CWD (aktuelles Arbeitsverzeichnis) in der Konsole Ihr Projektverzeichnis ist, und führen Sie node lh.js aus, wobei Sie meinen Dateinamen durch den von Ihnen verwendeten ersetzen.

Sie sollten sehen

$ node lh.js
Hello world

Wenn nicht, überprüfen Sie, ob Ihre Node-Installation funktioniert und Sie sich definitiv im richtigen Projektverzeichnis befinden.

Jetzt, wo das erledigt ist, können wir mit der Entwicklung des Tools selbst fortfahren.

Chrome mit Node.js öffnen

Lassen Sie uns die erste Abhängigkeit unseres Projekts installieren: Lighthouse selbst.

npm install lighthouse --save-dev

Dies erstellt ein Verzeichnis node_modules, das alle Dateien des Pakets enthält. Wenn Sie Git verwenden, ist das Einzige, was Sie damit tun sollten, es zu Ihrer .gitignore-Datei hinzufügen.

In lh.js müssen Sie als Nächstes die Test-console.log() löschen und das Lighthouse-Modul importieren, damit Sie es in Ihrem Code verwenden können. Wie folgt:

const lighthouse = require('lighthouse');

Darunter müssen Sie auch ein Modul namens chrome-launcher importieren, das eine der Abhängigkeiten von Lighthouse ist und es Node ermöglicht, Chrome selbst zu starten, damit das Audit ausgeführt werden kann.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

Nun, da wir Zugriff auf diese beiden Module haben, erstellen wir ein einfaches Skript, das einfach Chrome öffnet, ein Lighthouse-Audit ausführt und dann den Bericht in der Konsole ausgibt.

Erstellen Sie eine neue Funktion, die eine URL als Parameter akzeptiert. Da wir dies mit Node.js ausführen, können wir ES6-Syntax sicher verwenden, da wir uns keine Sorgen um die lästigen Internet Explorer-Benutzer machen müssen.

const launchChrome = (url) => {

}

Innerhalb der Funktion müssen wir als Erstes Chrome mit dem importierten chrome-launcher-Modul öffnen und es an die über den Parameter url übergebene Adresse senden. 

Dies können wir mit seiner launch()-Methode und der Option startingUrl tun.

const launchChrome = url => {
  chromeLauncher.launch({
    startingUrl: url
  });
};

Wenn Sie die folgende Funktion aufrufen und eine URL Ihrer Wahl übergeben, wird Chrome unter dieser URL geöffnet, wenn das Node-Skript ausgeführt wird.

launchChrome('https://www.lukeharrison.dev');

Die launch-Funktion gibt tatsächlich ein Promise zurück, das uns Zugriff auf ein Objekt mit einigen nützlichen Methoden und Eigenschaften gibt.

Zum Beispiel können wir mit dem folgenden Code Chrome öffnen, das Objekt in der Konsole ausgeben und dann Chrome drei Sekunden später mit seiner kill()-Methode schließen.

const launchChrome = url => {
  chromeLauncher
    .launch({
      startingUrl: url
    })
    .then(chrome => {
      console.log(chrome);
      setTimeout(() => chrome.kill(), 3000);
    });
};

launchChrome("https://www.lukeharrison.dev");

Nachdem wir Chrome eingerichtet haben, gehen wir zu Lighthouse über.

Lighthouse programmatisch ausführen

Zuerst benennen wir unsere Funktion launchChrome() in etwas um, das ihre endgültige Funktionalität besser widerspiegelt: launchChromeAndRunLighthouse(). Nachdem der schwierige Teil erledigt ist, können wir nun das Lighthouse-Modul verwenden, das wir im Tutorial zuvor importiert haben.

In der then-Funktion des Chrome-Launchers, die nur ausgeführt wird, wenn der Browser geöffnet ist, übergeben wir Lighthouse das url-Argument der Funktion und lösen ein Audit dieser Website aus.

const launchChromeAndRunLighthouse = url => {
  chromeLauncher
    .launch({
      startingUrl: url
    })
    .then(chrome => {
      const opts = {
        port: chrome.port
      };
      lighthouse(url, opts);
    });
};

launchChromeAndRunLighthouse("https://www.lukeharrison.dev");

Um die Lighthouse-Instanz mit unserem Chrome-Browserfenster zu verknüpfen, müssen wir dessen Port zusammen mit der URL übergeben.

Wenn Sie dieses Skript jetzt ausführen würden, würden Sie einen Fehler in der Konsole erhalten.

(node:47714) UnhandledPromiseRejectionWarning: Error: You probably have multiple tabs open to the same origin.

Um dies zu beheben, müssen wir einfach die Option startingUrl aus dem Chrome Launcher entfernen und Lighthouse die URL-Navigation ab hier übernehmen lassen.

const launchChromeAndRunLighthouse = url => {
  chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    lighthouse(url, opts);
  });
};

Wenn Sie diesen Code ausführen, werden Sie feststellen, dass definitiv etwas passiert. Wir erhalten jedoch keine Rückmeldung in der Konsole, um zu bestätigen, dass das Lighthouse-Audit definitiv ausgeführt wurde, noch schließt sich die Chrome-Instanz von selbst wie zuvor.

Glücklicherweise gibt die lighthouse()-Funktion ein Promise zurück, das uns Zugriff auf die Audit-Ergebnisse ermöglicht.

Lassen Sie uns Chrome schließen und dann diese Ergebnisse im JSON-Format über die report-Eigenschaft des Ergebnisobjekts in das Terminal ausgeben.

const launchChromeAndRunLighthouse = url => {
  chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    lighthouse(url, opts).then(results => {
      chrome.kill();
      console.log(results.report);
    });
  });
};

Obwohl die Konsole nicht der beste Weg ist, diese Ergebnisse anzuzeigen, wenn Sie sie in Ihre Zwischenablage kopieren und den Lighthouse Report Viewer besuchen, wird das Einfügen hier den Bericht in seiner ganzen Pracht anzeigen.

An diesem Punkt ist es wichtig, den Code etwas aufzuräumen, damit die Funktion launchChromeAndRunLighthouse() den Bericht zurückgibt, sobald sie ihre Ausführung beendet hat. Dies ermöglicht es uns, den Bericht später zu verarbeiten, ohne eine unübersichtliche JavaScript-Pyramide zu erhalten.

const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");

const launchChromeAndRunLighthouse = url => {
  return chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    return lighthouse(url, opts).then(results => {
      return chrome.kill().then(() => results.report);
    });
  });
};

launchChromeAndRunLighthouse("https://www.lukeharrison.dev").then(results => {
  console.log(results);
});

Eine Sache, die Sie vielleicht bemerkt haben, ist, dass unser Tool derzeit nur eine einzige Website auditieren kann. Ändern wir dies, damit Sie die URL als Argument über die Befehlszeile übergeben können.

Um die Arbeit mit Befehlszeilenargumenten zu erleichtern, werden wir sie mit einem Paket namens yargs behandeln.

npm install --save-dev yargs

Importieren Sie es dann oben in Ihrem Skript zusammen mit Chrome Launcher und Lighthouse. Wir brauchen hier nur seine argv-Funktion.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;

Das bedeutet, wenn Sie ein Befehlszeilenargument im Terminal wie folgt übergeben:

node lh.js --url https://www.google.co.uk

...können Sie das Argument im Skript wie folgt darauf zugreifen:

const url = argv.url // https://www.google.co.uk

Bearbeiten wir unser Skript, um das Befehlszeilen-URL-Argument an den url-Parameter der Funktion zu übergeben. Es ist wichtig, ein kleines Sicherheitsnetz über die if-Anweisung und die Fehlermeldung hinzuzufügen, falls kein Argument übergeben wird.

if (argv.url) {
  launchChromeAndRunLighthouse(argv.url).then(results => {
    console.log(results);
  });
} else {
  throw "You haven't passed a URL to Lighthouse";
}

Tada! Wir haben ein Tool, das Chrome startet und ein Lighthouse-Audit programmatisch ausführt, bevor es den Bericht im JSON-Format in das Terminal ausgibt.

Lighthouse-Berichte speichern

Wenn der Bericht in der Konsole ausgegeben wird, ist das nicht sehr nützlich, da Sie seinen Inhalt nicht leicht lesen können und er nicht für zukünftige Verwendungen gespeichert wird. In diesem Abschnitt des Tutorials werden wir dieses Verhalten ändern, sodass jeder Bericht in seiner eigenen JSON-Datei gespeichert wird.

Um zu verhindern, dass Berichte von verschiedenen Websites vermischt werden, werden wir sie wie folgt organisieren:

  • lukeharrison.dev
    • 2020-01-31T18:18:12.648Z.json
    • 2020-01-31T19:10:24.110Z.json
  • cnn.com
    • 2020-01-14T22:15:10.396Z.json
  • lh.js

Wir werden die Berichte mit einem Zeitstempel benennen, der das Datum/die Uhrzeit angibt, zu der der Bericht generiert wurde. Das bedeutet, dass niemals zwei Berichtsdateinamen gleich sind und es uns hilft, Berichte leicht zu unterscheiden.

Es gibt ein Problem unter Windows, das unsere Aufmerksamkeit erfordert: der Doppelpunkt (:) ist ein ungültiges Zeichen für Dateinamen. Um dieses Problem zu mildern, ersetzen wir alle Doppelpunkte durch Unterstriche (_), sodass ein typischer Berichtsdateiname wie folgt aussieht:

  • 2020-01-31T18_18_12.648Z.json

Verzeichnis erstellen

Zuerst müssen wir das Befehlszeilen-URL-Argument manipulieren, damit wir es für den Verzeichnisnamen verwenden können.

Dies beinhaltet mehr als nur das Entfernen von www, da es Audits auf Webseiten berücksichtigen muss, die sich nicht auf der Root-Ebene befinden (z. B. www.foo.com/bar), da die Schrägstriche ungültige Zeichen für Verzeichnisnamen sind. 

Für diese URLs ersetzen wir die ungültigen Zeichen wieder durch Unterstriche. Auf diese Weise wird bei einem Audit von https://www.foo.com/bar der resultierende Verzeichnisname, der den Bericht enthält, zu foo.com_bar.

Um die Arbeit mit URLs zu erleichtern, verwenden wir ein natives Node.js-Modul namens url. Dieses kann wie jedes andere Paket importiert werden, ohne es zur package.json hinzufügen und über npm abrufen zu müssen.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');

Als Nächstes verwenden wir es, um ein neues URL-Objekt zu instanziieren.

if (argv.url) {
  const urlObj = new URL(argv.url);

  launchChromeAndRunLighthouse(argv.url).then(results => {
    console.log(results);
  });
}

Wenn Sie urlObj in der Konsole ausgeben würden, würden Sie viele nützliche URL-Daten sehen, die wir verwenden können.

$ node lh.js --url https://www.foo.com/bar
URL {
  href: 'https://www.foo.com/bar',
  origin: 'https://www.foo.com',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'www.foo.com',
  hostname: 'www.foo.com',
  port: '',
  pathname: '/bar',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}

Erstellen Sie eine neue Variable namens dirName und verwenden Sie die Methode replace() der Zeichenkette auf der host-Eigenschaft unserer URL, um www sowie das https-Protokoll zu entfernen.

const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace('www.','');

Wir haben hier let verwendet, das im Gegensatz zu const neu zugewiesen werden kann, da wir den Verweis aktualisieren müssen, wenn die URL einen Pfad hat, um Schrägstriche durch Unterstriche zu ersetzen. Dies kann mit einem regulären Ausdruckmuster erfolgen und sieht wie folgt aus:

const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "/") {
  dirName = dirName + urlObj.pathname.replace(/\//g, "_");
}

Nun können wir das Verzeichnis selbst erstellen. Dies kann durch die Verwendung eines weiteren nativen Node.js-Moduls namens fs (kurz für "File System") erfolgen.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
const fs = require('fs');

Wir können seine mkdir()-Methode verwenden, um ein Verzeichnis zu erstellen, müssen aber zuerst seine existsSync()-Methode verwenden, um zu prüfen, ob das Verzeichnis bereits existiert, da Node.js sonst einen Fehler auslösen würde.

const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "/") {
  dirName = dirName + urlObj.pathname.replace(/\//g, "_");
}
if (!fs.existsSync(dirName)) {
  fs.mkdirSync(dirName);
}

Das Testen des Skripts zu diesem Zeitpunkt sollte zur Erstellung eines neuen Verzeichnisses führen. Die Übergabe von https://www.bbc.co.uk/news als URL-Argument würde zu einem Verzeichnis namens bbc.co.uk_news führen.

Bericht speichern

In der then-Funktion von launchChromeAndRunLighthouse() möchten wir das vorhandene console.log durch Logik zum Schreiben des Berichts auf die Festplatte ersetzen. Dies kann mit der writeFile()-Methode des fs-Moduls erfolgen.

launchChromeAndRunLighthouse(argv.url).then(results => {
  fs.writeFile("report.json", results, err => {
    if (err) throw err;
  });
});

Der erste Parameter repräsentiert den Dateinamen, der zweite den Inhalt der Datei und der dritte eine Callback-Funktion, die ein Fehlerobjekt enthält, falls während des Schreibvorgangs etwas schief geht. Dies würde eine neue Datei namens report.json erstellen, die das zurückgebene Lighthouse-Bericht-JSON-Objekt enthält.

Wir müssen es immer noch in das richtige Verzeichnis senden, mit einem Zeitstempel als Dateiname. Ersteres ist einfach – wir übergeben die zuvor erstellte Variable dirName, wie folgt:

launchChromeAndRunLighthouse(argv.url).then(results => {
  fs.writeFile(`${dirName}/report.json`, results, err => {
    if (err) throw err;
  });
});

Letzteres erfordert jedoch, dass wir irgendwie einen Zeitstempel abrufen, wann der Bericht generiert wurde. Glücklicherweise erfasst der Bericht selbst dies als Datenpunkt und speichert es als fetchTime-Eigenschaft. 

Wir müssen nur daran denken, alle Doppelpunkte (:) durch Unterstriche (_) zu ersetzen, damit er unter Windows-Dateisystemen funktioniert.

launchChromeAndRunLighthouse(argv.url).then(results => {
  fs.writeFile(
    `${dirName}/${results["fetchTime"].replace(/:/g, "_")}.json`,
    results,
    err => {
      if (err) throw err;
    }
  );
});

Wenn Sie dies jetzt ausführen würden, würden Sie anstelle eines Dateinamens timestamped.json wahrscheinlich einen Fehler wie diesen sehen:

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined

Das passiert, weil Lighthouse den Bericht derzeit im JSON-Format zurückgibt, anstatt als Objekt, das von JavaScript verarbeitet werden kann.

Glücklicherweise können wir, anstatt das JSON selbst zu parsen, Lighthouse einfach bitten, den Bericht als reguläres JavaScript-Objekt zurückzugeben.

Dazu muss die folgende Zeile geändert werden:

return chrome.kill().then(() => results.report);

...zu

return chrome.kill().then(() => results.lhr);

Wenn Sie das Skript nun erneut ausführen, wird die Datei korrekt benannt. Wenn sie jedoch geöffnet wird, ist ihr Inhalt leider nur...

[object Object]

Das liegt daran, dass wir jetzt das gegenteilige Problem haben wie zuvor. Wir versuchen, ein JavaScript-Objekt zu rendern, ohne es zuerst in ein JSON-Objekt zu stringifizieren.

Die Lösung ist einfach. Um nicht unnötig Ressourcen für das Parsen oder Stringifizieren dieses riesigen Objekts zu verschwenden, können wir *beide* Typen von Lighthouse zurückgeben:

return lighthouse(url, opts).then(results => {
  return chrome.kill().then(() => {
    return {
      js: results.lhr,
      json: results.report
    };
  });
});

Dann können wir die writeFile-Instanz wie folgt modifizieren:

fs.writeFile(
  `${dirName}/${results.js["fetchTime"].replace(/:/g, "_")}.json`,
  results.json,
  err => {
    if (err) throw err;
  }
);

Erledigt! Nach Abschluss des Lighthouse-Audits sollte unser Tool den Bericht nun in einer Datei mit einem eindeutigen, zeitgestempelten Dateinamen in einem Verzeichnis speichern, das nach der Website-URL benannt ist.

Dies bedeutet, dass Berichte jetzt viel effizienter organisiert sind und sich nicht gegenseitig überschreiben, egal wie viele Berichte gespeichert werden.

Lighthouse-Berichte vergleichen

Während der alltäglichen Entwicklung, wenn ich mich auf die Verbesserung der Performance konzentriere, könnte die Möglichkeit, Berichte sehr schnell direkt in der Konsole zu vergleichen und zu sehen, ob ich auf dem richtigen Weg bin, äußerst nützlich sein. Vor diesem Hintergrund sollten die Anforderungen dieser Vergleichsfunktion sein:

  1. Wenn ein vorheriger Bericht für dieselbe Website bereits existiert, wenn ein Lighthouse-Audit abgeschlossen ist, führe automatisch einen Vergleich damit durch und zeige alle Änderungen an wichtigen Performance-Metriken an.
  2. Ich sollte auch in der Lage sein, wichtige Performance-Metriken aus beliebigen zwei Berichten von beliebigen zwei Websites zu vergleichen, ohne einen neuen Lighthouse-Bericht generieren zu müssen, den ich möglicherweise nicht benötige.

Welche Teile eines Berichts sollten verglichen werden? Dies sind die numerischen Kennzahlen zur Leistung, die als Teil jedes Lighthouse-Berichts gesammelt werden. Sie geben Aufschluss über die objektive und wahrgenommene Leistung einer Website.

Darüber hinaus sammelt Lighthouse auch andere Metriken, die nicht in diesem Teil des Berichts aufgeführt sind, aber dennoch in einem geeigneten Format vorliegen, um in den Vergleich aufgenommen zu werden. Dies sind:

  • Zeit bis zum ersten Byte – Time To First Byte identifiziert die Zeit, zu der Ihr Server eine Antwort sendet.
  • Gesamte Blockierungszeit – Summe aller Zeitperioden zwischen FCP und Time to Interactive, wenn die Aufgabendauer 50 ms überschritt, ausgedrückt in Millisekunden.
  • Geschätzte Eingabelatenz – Geschätzte Eingabelatenz ist eine Schätzung, wie lange Ihre App braucht, um auf Benutzereingaben zu reagieren, in Millisekunden, während des geschäftigsten 5-Sekunden-Fensters des Seitenaufrufs. Wenn Ihre Latenz höher als 50 ms ist, kann es sein, dass Ihre App als träge empfunden wird.

Wie soll der Metrikvergleich in der Konsole ausgegeben werden? Wir erstellen einen einfachen prozentbasierten Vergleich unter Verwendung der alten und neuen Metriken, um zu sehen, wie sie sich von Bericht zu Bericht geändert haben.

Um ein schnelles Scannen zu ermöglichen, werden wir einzelne Metriken auch farblich kennzeichnen, je nachdem, ob sie schneller, langsamer oder unverändert sind.

Wir streben diese Ausgabe an:

First Contentful Paint is 0.49% slower
First Meaningful Paint is 0.47% slower
Speed Index is 12.92% slower
Estimated Input Latency is the same
Total Blocking Time is 85.71% faster
Max Potential First Input Delay is 10.53% faster
Time to first byte is 19.89% slower
First CPU Idle is 0.47% slower
Time to Interactive is 0.02% slower

Vergleichen Sie den neuen Bericht mit dem vorherigen Bericht

Beginnen wir mit der Erstellung einer neuen Funktion namens compareReports() direkt unter unserer Funktion launchChromeAndRunLighthouse(), die die gesamte Vergleichslogik enthalten wird. Wir geben ihr zwei Parameter – from und to –, um die beiden für den Vergleich verwendeten Berichte zu akzeptieren.

Vorläufig geben wir als Platzhalter nur einige Daten aus jedem Bericht in die Konsole aus, um zu überprüfen, ob sie korrekt empfangen werden.

const compareReports = (from, to) => {
  console.log(from["finalUrl"] + " " + from["fetchTime"]);
  console.log(to["finalUrl"] + " " + to["fetchTime"]);
};

Da dieser Vergleich nach der Erstellung eines neuen Berichts beginnen würde, sollte die Logik zur Ausführung dieser Funktion in der then-Funktion von launchChromeAndRunLighthouse() sitzen.

Wenn sich beispielsweise 30 Berichte in einem Verzeichnis befinden, müssen wir bestimmen, welcher der aktuellste ist und ihn als vorherigen Bericht festlegen, mit dem der neue verglichen wird. Glücklicherweise haben wir uns bereits entschieden, einen Zeitstempel als Dateinamen für einen Bericht zu verwenden, sodass wir etwas haben, womit wir arbeiten können.

Zunächst müssen wir alle vorhandenen Berichte sammeln. Um diesen Prozess zu vereinfachen, werden wir eine neue Abhängigkeit namens glob installieren, die Mustererkennung bei der Suche nach Dateien ermöglicht. Dies ist entscheidend, da wir nicht vorhersagen können, wie viele Berichte existieren werden oder wie sie heißen werden.

Installieren Sie es wie jede andere Abhängigkeit.

npm install glob --save-dev

Importieren Sie es dann am Anfang der Datei wie gewohnt.

const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
const fs = require('fs');
const glob = require('glob');

Wir werden glob verwenden, um alle Berichte im Verzeichnis zu sammeln, dessen Namen wir bereits über die Variable dirName kennen. Es ist wichtig, seine sync-Option auf true zu setzen, da wir nicht möchten, dass die JavaScript-Ausführung fortgesetzt wird, bis wir wissen, wie viele andere Berichte existieren.

launchChromeAndRunLighthouse(argv.url).then(results => {
  const prevReports = glob(`${dirName}/*.json`, {
    sync: true
  });

  // et al

});

Dieser Prozess gibt ein Array von Pfaden zurück. Wenn das Berichtsverzeichnis also so aussehen würde:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

...dann würde das resultierende Array so aussehen:

[
 'lukeharrison.dev/2020-01-31T10_18_12.648Z.json',
 'lukeharrison.dev/2020-01-31T10_18_24.110Z.json'
]

Da wir einen Vergleich nur durchführen können, wenn ein vorheriger Bericht existiert, verwenden wir dieses Array als Bedingung für die Vergleichslogik.

const prevReports = glob(`${dirName}/*.json`, {
  sync: true
});

if (prevReports.length) {
}

Wir haben eine Liste von Berichtsdateipfaden und müssen ihre zeitgestempelten Dateinamen vergleichen, um festzustellen, welcher der aktuellste ist.

Das bedeutet, wir müssen zuerst eine Liste aller Dateinamen sammeln, irrelevante Daten wie Verzeichnisnamen abschneiden und sorgfältig darauf achten, die Unterstriche (_) wieder durch Doppelpunkte (:) zu ersetzen, um sie wieder in gültige Daten umzuwandeln. Der einfachste Weg, dies zu tun, ist die Verwendung von path, einem weiteren nativen Node.js-Modul.

const path = require('path');

Wenn Sie den Pfad als Argument an seine parse-Methode übergeben, wie folgt:

path.parse('lukeharrison.dev/2020-01-31T10_18_24.110Z.json');

Gibt dieses nützliche Objekt zurück:

{
  root: '',
  dir: 'lukeharrison.dev',
  base: '2020-01-31T10_18_24.110Z.json',
  ext: '.json',
  name: '2020-01-31T10_18_24.110Z'
}

Daher können wir, um eine Liste aller Zeitstempel-Dateinamen zu erhalten, dies tun:

if (prevReports.length) {
  dates = [];
  for (report in prevReports) {
    dates.push(
      new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))
    );
  }
}

Was wieder, wenn unser Verzeichnis so aussehen würde:

  • lukeharrison.dev
    • 2020-01-31T10_18_12.648Z.json
    • 2020-01-31T10_18_24.110Z.json

Würde zu folgendem Ergebnis führen:

[
 '2020-01-31T10:18:12.648Z',
 '2020-01-31T10:18:24.110Z'
]

Eine nützliche Eigenschaft von Daten ist, dass sie von Natur aus vergleichbar sind.

const alpha = new Date('2020-01-31');
const bravo = new Date('2020-02-15');

console.log(alpha > bravo); // false
console.log(bravo > alpha); // true

Daher können wir mit einer reduce-Funktion unser Array von Daten so lange reduzieren, bis nur das aktuellste übrig bleibt.

dates = [];
for (report in prevReports) {
  dates.push(new Date(path.parse(prevReports[report]).name.replace(/_/g, ":")));
}
const max = dates.reduce(function(a, b) {
  return Math.max(a, b);
});

Wenn Sie den Inhalt von max in der Konsole ausgeben würden, würde ein UNIX-Zeitstempel angezeigt. Jetzt müssen wir nur noch eine weitere Zeile hinzufügen, um unser aktuellstes Datum zurück in das richtige ISO-Format zu konvertieren.

const max = dates.reduce(function(a, b) {
 return Math.max(a, b);
});
const recentReport = new Date(max).toISOString();

Angenommen, dies ist die Liste der Berichte:

  • 2020-01-31T23_24_41.786Z.json
  • 2020-01-31T23_25_36.827Z.json
  • 2020-01-31T23_37_56.856Z.json
  • 2020-01-31T23_39_20.459Z.json
  • 2020-01-31T23_56_50.959Z.json

Der Wert von recentReport wäre 2020-01-31T23:56:50.959Z.

Jetzt, da wir den aktuellsten Bericht kennen, müssen wir als Nächstes seinen Inhalt extrahieren. Erstellen Sie eine neue Variable namens recentReportContents unter der Variable recentReport und weisen Sie ihr eine leere Funktion zu.

Da wir wissen, dass diese Funktion immer ausgeführt werden muss, ist es sinnvoll, sie in eine IFFE (Immediately invoked function expression) zu verwandeln, die von selbst läuft, wenn der JavaScript-Parser sie erreicht, anstatt sie manuell aufzurufen. Dies wird durch die zusätzlichen Klammern signalisiert.

const recentReportContents = (() => {

})();

In dieser Funktion können wir den Inhalt des aktuellsten Berichts mit der readFileSync()-Methode des nativen fs-Moduls zurückgeben. Da dieser im JSON-Format vorliegt, ist es wichtig, ihn in ein reguläres JavaScript-Objekt zu parsen.

const recentReportContents = (() => {
  const output = fs.readFileSync(
    dirName + "/" + recentReport.replace(/:/g, "_") + ".json",
    "utf8",
    (err, results) => {
      return results;
    }
  );
  return JSON.parse(output);
})();

Und dann ist es nur noch eine Frage, die Funktion compareReports() aufzurufen und sowohl den aktuellen als auch den letzten Bericht als Argumente zu übergeben.

compareReports(recentReportContents, results.js);

Im Moment gibt dies nur einige Details in der Konsole aus, damit wir testen können, ob die Berichtsdaten korrekt durchkommen.

https://www.lukeharrison.dev/ 2020-02-01T00:25:06.918Z
https://www.lukeharrison.dev/ 2020-02-01T00:25:42.169Z

Wenn Sie zu diesem Zeitpunkt Fehler erhalten, versuchen Sie, alle report.json-Dateien oder Berichte ohne gültigen Inhalt aus früheren Teilen des Tutorials zu löschen.

Vergleichen Sie beliebige zwei Berichte

Die verbleibende Hauptanforderung war die Möglichkeit, beliebige zwei Berichte von beliebigen zwei Websites zu vergleichen. Der einfachste Weg, dies zu implementieren, wäre, dem Benutzer zu erlauben, die vollständigen Berichtsdateipfade als Befehlszeilenargumente zu übergeben, die wir dann an die Funktion compareReports() senden.

In der Befehlszeile würde dies so aussehen:

node lh.js --from lukeharrison.dev/2020-02-01T00:25:06.918Z --to cnn.com/2019-12-16T15:12:07.169Z

Um dies zu erreichen, muss die bedingte if-Anweisung, die die Anwesenheit eines URL-Befehlszeilenarguments prüft, bearbeitet werden. Wir fügen eine zusätzliche Prüfung hinzu, ob der Benutzer gerade einen from- und to-Pfad übergeben hat, andernfalls prüfen wir wie zuvor auf die URL. Auf diese Weise verhindern wir ein neues Lighthouse-Audit.

if (argv.from && argv.to) {

} else if (argv.url) {
 // et al
}

Lassen Sie uns den Inhalt dieser JSON-Dateien extrahieren, sie in JavaScript-Objekte parsen und sie dann an die Funktion compareReports() übergeben. 

Wir haben bereits zuvor JSON geparst, als wir den aktuellsten Bericht abgerufen haben. Wir können diese Funktionalität einfach in eine eigene Hilfsfunktion extrahieren und sie an beiden Stellen verwenden.

Verwenden Sie die Funktion recentReportContents() als Basis und erstellen Sie eine neue Funktion namens getContents(), die einen Dateipfad als Argument akzeptiert. Stellen Sie sicher, dass dies nur eine reguläre Funktion und keine IFFE ist, da wir nicht möchten, dass sie ausgeführt wird, sobald der JavaScript-Parser sie findet.

const getContents = pathStr => {
  const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
    return results;
  });
  return JSON.parse(output);
};

const compareReports = (from, to) => {
  console.log(from["finalUrl"] + " " + from["fetchTime"]);
  console.log(to["finalUrl"] + " " + to["fetchTime"]);
};

Aktualisieren Sie dann die Funktion recentReportContents(), damit sie stattdessen diese extrapolierte Hilfsfunktion verwendet.

const recentReportContents = getContents(dirName + '/' + recentReport.replace(/:/g, '_') + '.json');

Zurück in unserer neuen Bedingung müssen wir den Inhalt der Vergleichsberichte an die Funktion compareReports() übergeben.

if (argv.from && argv.to) {
  compareReports(
    getContents(argv.from + ".json"),
    getContents(argv.to + ".json")
  );
}

Wie zuvor sollte dies einige grundlegende Informationen über die Berichte in der Konsole ausgeben, um uns mitzuteilen, dass alles ordnungsgemäß funktioniert.

node lh.js --from lukeharrison.dev/2020-01-31T23_24_41.786Z --to lukeharrison.dev/2020-02-01T11_16_25.221Z

Würde zu folgendem Ergebnis führen:

https://www.lukeharrison.dev/ 2020-01-31T23_24_41.786Z
https://www.lukeharrison.dev/ 2020-02-01T11_16_25.221Z

Vergleichslogik

Dieser Teil der Entwicklung beinhaltet den Aufbau von Vergleichslogik, um die beiden Berichte, die von der Funktion compareReports() empfangen werden, zu vergleichen. 

Innerhalb des Objekts, das Lighthouse zurückgibt, gibt es eine Eigenschaft namens audits, die ein weiteres Objekt mit Performance-Metriken, Chancen und Informationen enthält. Es gibt viele Informationen hier, von denen wir für die Zwecke dieses Tools vieles nicht benötigen.

Hier ist der Eintrag für First Contentful Paint, eine der neun Performance-Metriken, die wir vergleichen möchten:

"first-contentful-paint": {
  "id": "first-contentful-paint",
  "title": "First Contentful Paint",
  "description": "First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://web.dev/first-contentful-paint).",
  "score": 1,
  "scoreDisplayMode": "numeric",
  "numericValue": 1081.661,
  "displayValue": "1.1 s"
}

Erstellen Sie ein Array, das die Schlüssel dieser neun Performance-Metriken auflistet. Wir können dies verwenden, um das Audit-Objekt zu filtern.

const compareReports = (from, to) => {
  const metricFilter = [
    "first-contentful-paint",
    "first-meaningful-paint",
    "speed-index",
    "estimated-input-latency",
    "total-blocking-time",
    "max-potential-fid",
    "time-to-first-byte",
    "first-cpu-idle",
    "interactive"
  ];
};

Dann werden wir durch das audits-Objekt eines der Berichte schleifen und seinen Namen mit unserer Filterliste abgleichen. (Es spielt keine Rolle, welches Audit-Objekt, da sie alle die gleiche Inhaltsstruktur haben.)

Wenn es darin enthalten ist, dann ist es großartig, wir wollen es benutzen.

const metricFilter = [
  "first-contentful-paint",
  "first-meaningful-paint",
  "speed-index",
  "estimated-input-latency",
  "total-blocking-time",
  "max-potential-fid",
  "time-to-first-byte",
  "first-cpu-idle",
  "interactive"
];

for (let auditObj in from["audits"]) {
  if (metricFilter.includes(auditObj)) {
    console.log(auditObj);
  }
}

Dieses console.log() würde die folgenden Schlüssel in der Konsole ausgeben:

first-contentful-paint
first-meaningful-paint
speed-index
estimated-input-latency
total-blocking-time
max-potential-fid
time-to-first-byte
first-cpu-idle
interactive

Das bedeutet, dass wir in dieser Schleife from['audits'][auditObj].numericValue bzw. to['audits'][auditObj].numericValue verwenden würden, um auf die Metriken selbst zuzugreifen.

Wenn wir diese mit dem Schlüssel in der Konsole ausgeben würden, würde dies zu einer Ausgabe wie dieser führen:

first-contentful-paint 1081.661 890.774
first-meaningful-paint 1081.661 954.774
speed-index 15576.70313351777 1098.622294504341
estimated-input-latency 12.8 12.8
total-blocking-time 59 31.5
max-potential-fid 153 102
time-to-first-byte 16.859999999999985 16.096000000000004
first-cpu-idle 1704.8490000000002 1918.774
interactive 2266.2835 2374.3615

Wir haben jetzt alle benötigten Daten. Wir müssen nur noch die prozentuale Differenz zwischen diesen beiden Werten berechnen und sie dann im zuvor beschriebenen farbcodierten Format in der Konsole ausgeben.

Wissen Sie, wie man die prozentuale Veränderung zwischen zwei Werten berechnet? Ich auch nicht. Glücklicherweise kam die beliebteste Suchmaschine aller Zeiten zu Hilfe.

Die Formel lautet

((From - To) / From) x 100

Nehmen wir also an, wir haben einen Speed Index von 5,7 s für den ersten Bericht (von) und dann einen Wert von 2,1 s für den zweiten (bis). Die Berechnung wäre

5.7 - 2.1 = 3.6
3.6 / 5.7 = 0.63157895
0.63157895 * 100 = 63.157895

Rundet man auf zwei Dezimalstellen, ergibt sich eine Verringerung des Speed Index um 63,16 %.

Setzen wir dies in eine Hilfsfunktion innerhalb der Funktion compareReports(), unterhalb des Arrays metricFilter.

const calcPercentageDiff = (from, to) => {
  const per = ((to - from) / from) * 100;
  return Math.round(per * 100) / 100;
};

Zurück in unserer auditObj-Bedingung können wir mit der Erstellung der endgültigen Vergleichsausgabe der Berichte beginnen.

Verwenden Sie zunächst die Hilfsfunktion, um die prozentuale Differenz für jede Metrik zu ermitteln.

for (let auditObj in from["audits"]) {
  if (metricFilter.includes(auditObj)) {
    const percentageDiff = calcPercentageDiff(
      from["audits"][auditObj].numericValue,
      to["audits"][auditObj].numericValue
    );
  }
}

Als Nächstes müssen wir Werte in diesem Format auf der Konsole ausgeben

First Contentful Paint is 0.49% slower
First Meaningful Paint is 0.47% slower
Speed Index is 12.92% slower
Estimated Input Latency is the same
Total Blocking Time is 85.71% faster
Max Potential First Input Delay is 10.53% faster
Time to first byte is 19.89% slower
First CPU Idle is 0.47% slower
Time to Interactive is 0.02% slower

Dies erfordert das Hinzufügen von Farbe zur Konsolenausgabe. In Node.js kann dies durch Übergeben eines Farb-Codes als Argument an die Funktion console.log() geschehen, wie folgt:

console.log('\x1b[36m', 'hello') // Would print 'hello' in cyan

Eine vollständige Referenz der Farb-Codes finden Sie in dieser Stackoverflow-Frage.  Wir benötigen Grün und Rot, also \x1b[32m und \x1b[31m. Für Metriken, bei denen sich der Wert nicht ändert, verwenden wir einfach Weiß. Das wäre \x1b[37m.

Abhängig davon, ob die prozentuale Steigerung eine positive oder negative Zahl ist, müssen die folgenden Dinge geschehen:

  • Die Konsolenfarbe muss sich ändern (Grün für negativ, Rot für positiv, Weiß für unverändert)
  • Der Inhalt des Konsolen-Textes muss sich ändern.
    • „[Name] ist X % langsamer“ für positive Zahlen
    • „[Name] ist X % schneller“ für negative Zahlen
    • „[Name] ist unverändert“ für Zahlen ohne prozentuale Differenz.
  • Wenn die Zahl negativ ist, möchten wir das Minus-/Negativzeichen entfernen, da sonst ein Satz wie „Speed Index ist -92,95 % schneller“ herauskommt, was keinen Sinn ergibt.

Es gibt viele Möglichkeiten, dies zu tun. Hier verwenden wir die Funktion Math.sign(), die 1 zurückgibt, wenn ihr Argument positiv ist, 0, wenn es eben 0 ist, und -1, wenn die Zahl negativ ist. Das sollte ausreichen.

for (let auditObj in from["audits"]) {
  if (metricFilter.includes(auditObj)) {
    const percentageDiff = calcPercentageDiff(
      from["audits"][auditObj].numericValue,
      to["audits"][auditObj].numericValue
    );

    let logColor = "\x1b[37m";
    const log = (() => {
      if (Math.sign(percentageDiff) === 1) {
        logColor = "\x1b[31m";
        return `${percentageDiff + "%"} slower`;
      } else if (Math.sign(percentageDiff) === 0) {
        return "unchanged";
      } else {
        logColor = "\x1b[32m";
        return `${percentageDiff + "%"} faster`;
      }
    })();
    console.log(logColor, `${from["audits"][auditObj].title} is ${log}`);
  }
}

So, da haben wir es.

Sie können neue Lighthouse-Berichte erstellen, und wenn ein früherer Bericht vorhanden ist, wird ein Vergleich durchgeführt.

Und Sie können auch beliebige zwei Berichte von beliebigen zwei Websites vergleichen.

Vollständiger Quellcode

Hier ist der vollständige Quellcode für das Tool, den Sie auch in einem Gist über den unten stehenden Link einsehen können.

const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
const argv = require("yargs").argv;
const url = require("url");
const fs = require("fs");
const glob = require("glob");
const path = require("path");

const launchChromeAndRunLighthouse = url => {
  return chromeLauncher.launch().then(chrome => {
    const opts = {
      port: chrome.port
    };
    return lighthouse(url, opts).then(results => {
      return chrome.kill().then(() => {
        return {
          js: results.lhr,
          json: results.report
        };
      });
    });
  });
};

const getContents = pathStr => {
  const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
    return results;
  });
  return JSON.parse(output);
};

const compareReports = (from, to) => {
  const metricFilter = [
    "first-contentful-paint",
    "first-meaningful-paint",
    "speed-index",
    "estimated-input-latency",
    "total-blocking-time",
    "max-potential-fid",
    "time-to-first-byte",
    "first-cpu-idle",
    "interactive"
  ];

  const calcPercentageDiff = (from, to) => {
    const per = ((to - from) / from) * 100;
    return Math.round(per * 100) / 100;
  };

  for (let auditObj in from["audits"]) {
    if (metricFilter.includes(auditObj)) {
      const percentageDiff = calcPercentageDiff(
        from["audits"][auditObj].numericValue,
        to["audits"][auditObj].numericValue
      );

      let logColor = "\x1b[37m";
      const log = (() => {
        if (Math.sign(percentageDiff) === 1) {
          logColor = "\x1b[31m";
          return `${percentageDiff.toString().replace("-", "") + "%"} slower`;
        } else if (Math.sign(percentageDiff) === 0) {
          return "unchanged";
        } else {
          logColor = "\x1b[32m";
          return `${percentageDiff.toString().replace("-", "") + "%"} faster`;
        }
      })();
      console.log(logColor, `${from["audits"][auditObj].title} is ${log}`);
    }
  }
};

if (argv.from && argv.to) {
  compareReports(
    getContents(argv.from + ".json"),
    getContents(argv.to + ".json")
  );
} else if (argv.url) {
  const urlObj = new URL(argv.url);
  let dirName = urlObj.host.replace("www.", "");
  if (urlObj.pathname !== "/") {
    dirName = dirName + urlObj.pathname.replace(/\//g, "_");
  }

  if (!fs.existsSync(dirName)) {
    fs.mkdirSync(dirName);
  }

  launchChromeAndRunLighthouse(argv.url).then(results => {
    const prevReports = glob(`${dirName}/*.json`, {
      sync: true
    });

    if (prevReports.length) {
      dates = [];
      for (report in prevReports) {
        dates.push(
          new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))
        );
      }
      const max = dates.reduce(function(a, b) {
        return Math.max(a, b);
      });
      const recentReport = new Date(max).toISOString();

      const recentReportContents = getContents(
        dirName + "/" + recentReport.replace(/:/g, "_") + ".json"
      );

      compareReports(recentReportContents, results.js);
    }

    fs.writeFile(
      `${dirName}/${results.js["fetchTime"].replace(/:/g, "_")}.json`,
      results.json,
      err => {
        if (err) throw err;
      }
    );
  });
} else {
  throw "You haven't passed a URL to Lighthouse";
}

Gist anzeigen

Nächste Schritte

Mit der Fertigstellung dieses einfachen Google Lighthouse-Tools gibt es viele Möglichkeiten, es weiterzuentwickeln. Zum Beispiel:

  • Eine Art einfaches Online-Dashboard, das es nicht-technischen Benutzern ermöglicht, Lighthouse-Audits durchzuführen und die Entwicklung von Metriken im Laufe der Zeit zu verfolgen. Die Einbeziehung von Stakeholdern in die Web-Performance kann eine Herausforderung sein, daher könnte etwas Greifbares, mit dem sie sich selbst beschäftigen können, ihr Interesse wecken.
  • Unterstützung für Performance-Budgets aufbauen, sodass, wenn ein Bericht generiert wird und die Performance-Metriken schlechter sind als sie sein sollten, das Tool nützliche Ratschläge zur Verbesserung ausgibt (oder Sie beschimpft).

Viel Glück!