Conversation
- Added `{ passive: true }` option to scroll event listeners in `BackToTop.tsx`, `DynamicHeaderWrapper.tsx`, and `Layout.tsx`.
- Throttled the execution of scroll handlers via `window.requestAnimationFrame` to avoid main-thread blocking.
- Addressed `no-restricted-syntax` lint warnings by wrapping tracking states in a const object.
Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds RAF-based scroll throttling and cleanup to three layout/back-to-top components; sets Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
components/layout/DynamicHeaderWrapper.tsx (1)
21-41: Drop the capturedscrolldependency from this RAF handler.The
scrollCheck !== scrollguard makes this effect depend onscroll, so the listener is removed and re-added whenever the threshold flips. A functional state update keeps the subscription mounted once, which is closer to the optimization this PR is aiming for.Suggested refactor
const handleScroll = (): void => { if (!state.isTicking) { state.rafId = window.requestAnimationFrame(() => { const scrollCheck: boolean = window.scrollY > 100; - if (scrollCheck !== scroll) { - setScroll(scrollCheck); - } + setScroll((prev) => (prev === scrollCheck ? prev : scrollCheck)); state.isTicking = false; }); state.isTicking = true; } }; @@ - }, [scroll]); + }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/layout/DynamicHeaderWrapper.tsx` around lines 21 - 41, The RAF handler currently closes over the `scroll` variable causing the effect to depend on `scroll`; change `handleScroll` (and its use of `state`, `state.rafId`, `state.isTicking`) so it does not reference the outer `scroll` value and instead performs a functional state update: inside the RAF callback compute const scrollCheck = window.scrollY > 100 and call setScroll(prev => (prev === scrollCheck ? prev : scrollCheck)); remove `scroll` from the effect dependency array (e.g., use []), keep the same `handleScroll` reference for add/removeEventListener, and preserve the existing cancelAnimationFrame logic for `state.rafId`.components/layout/Layout.tsx (1)
82-104: KeepAOS.init()out of the scroll subscription.Because this effect still depends on
scroll, Line 81 re-initializes AOS every time the header crosses the 100px threshold. That adds unrelated work back into the path this PR is trying to optimize. Split AOS into a mount-only effect and use a functionalsetScrollso the listener effect can stay stable.Suggested refactor
- useEffect(() => { - AOS.init(); - - const state = { rafId: 0, isTicking: false }; + useEffect(() => { + AOS.init(); + }, []); + + useEffect(() => { + const state = { rafId: 0, isTicking: false }; const handleScroll = (): void => { if (!state.isTicking) { state.rafId = window.requestAnimationFrame(() => { const scrollCheck: boolean = window.scrollY > 100; - if (scrollCheck !== scroll) { - setScroll(scrollCheck); - } + setScroll((prev) => (prev === scrollCheck ? prev : scrollCheck)); state.isTicking = false; }); state.isTicking = true; } }; document.addEventListener("scroll", handleScroll, { passive: true }); return () => { document.removeEventListener("scroll", handleScroll); if (state.rafId) { window.cancelAnimationFrame(state.rafId); } }; - }, [scroll]); + }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/layout/Layout.tsx` around lines 82 - 104, Split AOS initialization into its own mount-only useEffect that calls AOS.init() once (no dependencies) and remove AOS.init() from the scroll effect; in the scroll subscription effect keep a stable handle by using a functional state updater with setScroll(prev => { if (prev === scrollCheck) return prev; return scrollCheck; }) so the effect no longer depends on scroll, and keep the RAF tracking/cleanup inside that same effect (replace the local mutable state object with refs or variables scoped to the effect and ensure window.cancelAnimationFrame is called on cleanup). Target symbols: AOS.init(), setScroll, handleScroll, the scroll subscription useEffect and the RAF tracking (rafId/isTicking).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/elements/BackToTop.tsx`:
- Around line 10-25: The component currently only updates visibility when a
scroll event occurs; after attaching the listener call the existing onScroll
handler (or directly run setHasScrolled(window.scrollY > 100)) once to
initialize state so hasScrolled reflects the current position on mount; update
the setup around window.addEventListener("scroll", onScroll, { passive: true })
to invoke onScroll() immediately after registering the listener and keep the
existing cleanup that removes the listener and cancels state.rafId, referencing
the onScroll function, state, and setHasScrolled.
---
Nitpick comments:
In `@components/layout/DynamicHeaderWrapper.tsx`:
- Around line 21-41: The RAF handler currently closes over the `scroll` variable
causing the effect to depend on `scroll`; change `handleScroll` (and its use of
`state`, `state.rafId`, `state.isTicking`) so it does not reference the outer
`scroll` value and instead performs a functional state update: inside the RAF
callback compute const scrollCheck = window.scrollY > 100 and call
setScroll(prev => (prev === scrollCheck ? prev : scrollCheck)); remove `scroll`
from the effect dependency array (e.g., use []), keep the same `handleScroll`
reference for add/removeEventListener, and preserve the existing
cancelAnimationFrame logic for `state.rafId`.
In `@components/layout/Layout.tsx`:
- Around line 82-104: Split AOS initialization into its own mount-only useEffect
that calls AOS.init() once (no dependencies) and remove AOS.init() from the
scroll effect; in the scroll subscription effect keep a stable handle by using a
functional state updater with setScroll(prev => { if (prev === scrollCheck)
return prev; return scrollCheck; }) so the effect no longer depends on scroll,
and keep the RAF tracking/cleanup inside that same effect (replace the local
mutable state object with refs or variables scoped to the effect and ensure
window.cancelAnimationFrame is called on cleanup). Target symbols: AOS.init(),
setScroll, handleScroll, the scroll subscription useEffect and the RAF tracking
(rafId/isTicking).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3f896970-fd7f-4233-8dfd-295411cc1196
📒 Files selected for processing (3)
components/elements/BackToTop.tsxcomponents/layout/DynamicHeaderWrapper.tsxcomponents/layout/Layout.tsx
| const onScroll = () => { | ||
| setHasScrolled(window.scrollY > 100); | ||
| if (!state.isTicking) { | ||
| state.rafId = window.requestAnimationFrame(() => { | ||
| setHasScrolled(window.scrollY > 100); | ||
| state.isTicking = false; | ||
| }); | ||
| state.isTicking = true; | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener("scroll", onScroll); | ||
| return () => window.removeEventListener("scroll", onScroll); | ||
| window.addEventListener("scroll", onScroll, { passive: true }); | ||
| return () => { | ||
| window.removeEventListener("scroll", onScroll); | ||
| if (state.rafId) { | ||
| window.cancelAnimationFrame(state.rafId); | ||
| } |
There was a problem hiding this comment.
Initialize hasScrolled once after subscribing.
If the page mounts with scrollY > 100 already set, this stays hidden until the next scroll event. Run the handler once during setup so the initial visibility matches the current position.
Suggested fix
window.addEventListener("scroll", onScroll, { passive: true });
+ onScroll();
return () => {
window.removeEventListener("scroll", onScroll);
if (state.rafId) {
window.cancelAnimationFrame(state.rafId);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onScroll = () => { | |
| setHasScrolled(window.scrollY > 100); | |
| if (!state.isTicking) { | |
| state.rafId = window.requestAnimationFrame(() => { | |
| setHasScrolled(window.scrollY > 100); | |
| state.isTicking = false; | |
| }); | |
| state.isTicking = true; | |
| } | |
| }; | |
| window.addEventListener("scroll", onScroll); | |
| return () => window.removeEventListener("scroll", onScroll); | |
| window.addEventListener("scroll", onScroll, { passive: true }); | |
| return () => { | |
| window.removeEventListener("scroll", onScroll); | |
| if (state.rafId) { | |
| window.cancelAnimationFrame(state.rafId); | |
| } | |
| const onScroll = () => { | |
| if (!state.isTicking) { | |
| state.rafId = window.requestAnimationFrame(() => { | |
| setHasScrolled(window.scrollY > 100); | |
| state.isTicking = false; | |
| }); | |
| state.isTicking = true; | |
| } | |
| }; | |
| window.addEventListener("scroll", onScroll, { passive: true }); | |
| onScroll(); | |
| return () => { | |
| window.removeEventListener("scroll", onScroll); | |
| if (state.rafId) { | |
| window.cancelAnimationFrame(state.rafId); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/elements/BackToTop.tsx` around lines 10 - 25, The component
currently only updates visibility when a scroll event occurs; after attaching
the listener call the existing onScroll handler (or directly run
setHasScrolled(window.scrollY > 100)) once to initialize state so hasScrolled
reflects the current position on mount; update the setup around
window.addEventListener("scroll", onScroll, { passive: true }) to invoke
onScroll() immediately after registering the listener and keep the existing
cleanup that removes the listener and cancels state.rafId, referencing the
onScroll function, state, and setHasScrolled.
- Removed `minimatch` and `ajv` from `package.json` overrides to fix broken Jest and ESLint configurations. - Regenerated `package-lock.json` with `npm install --legacy-peer-deps`. - Updated Cypress test assertions in `home-editions.cy.ts` to check for `.hero8-header__event-line` instead of `h5` to match component structure. - Updated Cypress assertions in `talks-and-speakers-layout.cy.ts` and added missing `name="q"` to search inputs to make UI tests pass. Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
cypress/e2e/talks-and-speakers-layout.cy.ts (1)
11-16: Consider renaming the shared class for clarity.The test correctly uses
.talks-filter-barsinceSpeakersFilterBarreuses this class name. However, using a class named "talks-filter-bar" on the Speakers page is semantically confusing.Consider extracting a shared base class (e.g.,
.filter-bar) or renaming to a generic name in both components for better maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cypress/e2e/talks-and-speakers-layout.cy.ts` around lines 11 - 16, The test and components reuse the class ".talks-filter-bar" which is semantically confusing on the Speakers page; update components and tests to use a shared, generic class (e.g., ".filter-bar") by renaming the class in the SpeakersFilterBar and the TalksFilterBar usages and update this test assertion from ".talks-filter-bar" to the new generic selector, and also ensure any stylesheet/selectors referencing ".talks-filter-bar" are updated accordingly so both components use the same shared ".filter-bar" class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@cypress/e2e/talks-and-speakers-layout.cy.ts`:
- Around line 11-16: The test and components reuse the class ".talks-filter-bar"
which is semantically confusing on the Speakers page; update components and
tests to use a shared, generic class (e.g., ".filter-bar") by renaming the class
in the SpeakersFilterBar and the TalksFilterBar usages and update this test
assertion from ".talks-filter-bar" to the new generic selector, and also ensure
any stylesheet/selectors referencing ".talks-filter-bar" are updated accordingly
so both components use the same shared ".filter-bar" class.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fd9a3820-b4c3-4415-825c-5b2adb11d4b9
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
components/layout/SpeakersFilterBar.tsxcomponents/layout/TalksFilterBar.tsxcypress/e2e/home/home-editions.cy.tscypress/e2e/talks-and-speakers-layout.cy.tspackage.json
💤 Files with no reviewable changes (1)
- package.json
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly improves the application's scrolling performance and responsiveness by optimizing scroll event listeners. By leveraging Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a performance optimization for scroll event listeners by using requestAnimationFrame and the passive: true option. This is a great improvement to ensure smooth scrolling.
However, in DynamicHeaderWrapper.tsx and Layout.tsx, the useEffect hook that sets up the scroll listener has a dependency on the scroll state. This causes the event listener to be removed and re-added every time the scroll position crosses the 100px threshold, which is inefficient and partially negates the optimization.
I've left comments with suggestions to remove this dependency, which will make the optimization fully effective. The implementation in BackToTop.tsx is already correct and can be used as a reference.
| React.useEffect(() => { | ||
| const state = { rafId: 0, isTicking: false }; | ||
|
|
||
| const handleScroll = (): void => { | ||
| const scrollCheck: boolean = window.scrollY > 100; | ||
| if (scrollCheck !== scroll) { | ||
| setScroll(scrollCheck); | ||
| if (!state.isTicking) { | ||
| state.rafId = window.requestAnimationFrame(() => { | ||
| const scrollCheck: boolean = window.scrollY > 100; | ||
| if (scrollCheck !== scroll) { | ||
| setScroll(scrollCheck); | ||
| } | ||
| state.isTicking = false; | ||
| }); | ||
| state.isTicking = true; | ||
| } | ||
| }; | ||
| document.addEventListener("scroll", handleScroll); | ||
|
|
||
| document.addEventListener("scroll", handleScroll, { passive: true }); | ||
| return () => { | ||
| document.removeEventListener("scroll", handleScroll); | ||
| if (state.rafId) { | ||
| window.cancelAnimationFrame(state.rafId); | ||
| } | ||
| }; | ||
| }, [scroll]); |
There was a problem hiding this comment.
The useEffect hook has scroll in its dependency array. This causes the effect to re-run every time the scroll state changes, removing and re-adding the scroll event listener. This is inefficient and undermines the performance optimization.
The dependency on scroll can be removed by simplifying the scroll handler. React's state setter already prevents re-renders if the new value is the same as the current one, so the check if (scrollCheck !== scroll) is not necessary when the dependency is removed.
React.useEffect(() => {
const state = { rafId: 0, isTicking: false };
const handleScroll = (): void => {
if (!state.isTicking) {
state.rafId = window.requestAnimationFrame(() => {
setScroll(window.scrollY > 100);
state.isTicking = false;
});
state.isTicking = true;
}
};
document.addEventListener("scroll", handleScroll, { passive: true });
return () => {
document.removeEventListener("scroll", handleScroll);
if (state.rafId) {
window.cancelAnimationFrame(state.rafId);
}
};
}, []);
| useEffect(() => { | ||
| AOS.init(); | ||
|
|
||
| const state = { rafId: 0, isTicking: false }; | ||
|
|
||
| const handleScroll = (): void => { | ||
| const scrollCheck: boolean = window.scrollY > 100; | ||
| if (scrollCheck !== scroll) { | ||
| setScroll(scrollCheck); | ||
| if (!state.isTicking) { | ||
| state.rafId = window.requestAnimationFrame(() => { | ||
| const scrollCheck: boolean = window.scrollY > 100; | ||
| if (scrollCheck !== scroll) { | ||
| setScroll(scrollCheck); | ||
| } | ||
| state.isTicking = false; | ||
| }); | ||
| state.isTicking = true; | ||
| } | ||
| }; | ||
|
|
||
| document.addEventListener("scroll", handleScroll); | ||
| document.addEventListener("scroll", handleScroll, { passive: true }); | ||
|
|
||
| return () => { | ||
| document.removeEventListener("scroll", handleScroll); | ||
| if (state.rafId) { | ||
| window.cancelAnimationFrame(state.rafId); | ||
| } | ||
| }; | ||
| }, [scroll]); |
There was a problem hiding this comment.
The useEffect hook has scroll in its dependency array. This causes the effect to re-run every time the scroll state changes, removing and re-adding the scroll event listener. This is inefficient and undermines the performance optimization.
The dependency on scroll can be removed by simplifying the scroll handler, similar to the implementation in BackToTop.tsx. React's state setter already prevents re-renders if the new value is the same as the current one.
useEffect(() => {
AOS.init();
const state = { rafId: 0, isTicking: false };
const handleScroll = (): void => {
if (!state.isTicking) {
state.rafId = window.requestAnimationFrame(() => {
setScroll(window.scrollY > 100);
state.isTicking = false;
});
state.isTicking = true;
}
};
document.addEventListener("scroll", handleScroll, { passive: true });
return () => {
document.removeEventListener("scroll", handleScroll);
if (state.rafId) {
window.cancelAnimationFrame(state.rafId);
}
};
}, []);
- Regenerated `package-lock.json` with `npm install` without package-lock-only to ensure it syncs accurately with `package.json` dependencies and fixes `npm ci` failures. Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com>
- Used standard `npm install` without legacy flag specifically to match what the `npm ci` needs on the GitHub actions runner. This generates a stable tree preventing lock file desync issues. Co-authored-by: anyulled <100741+anyulled@users.noreply.github.com>
⚡ Bolt: Optimized Scroll Event Listeners
💡 What:
Added
{ passive: true }option andrequestAnimationFrameto throttle globalscrollevent listeners inBackToTop.tsx,DynamicHeaderWrapper.tsx, andLayout.tsx.🎯 Why:
Un-throttled synchronous scroll listeners trigger React state changes directly on the main thread, leading to layout trashing and frame drops during scrolling. Adding
passiveensures scrolling is not delayed by the JavaScript thread, andrequestAnimationFramelimits execution to screen refresh rate (typically 60 times a second).📊 Impact:
Eliminates layout trashing from main thread during scroll events, guaranteeing fluid scrolling regardless of computation time and preventing unnecessary re-renders in header and back-to-top components.
🔬 Measurement:
Run a Chrome DevTools performance trace with 4x CPU throttling during page scroll. The "Event: scroll" handling time should significantly decrease, and the main thread should show fewer long frames associated with layout updates.
PR created automatically by Jules for task 3605098856570520295 started by @anyulled
Summary by CodeRabbit