Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .storybook/components/Redirect.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IllustratedMessage
name="PageNotFound"
titleText="Page Moved"
subtitle={
<Text>
This page has been moved to <strong>{sectionName}</strong>.
<br />
Redirecting in {countdown} seconds...
</Text>
}
>
<Link href={newHref} target="_top">
Go to {sectionName}
</Link>
</IllustratedMessage>
);
};
1 change: 1 addition & 0 deletions .storybook/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './TableOfContent.js';
export * from './LabelWithWrapping.js';
export * from './CommandsAndQueries.js';
export * from './FilterBarExample.js';
export * from './Redirect.js';
4 changes: 4 additions & 0 deletions .storybook/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,92 @@
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';

<Meta title="Data Display / AnalyticalTable / Recipes" />
<Meta title="Data Display / AnalyticalTable / FAQ" />

# AnalyticalTable Recipes
# AnalyticalTable FAQ

<TableOfContent />

## 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.

<details>
<summary>Example: Performance Trace Comparison</summary>

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.

</details>

## 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.
If you pass a `react-table` hook to the `tableHooks` prop of the `AnalyticalTable` you will most likely experience some side effects or even break the table completely. We therefore strongly recommend to just not do it ;).
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.

<MessageStrip
type="Warning"
hideCloseButton
children={`Plugin hooks can manipulate the internal table instance. Please use with caution!`}
type="Warning"
hideCloseButton
children={`Plugin hooks can manipulate the internal table instance. Please use with caution!`}
/>

For adding custom plugin hooks you can use the following code snippet.
Expand Down Expand Up @@ -438,8 +505,8 @@ For multi-line content or custom components that don't handle ellipsis, truncati
### Example

<details>
<summary>Show Example</summary>
<Canvas of={ComponentStories.EllipsisExamples} />
<summary>Show Example</summary>
<Canvas of={ComponentStories.EllipsisExamples} />
</details>

<br />
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

<Meta title="Data Display / AnalyticalTable / Plugin Hooks / useIndeterminateRowSelection" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Meta } from '@storybook/addon-docs/blocks';
import { Redirect } from '@sb/components';

<Meta title="Data Display / AnalyticalTable / Recipes" excludeFromSidebar />

<Redirect from="analyticaltable-recipes" to="analyticaltable-faq" sectionName="FAQ" />
14 changes: 12 additions & 2 deletions packages/main/src/components/AnalyticalTable/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,9 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
* 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"
*/
Expand Down Expand Up @@ -860,6 +862,8 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
/**
* 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[];
Expand Down Expand Up @@ -906,6 +910,8 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
scaleXFactor?: number;
/**
* Defines the columns order by their `accessor` or `id`.
*
* __Must be memoized!__
*/
columnOrder?: string[];
/**
Expand All @@ -932,12 +938,14 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
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<string, unknown>;
/**
* You can use this prop to add custom hooks to the table.
*
* __Note:__ Should be memoized!
* __Must be memoized!__
*
* @default []
*/
Expand Down Expand Up @@ -973,6 +981,8 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
/**
* 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.
Expand Down
Loading