diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a63f0a6b07..0233dfe0b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Added `euiTextBreakWord()` to `EuiToast` header ([#2549](https://github.com/elastic/eui/pull/2549)) - Fixed `.eui-textBreakAll` on Firefox ([#2549](https://github.com/elastic/eui/pull/2549)) - Fixed `EuiBetaBadge` accessibility with `tab-index=0` ([#2559](https://github.com/elastic/eui/pull/2559)) +- Improved `EuiIcon` loading performance ([#2565](https://github.com/elastic/eui/pull/2565)) ## [`16.0.1`](https://github.com/elastic/eui/tree/v16.0.1) diff --git a/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap b/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap index d3829f8e2fb..1f7e047d4d1 100644 --- a/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap +++ b/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap @@ -114,18 +114,13 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the class="euiContextMenu__itemLayout" > - - + /> @@ -148,18 +143,13 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the 2a - - + /> diff --git a/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap b/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap index 8ddd71b50f3..b2385514d71 100644 --- a/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap +++ b/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap @@ -114,18 +114,13 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the class="euiContextMenu__itemLayout" > - - + /> @@ -148,18 +143,13 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the 2a - - + /> diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index 8a8c19ca1df..59b7fef7029 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -6,12 +6,13 @@ import React, { useMemo, useState, } from 'react'; -import { createPortal, unstable_batchedUpdates } from 'react-dom'; +import { createPortal } from 'react-dom'; import { EuiDataGridCellValueElementProps, EuiDataGridCellProps, } from './data_grid_cell'; import { EuiDataGridColumn, EuiDataGridInMemory } from './data_grid_types'; +import { enqueueStateChange } from '../../services/react'; interface EuiDataGridInMemoryRendererProps { inMemory: EuiDataGridInMemory; @@ -27,27 +28,6 @@ interface EuiDataGridInMemoryRendererProps { function noop() {} -const _queue: Function[] = []; - -function processQueue() { - // the queued functions trigger react setStates which, if unbatched, - // each cause a full update->render->dom pass _per function_ - // instead, tell React to wait until all updates are finished before re-rendering - unstable_batchedUpdates(() => { - for (let i = 0; i < _queue.length; i++) { - _queue[i](); - } - _queue.length = 0; - }); -} - -function enqueue(fn: Function) { - if (_queue.length === 0) { - setTimeout(processQueue); - } - _queue.push(fn); -} - function getElementText(element: HTMLElement) { return 'innerText' in element ? element.innerText @@ -71,7 +51,9 @@ const ObservedCell: FunctionComponent<{ onCellRender(i, column, getElementText(ref)); const observer = new MutationObserver(() => { // onMutation callbacks aren't in the component lifecycle, intentionally batch any effects - enqueue(onCellRender.bind(null, i, column, getElementText(ref))); + enqueueStateChange( + onCellRender.bind(null, i, column, getElementText(ref)) + ); }); observer.observe(ref, { characterData: true, diff --git a/src/components/icon/icon.test.tsx b/src/components/icon/icon.test.tsx index c624075243e..29a8e7bfea7 100644 --- a/src/components/icon/icon.test.tsx +++ b/src/components/icon/icon.test.tsx @@ -10,14 +10,14 @@ const prettyHtml = cheerio.load(''); function testIcon(props: PropsOf) { return () => { - const component = mount(); - + expect.assertions(1); return new Promise(resolve => { - setTimeout(() => { + const onIconLoad = () => { component.update(); expect(prettyHtml(component.html())).toMatchSnapshot(); resolve(); - }, 0); + }; + const component = mount(); }); }; } diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 7e7158bf103..bcbf82293ec 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -15,6 +15,7 @@ import { CommonProps, keysOf } from '../common'; // TS file (dev/docs) or the JS file (distributed), and it's more effort than worth // to generate & git track a TS module definition for each icon component import { icon as empty } from './assets/empty.js'; +import { enqueueStateChange } from '../../services/react'; const typeToPathMap = { addDataApp: 'app_add_data', @@ -428,6 +429,10 @@ export type EuiIconProps = CommonProps & * Note that every size other than `original` assumes the provided SVG sits on a square viewbox. */ size?: IconSize; + /** + * Callback when the icon has been loaded & rendered + */ + onIconLoad?: () => void; }; interface State { @@ -500,12 +505,22 @@ export class EuiIcon extends PureComponent { // eslint-disable-next-line prefer-template './assets/' + typeToPathMap[iconType] + '.js' ).then(({ icon }) => { - if (this.isMounted) { - this.setState({ - icon, - isLoading: false, - }); - } + enqueueStateChange(() => { + if (this.isMounted) { + this.setState( + { + icon, + isLoading: false, + }, + () => { + const { onIconLoad } = this.props; + if (onIconLoad) { + onIconLoad(); + } + } + ); + } + }); }); }; @@ -516,6 +531,7 @@ export class EuiIcon extends PureComponent { color, className, tabIndex, + onIconLoad, ...rest } = this.props; diff --git a/src/services/react.ts b/src/services/react.ts new file mode 100644 index 00000000000..43492511228 --- /dev/null +++ b/src/services/react.ts @@ -0,0 +1,22 @@ +import { unstable_batchedUpdates } from 'react-dom'; + +const _queue: Function[] = []; + +function processQueue() { + // the queued functions trigger react setStates which, if unbatched, + // each cause a full update->render->dom pass _per function_ + // instead, tell React to wait until all updates are finished before re-rendering + unstable_batchedUpdates(() => { + for (let i = 0; i < _queue.length; i++) { + _queue[i](); + } + _queue.length = 0; + }); +} + +export function enqueueStateChange(fn: Function) { + if (_queue.length === 0) { + setTimeout(processQueue); + } + _queue.push(fn); +}