From 2e4195750162f547a3b09a950536ff087d186132 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:22:05 +0000 Subject: [PATCH 1/3] perf: throttle scroll event listeners and mark passive - Add `{ passive: true }` to global `scroll` event listeners to prevent scroll blocking. - Throttle state updates inside scroll handlers using `requestAnimationFrame` to run at most once per frame. - Tested and verified no visual regression on scrolling. Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com> --- .../layout/DynamicHeaderWrapper.test.tsx | 3 +++ __tests__/components/layout/Layout.test.tsx | 3 +++ __tests__/snapshots/elements/BackToTop.test.tsx | 13 ++++++++++++- components/elements/BackToTop.tsx | 11 +++++++++-- components/layout/DynamicHeaderWrapper.tsx | 15 ++++++++++----- components/layout/Layout.tsx | 16 +++++++++++----- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/__tests__/components/layout/DynamicHeaderWrapper.test.tsx b/__tests__/components/layout/DynamicHeaderWrapper.test.tsx index 45f30ebc..5226b9b6 100644 --- a/__tests__/components/layout/DynamicHeaderWrapper.test.tsx +++ b/__tests__/components/layout/DynamicHeaderWrapper.test.tsx @@ -52,6 +52,9 @@ describe("DynamicHeaderWrapper", () => { expect(screen.getByTestId("header8")).toHaveAttribute("data-scroll", "false"); Object.defineProperty(window, "scrollY", { value: 120, writable: true, configurable: true }); + window.requestAnimationFrame = (cb) => { + cb(); + }; fireEvent.scroll(document); expect(screen.getByTestId("header8")).toHaveAttribute("data-scroll", "true"); diff --git a/__tests__/components/layout/Layout.test.tsx b/__tests__/components/layout/Layout.test.tsx index e326e1c3..dafa15e0 100644 --- a/__tests__/components/layout/Layout.test.tsx +++ b/__tests__/components/layout/Layout.test.tsx @@ -96,6 +96,9 @@ describe("Layout", () => { expect(screen.getByTestId("header-1")).toHaveAttribute("data-scroll", "false"); Object.defineProperty(window, "scrollY", { value: 150, writable: true, configurable: true }); + window.requestAnimationFrame = (cb) => { + cb(); + }; fireEvent.scroll(document); expect(screen.getByTestId("header-1")).toHaveAttribute("data-scroll", "true"); diff --git a/__tests__/snapshots/elements/BackToTop.test.tsx b/__tests__/snapshots/elements/BackToTop.test.tsx index 25288fb9..a8434a3d 100644 --- a/__tests__/snapshots/elements/BackToTop.test.tsx +++ b/__tests__/snapshots/elements/BackToTop.test.tsx @@ -1,5 +1,5 @@ import { expect, describe, it } from "@jest/globals"; -import { render } from "@testing-library/react"; +import { render, fireEvent, screen } from "@testing-library/react"; import BackToTop from "@/components/elements/BackToTop"; describe("BackToTop Component", () => { @@ -7,4 +7,15 @@ describe("BackToTop Component", () => { const { container } = render(); expect(container).toMatchSnapshot(); }); + + it("updates scroll state on scroll event", () => { + render(); + + Object.defineProperty(window, "scrollY", { value: 150, writable: true, configurable: true }); + window.requestAnimationFrame = (cb) => { + cb(performance.now()); + return 1; + }; + fireEvent.scroll(window); + }); }); diff --git a/components/elements/BackToTop.tsx b/components/elements/BackToTop.tsx index 3fdb6396..9aaacc42 100644 --- a/components/elements/BackToTop.tsx +++ b/components/elements/BackToTop.tsx @@ -5,11 +5,18 @@ export default function BackToTop({ target }: Readonly<{ target: string }>) { const [hasScrolled, setHasScrolled] = useState(false); useEffect(() => { + const state = { isTicking: false }; const onScroll = () => { - setHasScrolled(window.scrollY > 100); + if (!state.isTicking) { + window.requestAnimationFrame(() => { + setHasScrolled(window.scrollY > 100); + state.isTicking = false; + }); + state.isTicking = true; + } }; - window.addEventListener("scroll", onScroll); + window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); diff --git a/components/layout/DynamicHeaderWrapper.tsx b/components/layout/DynamicHeaderWrapper.tsx index 1206feb6..dbb5a706 100644 --- a/components/layout/DynamicHeaderWrapper.tsx +++ b/components/layout/DynamicHeaderWrapper.tsx @@ -16,17 +16,22 @@ export default function DynamicHeaderWrapper({ navigation }: Readonly(false); React.useEffect(() => { + const state = { isTicking: false }; const handleScroll = (): void => { - const scrollCheck: boolean = window.scrollY > 100; - if (scrollCheck !== scroll) { - setScroll(scrollCheck); + if (!state.isTicking) { + window.requestAnimationFrame(() => { + const scrollCheck: boolean = window.scrollY > 100; + setScroll((prev) => (prev !== scrollCheck ? scrollCheck : prev)); + state.isTicking = false; + }); + state.isTicking = true; } }; - document.addEventListener("scroll", handleScroll); + document.addEventListener("scroll", handleScroll, { passive: true }); return () => { document.removeEventListener("scroll", handleScroll); }; - }, [scroll]); + }, []); return ( <> diff --git a/components/layout/Layout.tsx b/components/layout/Layout.tsx index 7f289889..958cb750 100644 --- a/components/layout/Layout.tsx +++ b/components/layout/Layout.tsx @@ -79,19 +79,25 @@ export default function Layout({ headerStyle, footerStyle, breadcrumbTitle: _bre useEffect(() => { AOS.init(); + + const state = { isTicking: false }; const handleScroll = (): void => { - const scrollCheck: boolean = window.scrollY > 100; - if (scrollCheck !== scroll) { - setScroll(scrollCheck); + if (!state.isTicking) { + window.requestAnimationFrame(() => { + const scrollCheck: boolean = window.scrollY > 100; + setScroll((prev) => (prev !== scrollCheck ? scrollCheck : prev)); + state.isTicking = false; + }); + state.isTicking = true; } }; - document.addEventListener("scroll", handleScroll); + document.addEventListener("scroll", handleScroll, { passive: true }); return () => { document.removeEventListener("scroll", handleScroll); }; - }, [scroll]); + }, []); const defaultNavigation: EditionNavigation = { main: mainNavLinks, From 201dd1f165902425b204bce20eb0fccb7dfb720f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:26:51 +0000 Subject: [PATCH 2/3] perf: throttle scroll event listeners and mark passive - Add `{ passive: true }` to global `scroll` event listeners to prevent scroll blocking. - Throttle state updates inside scroll handlers using `requestAnimationFrame` to run at most once per frame. - Tested and verified no visual regression on scrolling. - Fixed unused variable `screen` in `__tests__/snapshots/elements/BackToTop.test.tsx` causing CI lint error. Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com> --- __tests__/snapshots/elements/BackToTop.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/snapshots/elements/BackToTop.test.tsx b/__tests__/snapshots/elements/BackToTop.test.tsx index a8434a3d..9368ca2e 100644 --- a/__tests__/snapshots/elements/BackToTop.test.tsx +++ b/__tests__/snapshots/elements/BackToTop.test.tsx @@ -1,5 +1,5 @@ import { expect, describe, it } from "@jest/globals"; -import { render, fireEvent, screen } from "@testing-library/react"; +import { render, fireEvent } from "@testing-library/react"; import BackToTop from "@/components/elements/BackToTop"; describe("BackToTop Component", () => { From 099469668a10a16ddaa088d535b3f39b270944c7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:30:31 +0000 Subject: [PATCH 3/3] perf: throttle scroll event listeners and mark passive - Add `{ passive: true }` to global `scroll` event listeners to prevent scroll blocking. - Throttle state updates inside scroll handlers using `requestAnimationFrame` to run at most once per frame. - Tested and verified no visual regression on scrolling. - Fixed unused variable `screen` in `__tests__/snapshots/elements/BackToTop.test.tsx` causing CI lint error. Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com> --- __tests__/components/layout/DynamicHeaderWrapper.test.tsx | 3 ++- __tests__/components/layout/Layout.test.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/__tests__/components/layout/DynamicHeaderWrapper.test.tsx b/__tests__/components/layout/DynamicHeaderWrapper.test.tsx index 5226b9b6..bd40f679 100644 --- a/__tests__/components/layout/DynamicHeaderWrapper.test.tsx +++ b/__tests__/components/layout/DynamicHeaderWrapper.test.tsx @@ -53,7 +53,8 @@ describe("DynamicHeaderWrapper", () => { Object.defineProperty(window, "scrollY", { value: 120, writable: true, configurable: true }); window.requestAnimationFrame = (cb) => { - cb(); + cb(performance.now()); + return 1; }; fireEvent.scroll(document); diff --git a/__tests__/components/layout/Layout.test.tsx b/__tests__/components/layout/Layout.test.tsx index dafa15e0..d3130390 100644 --- a/__tests__/components/layout/Layout.test.tsx +++ b/__tests__/components/layout/Layout.test.tsx @@ -97,7 +97,8 @@ describe("Layout", () => { Object.defineProperty(window, "scrollY", { value: 150, writable: true, configurable: true }); window.requestAnimationFrame = (cb) => { - cb(); + cb(performance.now()); + return 1; }; fireEvent.scroll(document);