This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 6b31eaa7f38 UI: Load the ELK graph layout engine only when a graph is
drawn (#75986)
6b31eaa7f38 is described below
commit 6b31eaa7f38d9348965c3f336733ddf903e5ae4e
Author: safaehar <[email protected]>
AuthorDate: Tue Nov 10 12:35:54 2026 -0500
UI: Load the ELK graph layout engine only when a graph is drawn (#75986)
* UI: Load the ELK graph layout engine only when a graph is drawn
useGraphLayout imported ELK from the elkjs package entry, whose
lib/main.js requires elk-worker.min.js as a fallback worker, and also
imported the same worker as a ?raw string for the same-origin blob
Worker. The layout engine was bundled twice and loaded with every page
that imports the graph components, not only the graph view.
Import only elkjs/lib/elk-api.js, since the hook supplies its own
worker, and load the worker source with a dynamic import the first time
a graph is laid out. A failed load is not cached, so the next layout
retries.
* Chain the ELK load retry onto the import promise
Attaching the handler on every getElk() call added a new listener per
layout instead of handling the failed load once.
---
.../ui/src/components/Graph/useGraphLayout.ts | 45 ++++++++++++++--------
1 file changed, 29 insertions(+), 16 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts
b/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts
index b04622e8635..a68513dd410 100644
--- a/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts
+++ b/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts
@@ -18,14 +18,7 @@
*/
import { useQuery } from "@tanstack/react-query";
import type { ElkNode } from "elkjs";
-import ELK from "elkjs/lib/elk-api.js";
-// ?raw imports the file content as a plain string without any transformation.
-// We create a blob: URL from it so the Worker is always same-origin to the
-// page — avoiding the cross-origin SecurityError that occurs in Airflow's dev
-// setup where Vite (5173) and Flask (28080) run on different ports, and all
-// URL-based worker approaches (?worker, ?worker&inline, new URL()) resolve to
-// the Vite origin which browsers reject for Workers.
-import ElkWorkerSource from "elkjs/lib/elk-worker.min.js?raw";
+import ElkConstructor, { type ELK } from "elkjs/lib/elk-api.js";
import type { NodeResponse, StructureDataResponse } from
"openapi/requests/types.gen";
@@ -33,15 +26,34 @@ import type { Direction } from "./DirectionDropdown";
import { generateElkGraph } from "./elkGraphUtils";
import { flattenGraph, formatFlowEdges } from "./reactflowUtils";
-// Blob URL created once at module load. `type: "classic"` preserves the
-// original CJS environment detection in elk-worker: as a classic script
-// `typeof module === "undefined"`, so the worker sets self.onmessage rather
-// than exporting a FakeWorker.
-const elkWorkerBlobUrl = URL.createObjectURL(new Blob([ElkWorkerSource], {
type: "application/javascript" }));
+// The layout engine (~1.4 MB) loads the first time a graph is laid out, not
with every page that can show one.
+let elkPromise: Promise<ELK> | undefined;
-const elk = new ELK({
- workerFactory: () => new Worker(elkWorkerBlobUrl, { type: "classic" }),
-});
+const getElk = () => {
+ elkPromise ??= import("elkjs/lib/elk-worker.min.js?raw")
+ .then(({ default: elkWorkerSource }) => {
+ // ?raw imports the file content as a plain string without any
transformation.
+ // We create a blob: URL from it so the Worker is always same-origin to
the
+ // page — avoiding the cross-origin SecurityError that occurs in
Airflow's dev
+ // setup where Vite (5173) and Flask (28080) run on different ports, and
all
+ // URL-based worker approaches (?worker, ?worker&inline, new URL())
resolve to
+ // the Vite origin which browsers reject for Workers.
+ // `type: "classic"` preserves the original CJS environment detection in
elk-worker: as a classic script
+ // `typeof module === "undefined"`, so the worker sets self.onmessage
rather than exporting a FakeWorker.
+ const elkWorkerBlobUrl = URL.createObjectURL(
+ new Blob([elkWorkerSource], { type: "application/javascript" }),
+ );
+
+ return new ElkConstructor({ workerFactory: () => new
Worker(elkWorkerBlobUrl, { type: "classic" }) });
+ })
+ .catch((error: unknown) => {
+ // Retry on the next layout if the chunk failed to load, rather than
keeping the rejection.
+ elkPromise = undefined;
+ throw error;
+ });
+
+ return elkPromise;
+};
export type LayoutNode = ElkNode & NodeResponse;
@@ -72,6 +84,7 @@ export const useGraphLayout = ({
});
// 2. use elk to generate the size and position of nodes and edges
+ const elk = await getElk();
const data = (await elk.layout(graph)) as LayoutNode;
// 3. Flatten the nodes and edges for xyflow to actually render the graph