From 53c0ef26494074a07ad509d807691ea33ee5351d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Wed, 7 Oct 2026 13:31:03 +0200 Subject: [PATCH 1/3] Reduce GraphWidget payload by sourcing the NDL stylesheet from the model The NDL stylesheet was bundled into the ESM (twice) and shipped again as _css, so every widget display carried ~6 MB of the same CSS. Read it from the model's _css trait instead (anywidget syncs it; Streamlit seeds it) and drop the dead NVL layout worker chunks from the build output. widget.js 7.3 MB -> 3.2 MB, index.html 8.5 MB -> 4.4 MB, streamlit graph.js 7.3 MB -> 5.2 MB. --- js-applet/src/graph-widget.test.tsx | 4 + js-applet/src/graph-widget.tsx | 77 +- js-applet/src/streamlit-entrypoint.ts | 6 +- js-applet/vite-plugin-clean-worker-chunks.ts | 33 + js-applet/vite.config.html.ts | 4 +- js-applet/vite.config.streamlit.ts | 4 +- js-applet/vite.config.ts | 4 +- .../resources/nvl_entrypoint/index.html | 210 +- .../resources/nvl_entrypoint/widget.js | 14818 +++++++-------- .../neo4j_viz/resources/streamlit_v2/graph.js | 15379 ++++++++-------- 10 files changed, 15303 insertions(+), 15236 deletions(-) create mode 100644 js-applet/vite-plugin-clean-worker-chunks.ts diff --git a/js-applet/src/graph-widget.test.tsx b/js-applet/src/graph-widget.test.tsx index d464a64a..be51eb2e 100644 --- a/js-applet/src/graph-widget.test.tsx +++ b/js-applet/src/graph-widget.test.tsx @@ -60,6 +60,7 @@ type WidgetState = { } | null; visible?: boolean; }; + _css?: string; }; // The static HTML render path uses the real `createLocalModel` shim, so tests @@ -137,6 +138,9 @@ async function renderWidgetInShadowRoot( theme: "light", selected: { nodeIds: [], relationshipIds: [] }, legend: { nodes: null, relationships: null, visible: true }, + // The stylesheet is host-provided (`_css`); a stub is enough to assert the + // shadow-root/document.head bridging. + _css: ".nvl-test-ndl { color: red; }", }); let teardown: RenderedWidget["teardown"] = undefined; diff --git a/js-applet/src/graph-widget.tsx b/js-applet/src/graph-widget.tsx index ab5e812a..859df66d 100644 --- a/js-applet/src/graph-widget.tsx +++ b/js-applet/src/graph-widget.tsx @@ -1,5 +1,4 @@ import { createRender, useModel, useModelState } from "@anywidget/react"; -import ndlCssText from "@neo4j-ndl/base/lib/neo4j-ds-styles.css?inline"; import { Gesture, GraphSelection, GraphVisualization } from "@neo4j-ndl/react-graph"; import type NVL from "@neo4j-nvl/base"; import type { Layout, NvlOptions } from "@neo4j-nvl/base"; @@ -78,6 +77,11 @@ export type WidgetData = { selected: GraphSelection; legend: LegendData; last_event: InteractionEvent | null; + /** + * The NDL stylesheet, provided by the host (anywidget syncs the widget's `_css` + * trait; Streamlit passes it in the component state). Never bundled into the JS. + */ + _css?: string; }; const EMPTY_SELECTION: GraphSelection = { nodeIds: [], relationshipIds: [] }; @@ -154,49 +158,50 @@ function useResolvedTheme(theme: Theme | undefined): "light" | "dark" { return normalizedTheme === "auto" ? detectedTheme : normalizedTheme; } -// @font-face rules in shadow DOM adopted stylesheets don't register fonts at the -// document level, so the browser can't find them for rendering. We extract and hoist -// them into document.head eagerly at module load so fonts begin loading immediately. -const fontFaceRules = (ndlCssText.match(/@font-face\s*\{[^}]*\}/g) || []).join("\n"); -if (fontFaceRules) { - const fontStyle = document.createElement("style"); - fontStyle.textContent = fontFaceRules; - document.head.appendChild(fontStyle); -} - -const documentStyleSelector = "[data-neo4j-viz-ndl-main]"; -const overlayStyleSelector = "[data-neo4j-viz-ndl-overlays]"; -const shadowRootStyleSelector = "[data-neo4j-viz-ndl-shadow-root]"; - -function appendStyle(root: Node & ParentNode, attributeName: string, cssText: string) { +const documentStyleAttr = "data-neo4j-viz-ndl-main"; +const overlayStyleAttr = "data-neo4j-viz-ndl-overlays"; +const shadowRootStyleAttr = "data-neo4j-viz-ndl-shadow-root"; +const fontStyleAttr = "data-neo4j-viz-ndl-fonts"; + +// The NDL stylesheet is deliberately NOT imported into the JS bundle: it is provided +// by the host via the model's `_css` trait (anywidget syncs it, Streamlit passes it in +// the component state). Bundling it here duplicated ~2 MB of CSS into every widget +// payload. `ensureStyle` keeps the injected copies in sync so `_css` updates (HMR) are +// reflected without adding duplicate