Skip to content

Commit e5f970d

Browse files
kibanamachineDosantweronikaolejniczak
authored
[9.2] [SideNav] Reduce re-renders on resize and items change (elastic#239888) (elastic#240884)
# Backport This will backport the following commits from `main` to `9.2`: - [[SideNav] Reduce re-renders on resize and items change (elastic#239888)](elastic#239888) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Anton Dosov","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-10-27T18:10:49Z","message":"[SideNav] Reduce re-renders on resize and items change (elastic#239888)\n\n## Summary\n\nResolves https://github.com/elastic/kibana/issues/239331\n\nI noticed that when nav present the window resizing becomes sluggish.\nThis was caused by height recalcs in resize observable. it is especially\nnoticeable on 4x slowdown.\n\nRe-renders before (see just number of renders around the nav)\n\n\nhttps://github.com/user-attachments/assets/4bf99d8d-eeca-4a5b-8e3f-578b20c0e4e3\n\nRe-renders after (see just number of renders around the nav)\n\n\nhttps://github.com/user-attachments/assets/db68007b-792e-499c-bcb0-259a3cd433c9\n\n---------\n\nCo-authored-by: Weronika Olejniczak <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>","sha":"29b3a5d8fb82723ab94b5d85f5c248808e737be4","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:SharedUX","backport:version","v9.2.0","v9.3.0"],"title":"[SideNav] Reduce re-renders on resize and items change ","number":239888,"url":"https://github.com/elastic/kibana/pull/239888","mergeCommit":{"message":"[SideNav] Reduce re-renders on resize and items change (elastic#239888)\n\n## Summary\n\nResolves https://github.com/elastic/kibana/issues/239331\n\nI noticed that when nav present the window resizing becomes sluggish.\nThis was caused by height recalcs in resize observable. it is especially\nnoticeable on 4x slowdown.\n\nRe-renders before (see just number of renders around the nav)\n\n\nhttps://github.com/user-attachments/assets/4bf99d8d-eeca-4a5b-8e3f-578b20c0e4e3\n\nRe-renders after (see just number of renders around the nav)\n\n\nhttps://github.com/user-attachments/assets/db68007b-792e-499c-bcb0-259a3cd433c9\n\n---------\n\nCo-authored-by: Weronika Olejniczak <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>","sha":"29b3a5d8fb82723ab94b5d85f5c248808e737be4"}},"sourceBranch":"main","suggestedTargetBranches":["9.2"],"targetPullRequestStates":[{"branch":"9.2","label":"v9.2.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/239888","number":239888,"mergeCommit":{"message":"[SideNav] Reduce re-renders on resize and items change (elastic#239888)\n\n## Summary\n\nResolves https://github.com/elastic/kibana/issues/239331\n\nI noticed that when nav present the window resizing becomes sluggish.\nThis was caused by height recalcs in resize observable. it is especially\nnoticeable on 4x slowdown.\n\nRe-renders before (see just number of renders around the nav)\n\n\nhttps://github.com/user-attachments/assets/4bf99d8d-eeca-4a5b-8e3f-578b20c0e4e3\n\nRe-renders after (see just number of renders around the nav)\n\n\nhttps://github.com/user-attachments/assets/db68007b-792e-499c-bcb0-259a3cd433c9\n\n---------\n\nCo-authored-by: Weronika Olejniczak <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>","sha":"29b3a5d8fb82723ab94b5d85f5c248808e737be4"}}]}] BACKPORT--> Co-authored-by: Anton Dosov <[email protected]> Co-authored-by: Weronika Olejniczak <[email protected]>
1 parent 8636e4f commit e5f970d

File tree

5 files changed

+174
-88
lines changed

5 files changed

+174
-88
lines changed

src/core/packages/chrome/navigation/src/__tests__/both_modes.test.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -510,11 +510,13 @@ describe('Both modes', () => {
510510
expect(link).toBeInTheDocument();
511511
});
512512

513-
const twelfthLink = screen.queryByRole('link', {
514-
name: securityMock.navItems.primaryItems[11].label,
515-
});
513+
await waitFor(() => {
514+
const twelfthLink = screen.queryByRole('link', {
515+
name: securityMock.navItems.primaryItems[11].label,
516+
});
516517

517-
expect(twelfthLink).not.toBeInTheDocument();
518+
expect(twelfthLink).not.toBeInTheDocument();
519+
});
518520

519521
const moreButton = screen.getByRole('button', {
520522
name: 'More',
@@ -559,7 +561,7 @@ describe('Both modes', () => {
559561
/>
560562
);
561563

562-
const moreButton = screen.getByRole('button', {
564+
const moreButton = await screen.findByRole('button', {
563565
name: 'More',
564566
});
565567

@@ -602,7 +604,7 @@ describe('Both modes', () => {
602604
// Security mock has exactly 13 primary menu items
603605
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
604606

605-
const moreButton = screen.getByRole('button', {
607+
const moreButton = await screen.findByRole('button', {
606608
name: 'More',
607609
});
608610

@@ -644,7 +646,7 @@ describe('Both modes', () => {
644646
/>
645647
);
646648

647-
const moreButton = screen.getByRole('button', {
649+
const moreButton = await screen.findByRole('button', {
648650
name: 'More',
649651
});
650652

@@ -1294,7 +1296,7 @@ describe('Both modes', () => {
12941296
it('should move focus to the first or last item in the popover when pressing Home or End', async () => {
12951297
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
12961298

1297-
const moreButton = screen.getByRole('button', { name: 'More' });
1299+
const moreButton = await screen.findByRole('button', { name: 'More' });
12981300

12991301
act(() => {
13001302
moreButton.focus();
@@ -1335,7 +1337,7 @@ describe('Both modes', () => {
13351337
it('should return focus to the menu item that opened the popover when it is closed', async () => {
13361338
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
13371339

1338-
const moreButton = screen.getByRole('button', { name: 'More' });
1340+
const moreButton = await screen.findByRole('button', { name: 'More' });
13391341

13401342
await user.click(moreButton);
13411343

@@ -1365,7 +1367,7 @@ describe('Both modes', () => {
13651367
it('should move focus to the next primary menu or footer menu item when pressing Tab', async () => {
13661368
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
13671369

1368-
const moreButton = screen.getByRole('button', { name: 'More' });
1370+
const moreButton = await screen.findByRole('button', { name: 'More' });
13691371

13701372
act(() => {
13711373
moreButton.focus();
@@ -1399,7 +1401,7 @@ describe('Both modes', () => {
13991401
it('should move focus to the previous primary menu or footer menu item when pressing Shift + Tab', async () => {
14001402
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
14011403

1402-
const moreButton = screen.getByRole('button', { name: 'More' });
1404+
const moreButton = await screen.findByRole('button', { name: 'More' });
14031405

14041406
act(() => {
14051407
moreButton.focus();
@@ -1435,7 +1437,7 @@ describe('Both modes', () => {
14351437
it('should focus the "Go back" button when opening a nested panel with Enter', async () => {
14361438
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
14371439

1438-
const moreButton = screen.getByRole('button', { name: 'More' });
1440+
const moreButton = await screen.findByRole('button', { name: 'More' });
14391441

14401442
act(() => {
14411443
moreButton.focus();
@@ -1471,7 +1473,7 @@ describe('Both modes', () => {
14711473
it('should keep focus within nested submenu items when using arrow keys', async () => {
14721474
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
14731475

1474-
const moreButton = screen.getByRole('button', { name: 'More' });
1476+
const moreButton = await screen.findByRole('button', { name: 'More' });
14751477

14761478
act(() => {
14771479
moreButton.focus();
@@ -1538,7 +1540,7 @@ describe('Both modes', () => {
15381540
it('should return focus to the trigger that opened the nested panel when activating the "Go back" button', async () => {
15391541
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
15401542

1541-
const moreButton = screen.getByRole('button', { name: 'More' });
1543+
const moreButton = await screen.findByRole('button', { name: 'More' });
15421544

15431545
act(() => {
15441546
moreButton.focus();
@@ -1574,7 +1576,7 @@ describe('Both modes', () => {
15741576
it('does NOT close the popover when onBlur has relatedTarget === null (Safari quirk)', async () => {
15751577
render(<TestComponent items={securityMock.navItems} logo={securityMock.logo} />);
15761578

1577-
const moreButton = screen.getByRole('button', { name: 'More' });
1579+
const moreButton = await screen.findByRole('button', { name: 'More' });
15781580
act(() => {
15791581
moreButton.focus();
15801582
});

src/core/packages/chrome/navigation/src/__tests__/collapsed_mode.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,13 +386,13 @@ describe('Collapsed mode', () => {
386386
* WHEN the navigation renders
387387
* THEN I should see a "More" primary menu item
388388
*/
389-
it('should render the "More" primary menu item when items overflow', () => {
389+
it('should render the "More" primary menu item when items overflow', async () => {
390390
// Renders 10 primary menu items + "More" item
391391
render(
392392
<TestComponent isCollapsed items={securityMock.navItems} logo={securityMock.logo} />
393393
);
394394

395-
const moreButton = screen.getByRole('button', {
395+
const moreButton = await screen.findByRole('button', {
396396
name: 'More',
397397
});
398398

@@ -410,7 +410,7 @@ describe('Collapsed mode', () => {
410410
<TestComponent isCollapsed items={securityMock.navItems} logo={securityMock.logo} />
411411
);
412412

413-
const moreButton = screen.getByRole('button', {
413+
const moreButton = await screen.findByRole('button', {
414414
name: 'More',
415415
});
416416

@@ -440,7 +440,7 @@ describe('Collapsed mode', () => {
440440
<TestComponent isCollapsed items={securityMock.navItems} logo={securityMock.logo} />
441441
);
442442

443-
const moreButton = screen.getByRole('button', {
443+
const moreButton = await screen.findByRole('button', {
444444
name: 'More',
445445
});
446446

@@ -491,7 +491,7 @@ describe('Collapsed mode', () => {
491491
<TestComponent isCollapsed items={securityMock.navItems} logo={securityMock.logo} />
492492
);
493493

494-
const moreButton = screen.getByRole('button', {
494+
const moreButton = await screen.findByRole('button', {
495495
name: 'More',
496496
});
497497

@@ -539,7 +539,7 @@ describe('Collapsed mode', () => {
539539
/>
540540
);
541541

542-
const moreButton = screen.getByRole('button', {
542+
const moreButton = await screen.findByRole('button', {
543543
name: 'More',
544544
});
545545

src/core/packages/chrome/navigation/src/__tests__/expanded_mode.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ describe('Expanded mode', () => {
385385
/>
386386
);
387387

388-
const moreButton = screen.getByRole('button', {
388+
const moreButton = await screen.findByRole('button', {
389389
name: 'More',
390390
});
391391

@@ -421,7 +421,7 @@ describe('Expanded mode', () => {
421421
/>
422422
);
423423

424-
const moreButton = screen.getByRole('button', {
424+
const moreButton = await screen.findByRole('button', {
425425
name: 'More',
426426
});
427427

@@ -490,7 +490,7 @@ describe('Expanded mode', () => {
490490
/>
491491
);
492492

493-
const moreButton = screen.getByRole('button', {
493+
const moreButton = await screen.findByRole('button', {
494494
name: 'More',
495495
});
496496

@@ -538,7 +538,7 @@ describe('Expanded mode', () => {
538538
/>
539539
);
540540

541-
const moreButton = screen.getByRole('button', {
541+
const moreButton = await screen.findByRole('button', {
542542
name: 'More',
543543
});
544544

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { useCallback, useLayoutEffect, useRef } from 'react';
11+
12+
/**
13+
* A hook that returns a debounced callback using requestAnimationFrame.
14+
* @param fn - The callback function to debounce.
15+
*/
16+
export function useRafDebouncedCallback(fn: () => void) {
17+
const rafIdRef = useRef<number | null>(null);
18+
const fnRef = useRef(fn);
19+
20+
// Always call the latest fn
21+
useLayoutEffect(() => {
22+
fnRef.current = fn;
23+
}, [fn]);
24+
25+
const schedule = useCallback(() => {
26+
if (rafIdRef.current != null) cancelAnimationFrame(rafIdRef.current);
27+
rafIdRef.current = requestAnimationFrame(() => {
28+
rafIdRef.current = null;
29+
fnRef.current();
30+
});
31+
}, []);
32+
33+
const cancel = useCallback(() => {
34+
if (rafIdRef.current != null) {
35+
cancelAnimationFrame(rafIdRef.current);
36+
rafIdRef.current = null;
37+
}
38+
}, []);
39+
40+
// Auto-cancel on unmount
41+
useLayoutEffect(() => () => cancel(), [cancel]);
42+
43+
return [schedule, cancel] as const;
44+
}

0 commit comments

Comments
 (0)