From 56cddc6ad610e46de9167983f8b0423abf3ef92d Mon Sep 17 00:00:00 2001 From: Edo Z <0xe1216@gmail.com> Date: Wed, 30 Sep 2026 16:34:17 +0400 Subject: [PATCH] perf: skip hidden notification progress updates --- src/Notification.tsx | 17 +++- tests/notification-rendering.test.tsx | 119 ++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 tests/notification-rendering.test.tsx diff --git a/src/Notification.tsx b/src/Notification.tsx index 0f68c54..4f13aec 100644 --- a/src/Notification.tsx +++ b/src/Notification.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { clsx } from 'clsx'; import useNoticeTimer from './hooks/useNoticeTimer'; -import { isNonNullable, isReactRenderable, useEvent } from '@rc-component/util'; +import { isNonNullable, isReactRenderable, useEvent, useLayoutEffect } from '@rc-component/util'; import useClosable, { type ClosableType } from './hooks/useClosable'; import DefaultProgress from './Progress'; import type { NotificationProgressProps } from './Progress'; @@ -106,6 +106,7 @@ const Notification = React.forwardRef((props, } = props; const [percent, setPercent] = React.useState(0); + const percentRef = React.useRef(0); const noticePrefixCls = `${prefixCls}-notice`; // ========================= Close ========================== @@ -118,7 +119,19 @@ const Notification = React.forwardRef((props, // ======================== Duration ======================== const [hovering, setHovering] = React.useState(false); - const [onResume, onPause] = useNoticeTimer(duration, onInternalClose, setPercent); + const [onResume, onPause] = useNoticeTimer(duration, onInternalClose, (nextPercent) => { + percentRef.current = nextPercent; + if (showProgress) { + setPercent(nextPercent); + } + }); + + // Progress can be enabled while paused, when no timer tick will update state. + useLayoutEffect(() => { + if (showProgress) { + setPercent(percentRef.current); + } + }, [showProgress]); const validPercent = 100 - Math.min(Math.max(percent * 100, 0), 100); const Progress = components?.progress || DefaultProgress; diff --git a/tests/notification-rendering.test.tsx b/tests/notification-rendering.test.tsx new file mode 100644 index 0000000..6374623 --- /dev/null +++ b/tests/notification-rendering.test.tsx @@ -0,0 +1,119 @@ +import { act, render } from '@testing-library/react'; +import React from 'react'; +import Notification from '../src/Notification'; + +function advanceFrames(count: number) { + for (let frame = 0; frame < count; frame += 1) { + act(() => vi.advanceTimersByTime(16)); + } +} + +describe('Notification timer rendering', () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + it.each([undefined, false])( + 'does not render hidden progress on timer ticks (%s)', + (showProgress) => { + const onRender = vi.fn(); + const onClose = vi.fn(); + const { queryByRole, unmount } = render( + + + , + ); + const initialRenderCount = onRender.mock.calls.length; + + advanceFrames(32); + + expect(onRender).toHaveBeenCalledTimes(initialRenderCount); + expect(queryByRole('progressbar')).toBeNull(); + expect(onClose).not.toHaveBeenCalled(); + + advanceFrames(32); + expect(onClose).toHaveBeenCalledTimes(1); + unmount(); + }, + ); + + it('continues rendering visible progress and closes at the original duration', () => { + const onClose = vi.fn(); + const { getByRole, unmount } = render( + , + ); + + advanceFrames(32); + expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(48.8, 1); + expect(onClose).not.toHaveBeenCalled(); + + advanceFrames(32); + expect(onClose).toHaveBeenCalledTimes(1); + unmount(); + }); + + it('updates progress when enabled during an existing countdown without restarting it', () => { + const onClose = vi.fn(); + const onRender = vi.fn(); + const notice = (showProgress: boolean) => ( + + + + ); + const { rerender, getByRole, unmount } = render(notice(false)); + + advanceFrames(16); + rerender(notice(true)); + advanceFrames(16); + expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(48.8, 1); + + rerender(notice(false)); + const renderCount = onRender.mock.calls.length; + advanceFrames(16); + expect(onRender).toHaveBeenCalledTimes(renderCount); + expect(onClose).not.toHaveBeenCalled(); + + advanceFrames(16); + expect(onClose).toHaveBeenCalledTimes(1); + unmount(); + }); + + it('shows elapsed progress when enabled while the countdown is paused', () => { + const onClose = vi.fn(); + const notice = (showProgress: boolean, hovering: boolean) => ( + + ); + const { rerender, getByRole, unmount } = render(notice(false, false)); + + advanceFrames(16); + rerender(notice(false, true)); + advanceFrames(32); + rerender(notice(true, true)); + + expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(74.4, 1); + advanceFrames(16); + expect((getByRole('progressbar') as HTMLProgressElement).value).toBeCloseTo(74.4, 1); + expect(onClose).not.toHaveBeenCalled(); + + rerender(notice(true, false)); + advanceFrames(48); + expect(onClose).toHaveBeenCalledTimes(1); + unmount(); + }); +});