From 25d1c09b2f55f9f10eff5918501d385554f237e6 Mon Sep 17 00:00:00 2001 From: Matthew Runyon Date: Mon, 26 Feb 2024 12:33:18 -0600 Subject: [PATCH] feat: Lazy loading and code splitting (#1802) This adds lazy loading for `chart` and `iris-grid` and enables it for `MarkdownComponent` as well. Replaced the exports for `Chart` and `IrisGrid` with lazy loading wrappers. This should mean anywhere else we use the components in the app should be automatically lazy loaded. --- packages/chart/src/LazyChart.tsx | 15 ++++ packages/chart/src/index.ts | 4 +- packages/chart/src/plotly/LazyPlot.tsx | 15 ++++ packages/code-studio/vite.config.ts | 3 + .../theme-spectrum-alias.module.css | 7 +- .../theme-spectrum-palette.module.css | 6 +- .../console/src/notebook/ScriptEditor.tsx | 2 +- .../src/panels/IrisGridPanel.tsx | 3 +- packages/iris-grid/src/IrisGrid.test.tsx | 2 +- packages/iris-grid/src/IrisGrid.tsx | 3 +- .../src/IrisGridCellOverflowModal.tsx | 2 +- packages/iris-grid/src/LazyIrisGrid.tsx | 26 +++++++ packages/iris-grid/src/index.ts | 3 +- .../src/key-handlers/ClearFilterKeyHandler.ts | 2 +- .../src/key-handlers/CopyCellKeyHandler.ts | 2 +- .../src/key-handlers/CopyKeyHandler.ts | 2 +- .../src/key-handlers/ReverseKeyHandler.ts | 2 +- .../IrisGridColumnSelectMouseHandler.ts | 2 +- .../IrisGridColumnTooltipMouseHandler.ts | 2 +- .../IrisGridDataSelectMouseHandler.ts | 2 +- .../IrisGridFilterMouseHandler.ts | 2 +- .../mousehandlers/IrisGridSortMouseHandler.ts | 2 +- tests/lazy-loading.spec.ts | 70 +++++++++++++++++++ tests/utils.ts | 4 ++ 24 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 packages/chart/src/LazyChart.tsx create mode 100644 packages/chart/src/plotly/LazyPlot.tsx create mode 100644 packages/iris-grid/src/LazyIrisGrid.tsx create mode 100644 tests/lazy-loading.spec.ts diff --git a/packages/chart/src/LazyChart.tsx b/packages/chart/src/LazyChart.tsx new file mode 100644 index 0000000000..036adfa6e6 --- /dev/null +++ b/packages/chart/src/LazyChart.tsx @@ -0,0 +1,15 @@ +import { LoadingOverlay } from '@deephaven/components'; +import { lazy, Suspense } from 'react'; + +const Chart = lazy(() => import('./Chart.js')); + +function LazyChart(props: React.ComponentProps): JSX.Element { + return ( + }> + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + ); +} + +export default LazyChart; diff --git a/packages/chart/src/index.ts b/packages/chart/src/index.ts index 3693c39bc9..dad43ef43d 100644 --- a/packages/chart/src/index.ts +++ b/packages/chart/src/index.ts @@ -1,4 +1,4 @@ -export { default as Chart } from './Chart'; +export { default as Chart } from './LazyChart'; export { default as ChartModelFactory } from './ChartModelFactory'; export { default as ChartModel } from './ChartModel'; export { default as ChartUtils } from './ChartUtils'; @@ -6,7 +6,7 @@ export * from './ChartUtils'; export * from './DownsamplingError'; export { default as FigureChartModel } from './FigureChartModel'; export { default as MockChartModel } from './MockChartModel'; -export { default as Plot } from './plotly/Plot'; +export { default as Plot } from './plotly/LazyPlot'; export * from './ChartTheme'; export * from './ChartThemeProvider'; export { default as isFigureChartModel } from './isFigureChartModel'; diff --git a/packages/chart/src/plotly/LazyPlot.tsx b/packages/chart/src/plotly/LazyPlot.tsx new file mode 100644 index 0000000000..b1716a50a6 --- /dev/null +++ b/packages/chart/src/plotly/LazyPlot.tsx @@ -0,0 +1,15 @@ +import { LoadingOverlay } from '@deephaven/components'; +import { lazy, Suspense } from 'react'; + +const PlotBase = lazy(() => import('./Plot.js')); + +function Plot(props: React.ComponentProps): JSX.Element { + return ( + }> + {/* eslint-disable react/jsx-props-no-spreading */} + + + ); +} + +export default Plot; diff --git a/packages/code-studio/vite.config.ts b/packages/code-studio/vite.config.ts index 3789e0212a..c27f36fa51 100644 --- a/packages/code-studio/vite.config.ts +++ b/packages/code-studio/vite.config.ts @@ -122,6 +122,9 @@ export default defineConfig(({ mode }) => { if (id.includes('plotly.js')) { return 'plotly'; } + if (id.includes('mathjax')) { + return 'mathjax'; + } return 'vendor'; } }, diff --git a/packages/components/src/theme/theme-spectrum/theme-spectrum-alias.module.css b/packages/components/src/theme/theme-spectrum/theme-spectrum-alias.module.css index cb9b9c13a5..8bd0ee9985 100644 --- a/packages/components/src/theme/theme-spectrum/theme-spectrum-alias.module.css +++ b/packages/components/src/theme/theme-spectrum/theme-spectrum-alias.module.css @@ -1,6 +1,11 @@ /* stylelint-disable custom-property-empty-line-before */ /* stylelint-disable alpha-value-notation */ -.dh-spectrum-alias { + +/** + * Intentionally using the classname twice so we have higher specificity than spectrum's definitions + * This is to ensure that our overrides are applied regardless of CSS chunk loading order + */ +.dh-spectrum-alias.dh-spectrum-alias { /*********** Override variables in spectrum-global.css **********************/ --spectrum-alias-background-color-default: var(--dh-color-bg); --spectrum-alias-background-color-disabled: var(--dh-color-disabled-bg); diff --git a/packages/components/src/theme/theme-spectrum/theme-spectrum-palette.module.css b/packages/components/src/theme/theme-spectrum/theme-spectrum-palette.module.css index 542d5bb60f..c1f25f2e57 100644 --- a/packages/components/src/theme/theme-spectrum/theme-spectrum-palette.module.css +++ b/packages/components/src/theme/theme-spectrum/theme-spectrum-palette.module.css @@ -1,4 +1,8 @@ -.dh-spectrum-palette { +/** + * Intentionally using the classname twice so we have higher specificity than spectrum's definitions + * This is to ensure that our overrides are applied regardless of CSS chunk loading order + */ +.dh-spectrum-palette.dh-spectrum-palette { /* Gray */ --spectrum-gray-50: var(--dh-color-gray-50); --spectrum-gray-75: var(--dh-color-gray-75); diff --git a/packages/console/src/notebook/ScriptEditor.tsx b/packages/console/src/notebook/ScriptEditor.tsx index 5ab9ef1285..22de241651 100644 --- a/packages/console/src/notebook/ScriptEditor.tsx +++ b/packages/console/src/notebook/ScriptEditor.tsx @@ -6,7 +6,7 @@ import { LoadingOverlay, ShortcutRegistry } from '@deephaven/components'; import Log from '@deephaven/log'; import type { IdeSession } from '@deephaven/jsapi-types'; import { assertNotNull } from '@deephaven/utils'; -import { editor, IDisposable } from 'monaco-editor'; +import type { editor, IDisposable } from 'monaco-editor'; import Editor from './Editor'; import { MonacoProviders, MonacoUtils } from '../monaco'; import './ScriptEditor.scss'; diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index 63ec4fa97b..c000ca3c1b 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -19,6 +19,7 @@ import { import { AdvancedSettings, IrisGrid, + type IrisGridType, IrisGridModel, IrisGridUtils, isIrisGridTableModelTemplate, @@ -362,7 +363,7 @@ export class IrisGridPanel extends PureComponent< } } - irisGrid: RefObject; + irisGrid: RefObject; pluginRef: RefObject; diff --git a/packages/iris-grid/src/IrisGrid.test.tsx b/packages/iris-grid/src/IrisGrid.test.tsx index 059e8fb9ba..24d58e7219 100644 --- a/packages/iris-grid/src/IrisGrid.test.tsx +++ b/packages/iris-grid/src/IrisGrid.test.tsx @@ -4,7 +4,7 @@ import dh from '@deephaven/jsapi-shim'; import { DateUtils, Settings } from '@deephaven/jsapi-utils'; import { TestUtils } from '@deephaven/utils'; import { TypeValue } from '@deephaven/filters'; -import { IrisGrid } from './IrisGrid'; +import IrisGrid from './IrisGrid'; import IrisGridTestUtils from './IrisGridTestUtils'; class MockPath2D { diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index fe4d5dd93c..d555b7af83 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -249,6 +249,7 @@ function isEmptyConfig({ sorts.length === 0 ); } + export type FilterData = { operator: FilterTypeValue; text: string; @@ -451,7 +452,7 @@ export interface IrisGridState { columnHeaderGroups: readonly ColumnHeaderGroup[]; } -export class IrisGrid extends Component { +class IrisGrid extends Component { static contextType = IrisGridThemeContext; static minDebounce = 150; diff --git a/packages/iris-grid/src/IrisGridCellOverflowModal.tsx b/packages/iris-grid/src/IrisGridCellOverflowModal.tsx index 509aeb912d..d2fade7e83 100644 --- a/packages/iris-grid/src/IrisGridCellOverflowModal.tsx +++ b/packages/iris-grid/src/IrisGridCellOverflowModal.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { Editor } from '@deephaven/console'; -import * as monaco from 'monaco-editor'; +import type * as monaco from 'monaco-editor'; import { Button, CopyButton, diff --git a/packages/iris-grid/src/LazyIrisGrid.tsx b/packages/iris-grid/src/LazyIrisGrid.tsx new file mode 100644 index 0000000000..9331ae3554 --- /dev/null +++ b/packages/iris-grid/src/LazyIrisGrid.tsx @@ -0,0 +1,26 @@ +import { forwardRef, lazy, Suspense } from 'react'; +import { LoadingOverlay } from '@deephaven/components'; +import type IrisGridType from './IrisGrid'; +import type { IrisGridProps } from './IrisGrid'; + +const IrisGrid = lazy(() => import('./IrisGrid.js')); + +const LazyIrisGrid = forwardRef< + IrisGridType, + JSX.LibraryManagedAttributes +>( + ( + // This creates the correct type to make defaultProps optional + props, + ref + ): JSX.Element => ( + }> + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + ) +); + +LazyIrisGrid.displayName = 'LazyIrisGrid'; + +export default LazyIrisGrid; diff --git a/packages/iris-grid/src/index.ts b/packages/iris-grid/src/index.ts index 046dc7d501..8cbdd42a9e 100644 --- a/packages/iris-grid/src/index.ts +++ b/packages/iris-grid/src/index.ts @@ -1,4 +1,4 @@ -import IrisGrid from './IrisGrid'; +import IrisGrid from './LazyIrisGrid'; export default IrisGrid; export { IrisGrid }; @@ -8,6 +8,7 @@ export * from './CommonTypes'; export { default as ColumnHeaderGroup } from './ColumnHeaderGroup'; export * from './PartitionedGridModel'; export * from './IrisGrid'; +export type { default as IrisGridType } from './IrisGrid'; export { default as SHORTCUTS } from './IrisGridShortcuts'; export { default as IrisGridModel } from './IrisGridModel'; export { default as IrisGridTableModel } from './IrisGridTableModel'; diff --git a/packages/iris-grid/src/key-handlers/ClearFilterKeyHandler.ts b/packages/iris-grid/src/key-handlers/ClearFilterKeyHandler.ts index 43b55ae1a0..9952febc5c 100644 --- a/packages/iris-grid/src/key-handlers/ClearFilterKeyHandler.ts +++ b/packages/iris-grid/src/key-handlers/ClearFilterKeyHandler.ts @@ -1,6 +1,6 @@ import { KeyboardEvent } from 'react'; import { KeyHandler } from '@deephaven/grid'; -import { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; import IrisGridShortcuts from '../IrisGridShortcuts'; class ClearFilterKeyHandler extends KeyHandler { diff --git a/packages/iris-grid/src/key-handlers/CopyCellKeyHandler.ts b/packages/iris-grid/src/key-handlers/CopyCellKeyHandler.ts index 8bcea06008..f5cf18589e 100644 --- a/packages/iris-grid/src/key-handlers/CopyCellKeyHandler.ts +++ b/packages/iris-grid/src/key-handlers/CopyCellKeyHandler.ts @@ -2,7 +2,7 @@ import { KeyboardEvent } from 'react'; import { KeyHandler } from '@deephaven/grid'; import { ContextActionUtils } from '@deephaven/components'; import type { Grid } from '@deephaven/grid'; -import { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; class CopyCellKeyHandler extends KeyHandler { private irisGrid: IrisGrid; diff --git a/packages/iris-grid/src/key-handlers/CopyKeyHandler.ts b/packages/iris-grid/src/key-handlers/CopyKeyHandler.ts index 3bb9c73aab..f8365fb37f 100644 --- a/packages/iris-grid/src/key-handlers/CopyKeyHandler.ts +++ b/packages/iris-grid/src/key-handlers/CopyKeyHandler.ts @@ -2,7 +2,7 @@ import { KeyboardEvent } from 'react'; import { ContextActionUtils } from '@deephaven/components'; import { KeyHandler } from '@deephaven/grid'; -import { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; import IrisGridUtils from '../IrisGridUtils'; class CopyKeyHandler extends KeyHandler { diff --git a/packages/iris-grid/src/key-handlers/ReverseKeyHandler.ts b/packages/iris-grid/src/key-handlers/ReverseKeyHandler.ts index 0e7112120d..bb3cc1047e 100644 --- a/packages/iris-grid/src/key-handlers/ReverseKeyHandler.ts +++ b/packages/iris-grid/src/key-handlers/ReverseKeyHandler.ts @@ -1,7 +1,7 @@ import { KeyboardEvent } from 'react'; import { KeyHandler } from '@deephaven/grid'; import { TableUtils } from '@deephaven/jsapi-utils'; -import { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; import IrisGridShortcuts from '../IrisGridShortcuts'; class ReverseKeyHandler extends KeyHandler { diff --git a/packages/iris-grid/src/mousehandlers/IrisGridColumnSelectMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridColumnSelectMouseHandler.ts index fcb5813fbe..d93d8f7de2 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridColumnSelectMouseHandler.ts +++ b/packages/iris-grid/src/mousehandlers/IrisGridColumnSelectMouseHandler.ts @@ -5,7 +5,7 @@ import { EventHandlerResult, } from '@deephaven/grid'; import type { Column } from '@deephaven/jsapi-types'; -import { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; import { DisplayColumn } from '../IrisGridModel'; /** diff --git a/packages/iris-grid/src/mousehandlers/IrisGridColumnTooltipMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridColumnTooltipMouseHandler.ts index 0b5eb4373a..4b753a2a55 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridColumnTooltipMouseHandler.ts +++ b/packages/iris-grid/src/mousehandlers/IrisGridColumnTooltipMouseHandler.ts @@ -6,7 +6,7 @@ import { Grid, GridMouseEvent, } from '@deephaven/grid'; -import type { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; /** * Detects mouse hover over column headers and displays the appropriate tooltip diff --git a/packages/iris-grid/src/mousehandlers/IrisGridDataSelectMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridDataSelectMouseHandler.ts index bf64068d44..e23c139899 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridDataSelectMouseHandler.ts +++ b/packages/iris-grid/src/mousehandlers/IrisGridDataSelectMouseHandler.ts @@ -5,7 +5,7 @@ import { GridPoint, EventHandlerResult, } from '@deephaven/grid'; -import type { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; /** * Handles sending data selected via double click diff --git a/packages/iris-grid/src/mousehandlers/IrisGridFilterMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridFilterMouseHandler.ts index 4d2af4112d..2e4402c320 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridFilterMouseHandler.ts +++ b/packages/iris-grid/src/mousehandlers/IrisGridFilterMouseHandler.ts @@ -4,7 +4,7 @@ import { GridPoint, EventHandlerResult, } from '@deephaven/grid'; -import type { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; /** * Trigger quick filters and advanced filters diff --git a/packages/iris-grid/src/mousehandlers/IrisGridSortMouseHandler.ts b/packages/iris-grid/src/mousehandlers/IrisGridSortMouseHandler.ts index 5eb31b34a6..c7bd60825e 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridSortMouseHandler.ts +++ b/packages/iris-grid/src/mousehandlers/IrisGridSortMouseHandler.ts @@ -8,7 +8,7 @@ import { GridRangeIndex, EventHandlerResult, } from '@deephaven/grid'; -import type { IrisGrid } from '../IrisGrid'; +import type IrisGrid from '../IrisGrid'; /** * Used to handle sorting on column header clicks diff --git a/tests/lazy-loading.spec.ts b/tests/lazy-loading.spec.ts new file mode 100644 index 0000000000..57ecc6d3c7 --- /dev/null +++ b/tests/lazy-loading.spec.ts @@ -0,0 +1,70 @@ +import { test, expect, Request } from '@playwright/test'; +import { openPlot } from './utils'; + +/** + * Checks the size of the response body of a request + * If the response body size is 0, the request is not checked + * This seems to happen for Safari sometimes + * + * @param request The request object. Playwright provides size on the request, not the response + * @param size The minimum size in bytes + */ +async function expectMinimumResponseSize( + request: Request | undefined, + size: number +) { + if (!request) { + throw new Error('Request is undefined'); + } + + const responseSize = (await request.sizes()).responseBodySize; + + // Safari doesn't seem to provide response size for some requests + if (responseSize > 0) { + expect(responseSize).toBeGreaterThan(size); + } +} + +test('lazy loads plotly', async ({ page }) => { + const requests: Request[] = []; + page.on('request', req => requests.push(req)); + + await page.goto(''); + await page.waitForLoadState('networkidle'); + + expect(requests.some(req => req.url().includes('assets/plotly'))).toBe(false); + + await openPlot(page, 'simple_plot'); + + const plotlyRequest = requests.find(req => + req.url().includes('assets/plotly') + ); + expect(plotlyRequest).toBeDefined(); + await expectMinimumResponseSize(plotlyRequest, 300 * 1000); // 300kB +}); + +test('lazy loads mathjax', async ({ page }) => { + const requests: Request[] = []; + page.on('request', req => requests.push(req)); + + await page.goto(''); + await page.waitForLoadState('networkidle'); + + expect(requests.some(req => req.url().includes('assets/mathjax'))).toBe( + false + ); + + const controlsButton = page.getByText('Controls'); + await controlsButton.click(); + const markdownButton = page.getByText('Markdown Widget'); + await markdownButton.click(); + + await expect(page.locator('.markdown-panel')).toBeVisible(); + await expect(page.locator('.markdown-panel .loading-spinner')).toHaveCount(0); + + const mathjaxRequest = requests.find(req => + req.url().includes('assets/mathjax') + ); + expect(mathjaxRequest).toBeDefined(); + await expectMinimumResponseSize(mathjaxRequest, 500 * 1000); // 500kB +}); diff --git a/tests/utils.ts b/tests/utils.ts index 797621387d..f9b62a87ce 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -109,9 +109,13 @@ export async function openPlot( if (waitForLoadFinished) { // Wait until it's done loading + await expect(page.locator('.chart-panel-container')).toHaveCount(1); await expect( page.locator('.chart-panel-container .loading-spinner') ).toHaveCount(0); + await expect( + page.locator('.chart-panel-container .chart-wrapper') + ).toHaveCount(1); } }