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

Reply via email to