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);