Beschleunigen von Diagrammen mit OffscreenCanvas

Das Rendern von Diagrammen kann den Browser ernsthaft beeinträchtigen. Insbesondere, wenn es darum geht, eine Vielzahl von Elementen auszugeben, die Diagramme in der Schnittstelle einer komplexen Anwendung darstellen. Sie können versuchen, die Situation mithilfe der Benutzeroberfläche zu verbessern OffscreenCanvas, deren Unterstützung allmählich zunimmt. Mithilfe eines Web-Workers können die Aufgaben zum Erstellen eines in einem sichtbaren Element angezeigten Bilds verschoben werden <canvas>. Der Artikel, dessen Übersetzung wir heute veröffentlichen, ist der Verwendung der Schnittstelle gewidmet . Hier werden wir darüber sprechen, warum diese Schnittstelle möglicherweise benötigt wird, was wirklich von ihrer Verwendung erwartet werden kann und welche Schwierigkeiten bei der Arbeit damit auftreten können.



OffscreenCanvas

Warum auf OffscreenCanvas achten?


Der beim Rendern des Diagramms beteiligte Code kann selbst eine ausreichend hohe Rechenkomplexität aufweisen. An vielen Stellen finden Sie detaillierte Geschichten zum Anzeigen einer reibungslosen Animation und zur Gewährleistung einer bequemen Benutzerinteraktion mit der Seite. Es ist erforderlich, dass das Rendern eines Frames innerhalb von etwa 10 ms passt, sodass Sie 60 Frames pro Sekunde erreichen können. Wenn die zur Ausgabe eines Frames erforderlichen Berechnungen länger als 10 ms dauern, führt dies zu spürbaren „Verlangsamungen“ der Seite.

Bei der Anzeige von Diagrammen wird die Situation jedoch durch Folgendes verschärft:

  • — - (SVG, canvas, WebGL) , , .
  • , , , , 10 . ( — ) .

Diese Probleme sind ideale Kandidaten für einen klassischen Optimierungsansatz namens Teilen und Erobern. In diesem Fall geht es um die Verteilung der Rechenlast auf mehrere Threads. Vor dem Erscheinen der Schnittstelle musste jedoch der OffscreenCanvasgesamte Rendering-Code im Hauptthread ausgeführt werden. Nur so kann dieser Code die benötigten APIs verwenden.

Aus technischer Sicht könnten "schwere" Berechnungen früher an einen Web-Worker-Thread ausgegeben werden. Da die für das Rendern verantwortlichen Aufrufe jedoch vom Hauptstrom aus erfolgen mussten, mussten komplexe Nachrichtenaustauschschemata zwischen dem Arbeitsstrom und dem Hauptstrom im Diagrammausgabecode verwendet werden. Darüber hinaus ergab dieser Ansatz oft nur einen geringen Leistungsgewinn.

SpezifikationOffscreenCanvasgibt uns einen Mechanismus zum Übertragen der Oberflächensteuerung zur Anzeige von Elementgrafiken <canvas>in einem Webworker. Diese Spezifikation wird derzeit von Chrome- und Edge-Browsern unterstützt (nachdem Edge auf Chromium migriert wurde). Es wird erwartet, dass der Support in Firefox OffscreenCanvasinnerhalb von sechs Monaten erfolgt. Wenn wir über Safari sprechen, ist noch nicht bekannt, ob die Unterstützung dieser Technologie in diesem Browser geplant ist.

Diagrammausgabe mit OffscreenCanvas



Ein Beispiel für ein Diagramm mit 100.000 mehrfarbigen Punkten

Um die potenziellen Vor- und Nachteile besser einschätzen zu könnenOffscreenCanvas, sehen wir uns ein Beispiel für die Ausgabe des in der vorherigen Abbildung gezeigten „schweren“ Diagramms an.

const offscreenCanvas = canvasContainer
  .querySelector('canvas')
  .transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

Zuerst fragen wir OffscreenCanvasdas Element canvasmit der neuen Methode ab transferControlToOffscreen(). Dann rufen wir die Methode worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas])auf und senden dazu eine Nachricht an den Worker, die einen Link zu enthält offscreenCanvas. Es ist sehr wichtig, dass Sie hier als zweites Argument verwenden [offscreenCanvas]. Auf diese Weise können Sie den Eigentümer dieses Objektarbeiters festlegen, sodass er ihn alleine verwalten kann OffscreenCanvas.

canvasContainer.addEventListener('measure', ({ detail }) => {
  const { width, height } = detail;
  worker.postMessage({ width, height });
});
canvasContainer.requestRedraw();

In Anbetracht der Tatsache, dass die Größen OffscreenCanvasursprünglich von den Attributen widthund dem heightElement geerbt wurden canvas, liegt es in unserer Verantwortung, diese Werte auf dem neuesten Stand zu halten, wenn die Größe des Elements geändert wird canvas. Hier nutzen wir das Ereignis measurevon d3fc-canvas, das uns in Übereinstimmung mit die Möglichkeit gibt, requestAnimationFramedem Arbeiter Informationen über die neuen Dimensionen des Elements zu übermitteln canvas.

Um das Beispiel zu vereinfachen, verwenden wir Komponenten aus der d3fc- Bibliothek . Dies ist eine Reihe von Hilfskomponenten, die entweder d3-Komponenten aggregieren oder deren Funktionalität ergänzen. Sie OffscreenCanvaskönnen ohne d3fc-Komponenten arbeiten. Alles, was besprochen wird, kann ausschließlich mit Standard-JavaScript-Funktionen durchgeführt werden.

Gehen Sie nun zum Code aus der Datei worker.js. In diesem Beispiel verwenden wir WebGL, um die Renderleistung wirklich zu steigern.

addEventListener('message', ({ data: { offscreenCanvas, width, height } }) => {
    if (offscreenCanvas != null) {
        const gl = offscreenCanvas.getContext('webgl');
        series.context(gl);
        series(data);
    }

    if (width != null && height != null) {
        const gl = series.context();
        gl.canvas.width = width;
        gl.canvas.height = height;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    }
});

Wenn wir eine Nachricht mit einer Eigenschaft erhalten canvas, gehen wir davon aus, dass die Nachricht vom Hauptthread stammt. Als nächstes erhalten wir den canvasKontext aus dem Element webglund übergeben ihn an die Komponente series. Dann rufen wir die Komponente auf seriesund übergeben ihr die Daten ( data), die wir damit rendern möchten (im Folgenden wird erläutert, woher die entsprechenden Variablen stammen ).

Darüber hinaus überprüfen wir die Eigenschaften widthund height, die in der Nachricht enthalten waren, und verwenden sie, um die Abmessungen offscreenCanvasund den Anzeigebereich von WebGL festzulegen. Der Link offscreenCanvaswird nicht direkt verwendet. Tatsache ist, dass die Nachricht entweder eine Eigenschaft offscreenCanvasoder Eigenschaften widthund enthält height.

Der verbleibende Worker-Code ist für das Einrichten des Renderings der Ausgabe verantwortlich. Hier gibt es nichts Besonderes. Wenn Ihnen all dies bekannt ist, können Sie sofort mit dem nächsten Abschnitt fortfahren, in dem wir die Leistung besprechen.

const randomNormal = d3.randomNormal(0, 1);
const randomLogNormal = d3.randomLogNormal();

const data = Array.from({ length: 1e5 }, () => ({
    x: randomNormal(),
    y: randomNormal(),
    size: randomLogNormal() * 10
}));

const xScale = d3.scaleLinear().domain([-5, 5]);

const yScale = d3.scaleLinear().domain([-5, 5]);

Zuerst erstellen wir einen Datensatz, der die Koordinaten von Punkten enthält, die zufällig um den Ursprung x / y verteilt sind, und passen die Skalierung an. Wir geben mit der Methode keinen Bereich von x- und y-Werten an range, da die WebGL-Reihen canvas (-1 -> +1in den normalisierten Koordinaten des Geräts in voller Größe des Elements angezeigt werden.

const series = fc
    .seriesWebglPoint()
    .xScale(xScale)
    .yScale(yScale)
    .crossValue(d => d.x)
    .mainValue(d => d.y)
    .size(d => d.size)
    .equals((previousData, data) => previousData.length > 0);

Als nächstes richten wir eine Reihe von Punkten mit xScaleund ein yScale. Anschließend konfigurieren wir die geeigneten Zugriffsmöglichkeiten, mit denen Sie Daten lesen können.

Darüber hinaus definieren wir eine eigene Gleichstellungsprüfungsfunktion, mit der sichergestellt werden soll, dass die Komponente nicht databei jedem Rendering GPUs überträgt . Wir müssen dies explizit ausdrücken, denn selbst wenn wir wissen, dass wir diese Daten nicht ändern werden, kann die Komponente nichts davon wissen, ohne eine ressourcenintensive „schmutzige“ Prüfung durchzuführen.

const colorScale = d3.scaleOrdinal(d3.schemeAccent);

const webglColor = color => {
    const { r, g, b, opacity } = d3.color(color).rgb();
    return [r / 255, g / 255, b / 255, opacity];
};

const fillColor = fc
    .webglFillColor()
    .value((d, i) => webglColor(colorScale(i)))
    .data(data);

series.decorate(program => {
    fillColor(program);
});

Mit diesem Code können Sie das Diagramm einfärben. Wir verwenden den Index des Punkts im Datensatz, um die entsprechende Farbe auszuwählen colorScale, konvertieren ihn dann in das gewünschte Format und verwenden ihn zum Dekorieren des Ausgabepunkts.

function render() {
    const ease = 5 * (0.51 + 0.49 * Math.sin(Date.now() / 1e3));
    xScale.domain([-ease, ease]);
    yScale.domain([-ease, ease]);
    series(data);
    requestAnimationFrame(render);
}

Nachdem die Punkte gefärbt sind, müssen Sie nur noch das Diagramm animieren. Aus diesem Grund benötigen wir einen konstanten Methodenaufruf render(). Dies führt außerdem zu einer gewissen Belastung des Systems. Wir simulieren die Vergrößerung und Verkleinerung des Diagrammmaßstabs, indem wir requestAnimationFramedie Eigenschaften xScale.domainund yScale.domainjeden Frame ändern . Hier werden zeitabhängige Werte angewendet und berechnet, damit sich der Maßstab des Diagramms reibungslos ändert. Außerdem ändern wir den Nachrichtenkopf so, dass eine Methode aufgerufen wird, um den Renderzyklus zu starten render(), und dass wir nicht direkt aufrufen müssen series(data).

importScripts(
    './node_modules/d3-array/dist/d3-array.js',
    './node_modules/d3-collection/dist/d3-collection.js',
    './node_modules/d3-color/dist/d3-color.js',
    './node_modules/d3-interpolate/dist/d3-interpolate.js',
    './node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
    './node_modules/d3-random/dist/d3-random.js',
    './node_modules/d3-scale/dist/d3-scale.js',
    './node_modules/d3-shape/dist/d3-shape.js',
    './node_modules/d3fc-extent/build/d3fc-extent.js',
    './node_modules/d3fc-random-data/build/d3fc-random-data.js',
    './node_modules/d3fc-rebind/build/d3fc-rebind.js',
    './node_modules/d3fc-series/build/d3fc-series.js',
    './node_modules/d3fc-webgl/build/d3fc-webgl.js'
);

Damit dieses Beispiel funktioniert, müssen nur die erforderlichen Bibliotheken in den Worker importiert werden. Wir verwenden dies importScripts, und um die Tools zum Erstellen von Projekten nicht zu verwenden, verwenden wir eine Liste manuell erstellter Abhängigkeiten. Leider können wir nicht einfach die vollständigen d3 / d3fc-Builds herunterladen, da sie vom DOM abhängen und das, was sie benötigen, im Worker nicht verfügbar ist.

→ Den vollständigen Code für dieses Beispiel finden Sie auf GitHub

OffscreenCanvas und Leistung


Die Animation in unserem Projekt funktioniert dank des Einsatzes requestAnimationFrameeines Arbeiters. Auf diese Weise kann der Worker Seiten rendern, auch wenn der Hauptthread mit anderen Dingen beschäftigt ist. Wenn Sie sich die im vorherigen Abschnitt beschriebene Projektseite ansehen und auf die Schaltfläche klicken Stop main thread, können Sie darauf achten, dass die Aktualisierung der Zeitstempelinformationen gestoppt wird, wenn das Meldungsfeld angezeigt wird. Der Haupt-Thread ist blockiert, aber die Diagrammanimation wird nicht gestoppt.


Projektfenster

Wir könnten ein Rendering organisieren, das durch den Empfang von Nachrichten vom Hauptstrom initiiert wird. Solche Ereignisse können beispielsweise gesendet werden, wenn ein Benutzer mit einem Diagramm interagiert oder wenn frische Daten über das Netzwerk empfangen werden. Bei diesem Ansatz wird das Rendern jedoch gestoppt, wenn der Hauptthread mit etwas beschäftigt ist und keine Nachrichten im Worker empfangen werden.

Das kontinuierliche Rendern des Diagramms unter Bedingungen, unter denen nichts passiert, ist eine großartige Möglichkeit, den Benutzer mit einem summenden Lüfter und einer schwachen Batterie zu ärgern. Infolgedessen stellt sich heraus, dass in der realen Welt die Entscheidung über die Aufteilung der Verantwortlichkeiten zwischen dem Hauptthread und dem Arbeitsthread davon abhängt, ob der Arbeiter etwas Nützliches leisten kann, wenn er keine Nachrichten über eine Änderung der Situation vom Hauptthread erhält.

Wenn wir über die Übertragung von Nachrichten zwischen Threads sprechen, sollte beachtet werden, dass wir hier einen sehr einfachen Ansatz angewendet haben. Ehrlich gesagt müssen wir nur sehr wenige Nachrichten zwischen Threads übertragen, daher würde ich unseren Ansatz eher als Folge von Pragmatismus und nicht als Faulheit bezeichnen. Wenn wir jedoch über reale Anwendungen sprechen, kann festgestellt werden, dass das Nachrichtenübertragungsschema zwischen den Flüssen komplizierter wird, wenn die Interaktion des Benutzers mit dem Diagramm und die Aktualisierung der visualisierten Daten davon abhängen.

Sie können die Standard- MessageChannel- Schnittstelle verwenden, um separate Nachrichtenkanäle zwischen dem Hauptthread und dem Arbeitsthread zu erstellen .. Jeder dieser Kanäle kann für eine bestimmte Kategorie von Nachrichten verwendet werden, was die Verarbeitung von Nachrichten erleichtert. Als Alternative zu Standardmechanismen können Bibliotheken von Drittanbietern wie Comlink fungieren . Diese Bibliothek verbirgt Details auf niedriger Ebene hinter einer Schnittstelle auf hoher Ebene unter Verwendung von Proxy-Objekten .

Ein weiteres interessantes Merkmal unseres Beispiels, das im Nachhinein deutlich sichtbar wird, ist die Tatsache, dass es die Tatsache berücksichtigt, dass GPU-Ressourcen wie andere Systemressourcen alles andere als endlos sind. Durch die Übersetzung des Renderns in einen Worker kann der Haupt-Thread andere Probleme lösen. Der Browser muss jedoch die GPU verwenden, um das DOM und die mit den Mitteln generierten Daten zu rendern OffscreenCanvas.

Wenn der Worker alle GPU-Ressourcen verbraucht, treten im Hauptanwendungsfenster Leistungsprobleme auf, unabhängig davon, wo das Rendern ausgeführt wird. Achten Sie darauf, wie die Geschwindigkeit der Aktualisierung des Zeitstempels im Beispiel mit zunehmender Anzahl der angezeigten Punkte abnimmt. Wenn Sie über eine leistungsstarke Grafikkarte verfügen, müssen Sie möglicherweise die Anzahl der auf die Seite übertragenen Punkte in der Abfragezeichenfolge erhöhen. Dazu können Sie einen Wert verwenden, der den Maximalwert von 100000 überschreitet, der über einen der Links auf der Beispielseite angegeben wird.

Es sollte beachtet werden, dass wir hier die geheime Welt der Kontextattribute nicht erforscht haben. Eine solche Studie könnte uns helfen, die Produktivität der Lösung zu steigern. Ich habe dies jedoch nicht getan, da diese Attribute nur wenig unterstützt werden.

Wenn wir über das Rendern mit sprechen OffscreenCanvas, sieht das Attribut hier am interessantesten aus desynchronized. Wenn es unterstützt wird und einige Einschränkungen berücksichtigt werden, können Sie die Synchronisation zwischen der Ereignisschleife und der im Worker ausgeführten Rendering-Schleife aufheben. Dies minimiert die Verzögerung beim Aktualisieren des Bildes. Details dazu finden Sie hier .

Zusammenfassung


Die Benutzeroberfläche OffscreenCanvasbietet Entwicklern die Möglichkeit, die Leistung beim Rendern von Diagrammen zu verbessern. Die Verwendung erfordert jedoch einen durchdachten Ansatz. Bei der Verwendung müssen Sie Folgendes berücksichtigen:

  • (, , ) - .

    • , . , , , postMessage.
  • , (, SVG/HTML- <canvas>), , , .

    • , , , , , , , , .
  • , GPU, OffscreenCanvas .

    • OffscreenCanvas, GPU-, , . , , .

Hier ist ein Beispielcode und hier ist seine Arbeitsversion.

Liebe Leser! Haben Sie OffscreenCanvas verwendet, um die Anzeige von Grafiken auf Webseiten zu beschleunigen?


All Articles