Skip to content

perf: optimise trackpad/mousepad scrolling with Lenis#35

Merged
prdai merged 3 commits intoBitByBit-B3:mainfrom
Thanukamax:perf/trackpad-scroll-optimisation
May 10, 2026
Merged

perf: optimise trackpad/mousepad scrolling with Lenis#35
prdai merged 3 commits intoBitByBit-B3:mainfrom
Thanukamax:perf/trackpad-scroll-optimisation

Conversation

@Thanukamax
Copy link
Copy Markdown
Contributor

@Thanukamax Thanukamax commented May 7, 2026

Problem

Two symptoms on trackpads / precision touchpads:

  1. Jank during momentum scrollingusePinScale and useHScroll fired a scroll callback on every scroll event. During trackpad momentum the browser can dispatch 3–5 events per animation frame, each calling getBoundingClientRect() and writing a new transform. Repeated forced-layout + paint per frame → visible stutter.

  2. No consistent scroll easing — without a smooth-scroll library, each OS/browser combination applies its own trackpad momentum curve, causing the pin-scale and horizontal-scroll sections to feel different (or sluggish) across platforms.

  3. Double-smoothing on anchor navigationhtml { scroll-behavior: smooth } was unconditional. Trackpads already supply native inertia; CSS smooth-scroll on top creates a rubber-band / over-animated feel.

Changes

package.json

  • Added lenis ^1.1.14 — same version used in the portfolio site (thanukamax.github.io).

src/layouts/Layout.astro

  • Initialized Lenis with a simple requestAnimationFrame loop and smoothWheel: true. Lenis intercepts wheel/trackpad events and replaces the browser's variable per-platform momentum with a consistent lerp easing curve. Since this repo doesn't use Framer Motion (unlike the portfolio's frame.update sync), Lenis manages its own RAF loop.

src/styles/global.css

  • Added Lenis CSS resets (.lenis.lenis-smooth { scroll-behavior: auto } etc.) so Lenis doesn't fight CSS smooth-scroll on anchor nav.
  • Moved html { scroll-behavior: smooth } inside @media (prefers-reduced-motion: no-preference) — also satisfies the accessibility requirement.

src/hooks/use-b3-animations.ts

  • Wrapped both usePinScale and useHScroll scroll callbacks in a requestAnimationFrame guard so DOM reads/writes happen at most once per frame.
  • Cached total / maxTx on mount; a passive resize listener keeps them fresh instead of recalculating every tick.
  • Guard against total === 0 to avoid NaN transforms on edge-case layouts.

Test plan

  • Scroll through the home page on a trackpad — should feel smooth and consistent (same lerp curve as the portfolio site)
  • Pin-scale section (section 2): content should scale smoothly locked to scroll progress
  • Horizontal scroll section (products): cards should pan smoothly with no lag or over-shoot
  • Anchor links still animate smoothly on desktop mouse
  • DevTools → prefers-reduced-motion: reducescroll-behavior should be auto
  • No console errors related to Lenis or scroll handlers

🤖 Generated with Claude Code

- Wrap usePinScale and useHScroll scroll callbacks in requestAnimationFrame
  so DOM reads/writes happen once per frame even during trackpad momentum
  bursts (prevents layout-thrashing on high-frequency scroll events)
- Cache `total` and `maxTx` on mount; update via a passive resize listener
  instead of recalculating on every scroll tick
- Guard against zero-total edge case (avoids NaN transform values)
- Move `scroll-behavior: smooth` under prefers-reduced-motion media query
  so native trackpad inertia is no longer double-smoothed by CSS
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@Thanukamax has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 33 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7193501d-37b9-4fd1-8646-cf9a388eeb3d

📥 Commits

Reviewing files that changed from the base of the PR and between b1fe9b1 and 5526568.

📒 Files selected for processing (4)
  • package.json
  • src/hooks/use-b3-animations.ts
  • src/layouts/Layout.astro
  • src/styles/global.css

Walkthrough

Two animation hooks (usePinScale and useHScroll) are refactored to throttle scroll-driven DOM updates via requestAnimationFrame with proper cleanup and range recalculation on resize. CSS styling for html is reordered to prioritize scroll-behavior: smooth within reduced-motion media queries, removing duplicate declarations.

Changes

Scroll Animation Performance Optimization

Layer / File(s) Summary
Pin Scale Hook RAF Optimization
src/hooks/use-b3-animations.ts
usePinScale now computes scrollable range on resize, applies scale/opacity changes inside RAF callback guarded by rafId, and cancels pending RAF during cleanup.
Horizontal Scroll Hook RAF Optimization
src/hooks/use-b3-animations.ts
useHScroll now computes scroll metrics on resize, schedules translate transforms inside RAF callback, and properly cancels RAF on cleanup.
Scroll Behavior Styling
src/styles/global.css
html declarations reordered so scroll-behavior: smooth appears first in @media (prefers-reduced-motion: no-preference), with duplicate ordering removed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

Smooth scrolls now dance with grace,

RequestAnimationFrame sets the pace,

No stutters, no skips across the screen—

CSS smoothness joins the scene! ✨📜

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: optimizing scroll event handling for trackpad/mousepad interactions using requestAnimationFrame throttling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes scroll-based animations in use-b3-animations.ts by introducing requestAnimationFrame for throttling and adding resize listeners to maintain accurate scroll boundaries. Additionally, it enhances accessibility by restricting smooth scrolling to users who haven't enabled reduced motion settings. Feedback focuses on ensuring the UI remains synchronized during window resizing by triggering scroll updates immediately and clamping scroll translation values to avoid layout issues.

Comment thread src/hooks/use-b3-animations.ts Outdated
Comment thread src/hooks/use-b3-animations.ts Outdated
Comment thread src/hooks/use-b3-animations.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/use-b3-animations.ts`:
- Around line 37-45: The scroll-driven scale/scroll effects in usePinScale (the
requestAnimationFrame block that sets content.style.transform/opacity and
manages rafId) and the similar logic in useHScroll must be skipped when the user
prefers reduced motion: detect window.matchMedia('(prefers-reduced-motion:
reduce)').matches at the start of each effect and bail out (return) before
registering any event listeners or requestAnimationFrame; also ensure any
existing rafId is cancelled in cleanup. Update both hooks to gate their effect
setup on this media-query check so transforms/RAF are not used for users who
opted out of motion.
- Around line 43-44: The opacity "1" is being written to content.style on every
RAF frame inside the scroll/rAF handler in useB3Animations, which can stomp
entry animations; move the line that sets content.style.opacity = "1" out of the
RAF/scroll callback and run it once during effect setup (e.g., in the hook’s
initialization where you obtain content) or remove it entirely if the element
should start visible, ensuring only the transform/scale updates remain inside
the rAF handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f71d3798-f6de-48ab-9293-62249c9fbc86

📥 Commits

Reviewing files that changed from the base of the PR and between 031b236 and b1fe9b1.

📒 Files selected for processing (2)
  • src/hooks/use-b3-animations.ts
  • src/styles/global.css

Comment thread src/hooks/use-b3-animations.ts
Comment thread src/hooks/use-b3-animations.ts Outdated
Install lenis ^1.1.14 and wire up a simple requestAnimationFrame loop
in the root Astro layout — same pattern used in the portfolio site's
smooth-scroll.tsx (minus the Framer Motion frame.update sync since
Framer isn't present here).

Lenis intercepts wheel/trackpad events and eases them with a consistent
lerp curve, replacing the browser's variable per-platform momentum with
predictable smooth deceleration. The .lenis CSS resets prevent the
lenis-smooth class from fighting scroll-behavior: smooth on anchor nav.
@Thanukamax Thanukamax changed the title perf: optimise trackpad/mousepad scroll handlers perf: optimise trackpad/mousepad scrolling with Lenis May 7, 2026
- Gate both usePinScale and useHScroll behind prefers-reduced-motion
  check so scroll-driven transforms don't fire for users who opted out
- Move content.style.opacity = "1" outside the rAF to a one-time setup
  call so it can't stomp entry animations driven by useB3Observe
- Call onScroll() inside onResize() so visual state syncs immediately
  after a viewport resize (not deferred until next scroll event)
- Clamp maxTx to Math.max(0, ...) so narrow viewports where track is
  smaller than the viewport don't produce a negative translation
@prdai prdai merged commit e006ae4 into BitByBit-B3:main May 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants