diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx
index 1e8f756c8..79565e8b6 100644
--- a/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx
+++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx
@@ -25,6 +25,62 @@ vi.mock('recharts', () => ({
Tooltip: () => null,
}));
+// Mock reactflow (lineage/dimensions/graph tabs). Tabs are lazy-loaded, so
+// reactflow is dynamically imported mid-render; loading the real lib inside
+// jsdom during render blocks the event loop. The dedicated tab tests cover
+// reactflow rendering — here we only need NodePage to mount the right tab.
+vi.mock('reactflow/dist/style.css', () => ({}));
+vi.mock('reactflow', async () => {
+ const React = await vi.importActual('react');
+ const { useState } = React;
+ const Passthrough = ({ children }) =>
{children}
;
+ return {
+ __esModule: true,
+ default: ({ children, nodes, nodeTypes }) => (
+
+ {(nodes || []).map(n => {
+ const NodeComp = nodeTypes?.[n.type];
+ return NodeComp ? (
+
+ ) : (
+
{n.id}
+ );
+ })}
+ {children}
+
+ ),
+ ReactFlowProvider: Passthrough,
+ Panel: Passthrough,
+ Background: () => null,
+ Controls: () => null,
+ MiniMap: () => null,
+ Handle: () => null,
+ Position: { Left: 'left', Right: 'right', Top: 'top', Bottom: 'bottom' },
+ MarkerType: { ArrowClosed: 'arrowclosed', Arrow: 'arrow' },
+ useNodesState: initial => {
+ const [n, s] = useState(initial || []);
+ return [n, s, () => {}];
+ },
+ useEdgesState: initial => {
+ const [e, s] = useState(initial || []);
+ return [e, s, () => {}];
+ },
+ addEdge: (params, eds) => [...(eds || []), params],
+ };
+});
+
+// Mock CodeMirror (materialization config editor). Like reactflow, it's pulled
+// in via a lazy tab and dynamically importing it (plus every language grammar
+// from codemirror-extensions-langs) mid-render blocks jsdom. The materialization
+// tab's own tests cover the editor.
+vi.mock('@uiw/react-codemirror', () => ({
+ __esModule: true,
+ default: ({ value }) => {value}
,
+}));
+vi.mock('@uiw/codemirror-extensions-langs', () => ({
+ langs: new Proxy({}, { get: () => () => [] }),
+}));
+
// ResizeObserver is not defined in jsdom
global.ResizeObserver = function (callback) {
return { observe: () => {}, disconnect: () => {}, unobserve: () => {} };
diff --git a/datajunction-ui/src/app/pages/NodePage/index.jsx b/datajunction-ui/src/app/pages/NodePage/index.jsx
index d6ce87160..c38a19b00 100644
--- a/datajunction-ui/src/app/pages/NodePage/index.jsx
+++ b/datajunction-ui/src/app/pages/NodePage/index.jsx
@@ -1,28 +1,32 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
-import { useContext, useEffect, useState } from 'react';
+import { useContext, useEffect, useState, lazy, Suspense } from 'react';
import Tab from '../../components/Tab';
import NamespaceHeader from '../../components/NamespaceHeader';
-import NodeInfoTab from './NodeInfoTab';
-import NodeColumnTab from './NodeColumnTab';
-import NodeDataFlowTab from './NodeDataFlowTab';
-import NodeDimensionsTab from './NodeDimensionsTab';
-import NodeHistory from './NodeHistory';
import NotebookDownload from './NotebookDownload';
import DJClientContext from '../../providers/djclient';
-import NodeMaterializationTab from './NodeMaterializationTab';
-import NodePreAggregationsTab from './NodePreAggregationsTab';
import ClientCodePopover from './ClientCodePopover';
import WatchButton from './WatchNodeButton';
-import NodesWithDimension from './NodesWithDimension';
-import NodeColumnLineage from './NodeLineageTab';
import EditIcon from '../../icons/EditIcon';
import ChartIcon from '../../icons/ChartIcon';
import AlertIcon from '../../icons/AlertIcon';
import LoadingIcon from '../../icons/LoadingIcon';
-import NodeDependenciesTab from './NodeDependenciesTab';
import { useNavigate } from 'react-router-dom';
+// Tabs are lazy-loaded so a node page only pulls the deps of the tab actually
+// shown. Otherwise every visit loads reactflow (lineage/dimensions/graph),
+// recharts (data flow) and codemirror (materialization) even for the Info tab.
+const NodeInfoTab = lazy(() => import('./NodeInfoTab'));
+const NodeColumnTab = lazy(() => import('./NodeColumnTab'));
+const NodeDataFlowTab = lazy(() => import('./NodeDataFlowTab'));
+const NodeDimensionsTab = lazy(() => import('./NodeDimensionsTab'));
+const NodeHistory = lazy(() => import('./NodeHistory'));
+const NodeMaterializationTab = lazy(() => import('./NodeMaterializationTab'));
+const NodePreAggregationsTab = lazy(() => import('./NodePreAggregationsTab'));
+const NodesWithDimension = lazy(() => import('./NodesWithDimension'));
+const NodeColumnLineage = lazy(() => import('./NodeLineageTab'));
+const NodeDependenciesTab = lazy(() => import('./NodeDependenciesTab'));
+
export function NodePage() {
const djClient = useContext(DJClientContext).DataJunctionAPI;
const navigate = useNavigate();
@@ -317,7 +321,7 @@ export function NodePage() {
{tabsList(node).map(buildTabs)}
- {tabToDisplay}
+ }>{tabToDisplay}
) : node?.message !== undefined ? (