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 OffscreenCanvas
gesamte 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.SpezifikationOffscreenCanvas
gibt 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 OffscreenCanvas
innerhalb 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 PunktenUm 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 OffscreenCanvas
das Element canvas
mit 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 OffscreenCanvas
ursprünglich von den Attributen width
und dem height
Element 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 measure
von d3fc-canvas
, das uns in Übereinstimmung mit die Möglichkeit gibt, requestAnimationFrame
dem 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 OffscreenCanvas
kö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 canvas
Kontext aus dem Element webgl
und übergeben ihn an die Komponente series
. Dann rufen wir die Komponente auf series
und ü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 width
und height
, die in der Nachricht enthalten waren, und verwenden sie, um die Abmessungen offscreenCanvas
und den Anzeigebereich von WebGL festzulegen. Der Link offscreenCanvas
wird nicht direkt verwendet. Tatsache ist, dass die Nachricht entweder eine Eigenschaft offscreenCanvas
oder Eigenschaften width
und 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 -> +1
in 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 xScale
und 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 data
bei 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 requestAnimationFrame
die Eigenschaften xScale.domain
und yScale.domain
jeden 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 GitHubOffscreenCanvas und Leistung
Die Animation in unserem Projekt funktioniert dank des Einsatzes requestAnimationFrame
eines 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.ProjektfensterWir 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 OffscreenCanvas
bietet 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:- (, , ) - .
- , (, 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?