From 8dad9dcdc915442f47db1d5687045e5fdf9b32b3 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Mon, 2 Mar 2026 13:26:11 +0100 Subject: [PATCH] docs(AnalyticalTable): add FAQ with dev mode performance guide --- .storybook/components/Redirect.tsx | 58 +++++++++++++ .storybook/components/index.ts | 1 + .storybook/manager.tsx | 4 + .../{ => docs}/AnalyticalTable.mdx | 0 .../{ => docs}/AnalyticalTable.stories.tsx | 38 ++++----- .../{ => docs}/AnalyticalTableHooks.mdx | 0 .../AnalyticalTableHooks.stories.tsx | 32 +++---- .../{Recipes.mdx => docs/FAQ.mdx} | 83 +++++++++++++++++-- .../{ => docs}/PluginAnnounceEmptyCells.mdx | 0 .../{ => docs}/PluginDisableRowSelection.mdx | 0 .../{ => docs}/PluginF2CellEdit.mdx | 0 .../PluginIndeterminateRowSelection.mdx | 2 +- .../{ => docs}/PluginManualRowSelect.mdx | 0 .../{ => docs}/PluginOnColumnResize.mdx | 0 .../{ => docs}/PluginOrderedMultiSort.mdx | 0 .../AnalyticalTable/docs/Recipes.mdx | 6 ++ .../components/AnalyticalTable/types/index.ts | 14 +++- 17 files changed, 192 insertions(+), 46 deletions(-) create mode 100644 .storybook/components/Redirect.tsx rename packages/main/src/components/AnalyticalTable/{ => docs}/AnalyticalTable.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/AnalyticalTable.stories.tsx (94%) rename packages/main/src/components/AnalyticalTable/{ => docs}/AnalyticalTableHooks.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/AnalyticalTableHooks.stories.tsx (92%) rename packages/main/src/components/AnalyticalTable/{Recipes.mdx => docs/FAQ.mdx} (76%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginAnnounceEmptyCells.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginDisableRowSelection.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginF2CellEdit.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginIndeterminateRowSelection.mdx (96%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginManualRowSelect.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginOnColumnResize.mdx (100%) rename packages/main/src/components/AnalyticalTable/{ => docs}/PluginOrderedMultiSort.mdx (100%) create mode 100644 packages/main/src/components/AnalyticalTable/docs/Recipes.mdx diff --git a/.storybook/components/Redirect.tsx b/.storybook/components/Redirect.tsx new file mode 100644 index 00000000000..da1b13bfe13 --- /dev/null +++ b/.storybook/components/Redirect.tsx @@ -0,0 +1,58 @@ +import '@ui5/webcomponents-fiori/dist/illustrations/PageNotFound.js'; +import { IllustratedMessage, Link, Text } from '@ui5/webcomponents-react'; +import { useEffect, useState } from 'react'; + +interface RedirectProps { + from: string; + to: string; + sectionName: string; + /** + * Delay in ms before redirecting + * @default 5000 + */ + delay?: number; +} + +export const Redirect = ({ from, to, sectionName, delay = 1000000 }: RedirectProps) => { + const [countdown, setCountdown] = useState(Math.ceil(delay / 1000)); + + const target = typeof window !== 'undefined' ? window.top || window.parent || window : null; + const currentHref = target?.location.href ?? ''; + const newHref = currentHref.replace(from, to); + + useEffect(() => { + if (typeof window === 'undefined') return; + if (!currentHref.includes(from)) return; + + const timer = setTimeout(() => { + target.location.replace(newHref); + }, delay); + + const countdownInterval = setInterval(() => { + setCountdown((prev) => Math.max(0, prev - 1)); + }, 1000); + + return () => { + clearTimeout(timer); + clearInterval(countdownInterval); + }; + }, [from, to, delay, currentHref, newHref, target]); + + return ( + + This page has been moved to {sectionName}. +
+ Redirecting in {countdown} seconds... + + } + > + + Go to {sectionName} + +
+ ); +}; diff --git a/.storybook/components/index.ts b/.storybook/components/index.ts index da4d3b46f2f..62a637e0c84 100644 --- a/.storybook/components/index.ts +++ b/.storybook/components/index.ts @@ -8,3 +8,4 @@ export * from './TableOfContent.js'; export * from './LabelWithWrapping.js'; export * from './CommandsAndQueries.js'; export * from './FilterBarExample.js'; +export * from './Redirect.js'; diff --git a/.storybook/manager.tsx b/.storybook/manager.tsx index b3e79874503..16ceeee0ce6 100644 --- a/.storybook/manager.tsx +++ b/.storybook/manager.tsx @@ -56,6 +56,10 @@ addons.setConfig({ showRoots: true, filters: { patterns: (item) => { + // todo: remove once "AnalyticalTable / Recipes" section is removed. + if (item.id?.includes('analyticaltable') && item.title.includes('Recipes')) { + return false; + } return !item.tags.includes('excludeFromSidebar'); }, }, diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.mdx b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/AnalyticalTable.mdx rename to packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.mdx diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx similarity index 94% rename from packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx rename to packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx index 40cefa4437f..94ad5488146 100644 --- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx +++ b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTable.stories.tsx @@ -17,25 +17,25 @@ import { FlexBoxDirection, FlexBoxJustifyContent, TextAlign, -} from '../../enums/index.js'; -import { Button } from '../../webComponents/Button/index.js'; -import { IllustratedMessage } from '../../webComponents/IllustratedMessage/index.js'; -import { Label } from '../../webComponents/Label/index.js'; -import { MultiComboBox } from '../../webComponents/MultiComboBox/index.js'; -import { MultiComboBoxItem } from '../../webComponents/MultiComboBoxItem/index.js'; -import { Option } from '../../webComponents/Option/index.js'; -import type { SegmentedButtonPropTypes } from '../../webComponents/SegmentedButton/index.js'; -import { SegmentedButton } from '../../webComponents/SegmentedButton/index.js'; -import { SegmentedButtonItem } from '../../webComponents/SegmentedButtonItem/index.js'; -import { Select } from '../../webComponents/Select/index.js'; -import { Tag } from '../../webComponents/Tag/index.js'; -import { Text } from '../../webComponents/Text/index.js'; -import type { ToggleButtonPropTypes } from '../../webComponents/ToggleButton/index.js'; -import { ToggleButton } from '../../webComponents/ToggleButton/index.js'; -import { FlexBox } from '../FlexBox/index.js'; -import { ObjectStatus } from '../ObjectStatus/index.js'; -import type { AnalyticalTableColumnDefinition, AnalyticalTablePropTypes } from './index.js'; -import { AnalyticalTable } from './index.js'; +} from '../../../enums/index.js'; +import { Button } from '../../../webComponents/Button/index.js'; +import { IllustratedMessage } from '../../../webComponents/IllustratedMessage/index.js'; +import { Label } from '../../../webComponents/Label/index.js'; +import { MultiComboBox } from '../../../webComponents/MultiComboBox/index.js'; +import { MultiComboBoxItem } from '../../../webComponents/MultiComboBoxItem/index.js'; +import { Option } from '../../../webComponents/Option/index.js'; +import type { SegmentedButtonPropTypes } from '../../../webComponents/SegmentedButton/index.js'; +import { SegmentedButton } from '../../../webComponents/SegmentedButton/index.js'; +import { SegmentedButtonItem } from '../../../webComponents/SegmentedButtonItem/index.js'; +import { Select } from '../../../webComponents/Select/index.js'; +import { Tag } from '../../../webComponents/Tag/index.js'; +import { Text } from '../../../webComponents/Text/index.js'; +import type { ToggleButtonPropTypes } from '../../../webComponents/ToggleButton/index.js'; +import { ToggleButton } from '../../../webComponents/ToggleButton/index.js'; +import { FlexBox } from '../../FlexBox/index.js'; +import { ObjectStatus } from '../../ObjectStatus/index.js'; +import type { AnalyticalTableColumnDefinition, AnalyticalTablePropTypes } from '../index.js'; +import { AnalyticalTable } from '../index.js'; const kitchenSinkArgs: AnalyticalTablePropTypes = { data: dataLarge, diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.mdx b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTableHooks.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.mdx rename to packages/main/src/components/AnalyticalTable/docs/AnalyticalTableHooks.mdx diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTableHooks.stories.tsx similarity index 92% rename from packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx rename to packages/main/src/components/AnalyticalTable/docs/AnalyticalTableHooks.stories.tsx index 7d33772f065..57f832c45e8 100644 --- a/packages/main/src/components/AnalyticalTable/AnalyticalTableHooks.stories.tsx +++ b/packages/main/src/components/AnalyticalTable/docs/AnalyticalTableHooks.stories.tsx @@ -6,22 +6,22 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import InputType from '@ui5/webcomponents/dist/types/InputType.js'; import paperPlaneIcon from '@ui5/webcomponents-icons/dist/paper-plane'; import { useCallback, useMemo, useReducer, useState } from 'react'; -import { AnalyticalTableSelectionMode, FlexBoxAlignItems, FlexBoxDirection } from '../../enums'; -import { Button } from '../../webComponents/Button/index.js'; -import { CheckBox } from '../../webComponents/CheckBox/index.js'; -import type { InputDomRef } from '../../webComponents/Input/index.js'; -import { Input } from '../../webComponents/Input/index.js'; -import { Label } from '../../webComponents/Label/index.js'; -import { Switch } from '../../webComponents/Switch/index.js'; -import { Tag } from '../../webComponents/Tag/index.js'; -import { Text } from '../../webComponents/Text/index.js'; -import { ToggleButton } from '../../webComponents/ToggleButton/index.js'; -import { FlexBox } from '../FlexBox'; -import meta from './AnalyticalTable.stories'; -import * as AnalyticalTableHooks from './pluginHooks/AnalyticalTableHooks'; -import { useF2CellEdit } from './pluginHooks/AnalyticalTableHooks'; -import { AnalyticalTable } from './index'; -import type { AnalyticalTableCellInstance, AnalyticalTableColumnDefinition } from './index'; +import { AnalyticalTableSelectionMode, FlexBoxAlignItems, FlexBoxDirection } from '../../../enums/index.js'; +import { Button } from '../../../webComponents/Button/index.js'; +import { CheckBox } from '../../../webComponents/CheckBox/index.js'; +import type { InputDomRef } from '../../../webComponents/Input/index.js'; +import { Input } from '../../../webComponents/Input/index.js'; +import { Label } from '../../../webComponents/Label/index.js'; +import { Switch } from '../../../webComponents/Switch/index.js'; +import { Tag } from '../../../webComponents/Tag/index.js'; +import { Text } from '../../../webComponents/Text/index.js'; +import { ToggleButton } from '../../../webComponents/ToggleButton/index.js'; +import { FlexBox } from '../../FlexBox/index.js'; +import { AnalyticalTable } from '../index.js'; +import type { AnalyticalTableCellInstance, AnalyticalTableColumnDefinition } from '../index.js'; +import * as AnalyticalTableHooks from '../pluginHooks/AnalyticalTableHooks.js'; +import { useF2CellEdit } from '../pluginHooks/AnalyticalTableHooks.js'; +import meta from './AnalyticalTable.stories.js'; const pluginsMeta = { ...meta, diff --git a/packages/main/src/components/AnalyticalTable/Recipes.mdx b/packages/main/src/components/AnalyticalTable/docs/FAQ.mdx similarity index 76% rename from packages/main/src/components/AnalyticalTable/Recipes.mdx rename to packages/main/src/components/AnalyticalTable/docs/FAQ.mdx index efc31069db1..0775e3c8e21 100644 --- a/packages/main/src/components/AnalyticalTable/Recipes.mdx +++ b/packages/main/src/components/AnalyticalTable/docs/FAQ.mdx @@ -1,15 +1,82 @@ import { TableOfContent } from '@sb/components'; import { Meta, Canvas } from '@storybook/addon-docs/blocks'; import { Footer } from '@sb/components'; -import { MessageStrip } from '../../webComponents/MessageStrip/index'; +import { MessageStrip } from '../../../webComponents/MessageStrip/index.js'; import * as ComponentStories from './AnalyticalTable.stories'; - + -# AnalyticalTable Recipes +# AnalyticalTable FAQ +## Why is the AnalyticalTable slow in development mode? + +When using the `AnalyticalTable` (or other virtualized components) in development mode, you may experience noticeable performance degradation compared to production builds. This is expected behavior caused by the combination of **React's dev mode overhead** and **browser debugger instrumentation**. + +### Why Dev Mode is Slower + +#### React Dev Mode Overhead + +React's development build includes significant overhead: + +- **Strict Mode double-rendering**: Components, `useEffect`, and callback refs render an additional time to help detect side effects +- **Validation checks**: Hooks order, prop types, deprecated API usage +- **Extended error messages**: Component stack traces for better debugging +- **Extra code paths**: Development-only warnings and diagnostics + +For a virtualized table that frequently mounts and unmounts cells during scrolling, this overhead compounds quickly. + +#### Browser Debugger Instrumentation + +Chromium-based browsers (Chrome, Edge, Brave, etc.) instrument async operations heavily in dev mode via the V8 engine - even when DevTools is closed. Firefox and Safari have significantly lighter debugger overhead in dev mode for example. + +### Recommendations + +#### High Impact + +1. **Use Firefox or Safari for development**: These browsers have significantly lighter debugger overhead in dev mode. + +2. **Test with production builds**: When working on performance-sensitive features, test with a production build to get accurate performance metrics. + +3. **Disable React Strict Mode** (temporarily): If dev mode performance is severely impacting your workflow, you can temporarily disable Strict Mode in development. Note that Strict Mode helps catch bugs, so re-enable it periodically. + +4. **Memoize props that require it**: Props marked with "Must be memoized" (e.g., `columns`) need a stable reference. Define them outside the component or use `useMemo`/`useCallback` to prevent unnecessary recalculations. + +#### Medium Impact + +5. **Disable React DevTools extension**: The React DevTools browser extension adds performance overhead and has been observed to cause small memory leaks. + +6. **Consider memoizing expensive Cell components**: If you have custom `Cell` renderers with expensive computations or deep component trees, wrapping them with `React.memo()` can help by creating render boundaries. However, avoid over-memoization - for simple cells, the overhead of memoization may outweigh the benefits. + +7. **Use [direct imports](?path=/docs/knowledge-base-faq--docs#why-use-direct-imports-via-package-export-maps)**: Since tree shaking is not available for most bundlers in dev mode, use direct imports to reduce initial load. + +#### Lower Impact + +8. **Keep DevTools closed**: When not actively debugging, keep DevTools closed to reduce overhead from source map parsing, DOM mutation observers, and memory tracking. + +9. **Deactivate UI5 Web Components animations in dev mode**: Animations cause components to recalculate multiple times. While we debounce frequent state changes, disabling animations can help. + +### Summary + +Dev mode slowness for virtualized components is expected behavior, not a bug. In production builds, the `AnalyticalTable` performs well. For the best development experience with heavy tables, consider using Firefox or Safari, or testing with production builds when performance is critical. + +
+Example: Performance Trace Comparison + +The following table shows a performance trace comparison from an investigation into dev mode performance ([GitHub issue #8234](https://github.com/SAP/ui5-webcomponents-react/issues/8234)). Your results may vary depending on your implementation, but the general pattern holds: + +| Metric | Chrome DEV | Chrome PROD | Firefox DEV | +| --------------- | ---------- | ----------- | ----------- | +| Trace file size | 561 MB | 3.3 MB | 6.3 MB | +| Event count | ~millions | ~14,600 | ~3,700 | + +Firefox in dev mode has similar overhead to Chrome in prod mode - both are lightweight. Chromium-based browsers in dev mode are the outlier with massive V8 debugger instrumentation. + +For more context, see the [detailed findings comment](https://github.com/SAP/ui5-webcomponents-react/issues/8234#issuecomment-3971742068) and [production environment performance videos](https://github.com/SAP/ui5-webcomponents-react/issues/8234#issuecomment-3960465202) showing table performance (inside a complex container) on high-end, medium-range, and low-end devices. + +
+ ## How to add your own plugin hooks? The `AnalyticalTable` internally uses all plugin hooks of the [react-table v7](https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/api/overview.md) except for `usePagination` and hooks that change the layout. @@ -17,9 +84,9 @@ If you pass a `react-table` hook to the `tableHooks` prop of the `AnalyticalTabl If you encounter a functionality that should be available with `react-table` hooks, but isn't in the `AnalyticalTable` please open an issue in GitHub. For adding custom plugin hooks you can use the following code snippet. @@ -438,8 +505,8 @@ For multi-line content or custom components that don't handle ellipsis, truncati ### Example
- Show Example - + Show Example +

diff --git a/packages/main/src/components/AnalyticalTable/PluginAnnounceEmptyCells.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginAnnounceEmptyCells.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/PluginAnnounceEmptyCells.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginAnnounceEmptyCells.mdx diff --git a/packages/main/src/components/AnalyticalTable/PluginDisableRowSelection.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginDisableRowSelection.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/PluginDisableRowSelection.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginDisableRowSelection.mdx diff --git a/packages/main/src/components/AnalyticalTable/PluginF2CellEdit.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginF2CellEdit.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/PluginF2CellEdit.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginF2CellEdit.mdx diff --git a/packages/main/src/components/AnalyticalTable/PluginIndeterminateRowSelection.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginIndeterminateRowSelection.mdx similarity index 96% rename from packages/main/src/components/AnalyticalTable/PluginIndeterminateRowSelection.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginIndeterminateRowSelection.mdx index 3a592a23711..5c12ed6136b 100644 --- a/packages/main/src/components/AnalyticalTable/PluginIndeterminateRowSelection.mdx +++ b/packages/main/src/components/AnalyticalTable/docs/PluginIndeterminateRowSelection.mdx @@ -1,7 +1,7 @@ import { ImportStatement } from '@sb/components/Import'; import { Canvas, Meta } from '@storybook/addon-docs/blocks'; import { Footer } from '@sb/components'; -import { MessageStrip } from '../../webComponents/MessageStrip/index'; +import { MessageStrip } from '../../../webComponents/MessageStrip/index.js'; import * as ComponentStories from './AnalyticalTableHooks.stories'; diff --git a/packages/main/src/components/AnalyticalTable/PluginManualRowSelect.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginManualRowSelect.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/PluginManualRowSelect.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginManualRowSelect.mdx diff --git a/packages/main/src/components/AnalyticalTable/PluginOnColumnResize.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginOnColumnResize.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/PluginOnColumnResize.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginOnColumnResize.mdx diff --git a/packages/main/src/components/AnalyticalTable/PluginOrderedMultiSort.mdx b/packages/main/src/components/AnalyticalTable/docs/PluginOrderedMultiSort.mdx similarity index 100% rename from packages/main/src/components/AnalyticalTable/PluginOrderedMultiSort.mdx rename to packages/main/src/components/AnalyticalTable/docs/PluginOrderedMultiSort.mdx diff --git a/packages/main/src/components/AnalyticalTable/docs/Recipes.mdx b/packages/main/src/components/AnalyticalTable/docs/Recipes.mdx new file mode 100644 index 00000000000..0a9d9242fa2 --- /dev/null +++ b/packages/main/src/components/AnalyticalTable/docs/Recipes.mdx @@ -0,0 +1,6 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { Redirect } from '@sb/components'; + + + + diff --git a/packages/main/src/components/AnalyticalTable/types/index.ts b/packages/main/src/components/AnalyticalTable/types/index.ts index 80416338ee9..dbcfbcbc98d 100644 --- a/packages/main/src/components/AnalyticalTable/types/index.ts +++ b/packages/main/src/components/AnalyticalTable/types/index.ts @@ -832,7 +832,9 @@ export interface AnalyticalTablePropTypes extends Omit { * The value of this prop can either be a `string` pointing to a `ValueState` or an `IndicationColor` in your dataset * or an accessor function which should return a `ValueState` or an `IndicationColor`. * - * __Note:__ `IndicationColor`s are available since `v1.26.0`. + * __Note:__ If a function is used, it __must be memoized!__ + * + * @since 1.26.0 * * @default "status" */ @@ -860,6 +862,8 @@ export interface AnalyticalTablePropTypes extends Omit { /** * Group table rows by adding the column's `accessor` or `id` to the array. * + * __Must be memoized!__ + * * __Note:__ This prop has no effect when `isTreeTable` is true or `renderRowSubComponent` is set. */ groupBy?: string[]; @@ -906,6 +910,8 @@ export interface AnalyticalTablePropTypes extends Omit { scaleXFactor?: number; /** * Defines the columns order by their `accessor` or `id`. + * + * __Must be memoized!__ */ columnOrder?: string[]; /** @@ -932,12 +938,14 @@ export interface AnalyticalTablePropTypes extends Omit { globalFilterValue?: string; /** * Additional options which will be passed to [v7 react-table“s useTable hook](https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/api/useTable.md#table-options) + * + * __Must be memoized!__ */ reactTableOptions?: Record; /** * You can use this prop to add custom hooks to the table. * - * __Note:__ Should be memoized! + * __Must be memoized!__ * * @default [] */ @@ -973,6 +981,8 @@ export interface AnalyticalTablePropTypes extends Omit { /** * Defines the subcomponent that should be displayed below each row. * + * __Must be memoized!__ + * * __Note:__ * - __There is no design concept regarding this functionality!__ * - When rendering active elements inside the subcomponent, make sure to add the `data-subcomponent-active-element' attribute, otherwise focus behavior won't be consistent.