Performance is User Experience. So this post will show how I create a performant menu.
Below is the performance recording for my menu under 6x slowdown CPU (with 8 categories and each category has 10 items). You can see there are no dropping frames when I scroll through all the 80-items long menu.

Context
I want to build a menu with some anchors attached to it. Those anchors help users scroll to exact category they are looking for. And I want to give users an overview of where they are, what is the current category they are watching.
In order to accomplish my goal. I know that I need to make something that can listen to the scroll event. Find a way to share the current category information through the app. And build a mechanism to prevent unnecessary renders.
Oh, one last thing to say. I also want to activate the last anchor chip when users scroll to the bottom.
Technology Overview
Here are the primary technologies used in this project:
- React: For the UI
- Create React App: Set up a modern web app by running one command
- TypeScript: Typed JavaScript (necessary for any project you plan to maintain)
- RxJS: For composing asynchronous events
- Tailwind CSS: Utility classes for consistent/maintainable styling
- Intersection Observer: For observing whether categories and footer are in view or not
- requestAnimationFrame: For smoothly scrolling vertically and horizontally
Design Overview

Combine complex events into one data stream
I expect the final data stream looks like the following. Please allow me using [marble diagram](https://rxmarbles.com/) here.
Imagine there are four categories. And the user scrolls the viewport top to bottom then goes back to top.
Complete code for my useSubscribeToScrollSpyGroup
is here.
Detect what users are browsing
I use intersectionObserver
instead of a scroll event listener. This way, sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimize the management of intersections as it sees fit.
Complete code for my spyTargetWithIntersectionObserver
is here.
Prevent unnecessary renders by observable
It can cause unnecessary rerenders if using state to manage those in view categories. With `Subject`, we can defer the computation and provide a way to subscribe to. Therefore, use RxJS to transform those menu category is in view notifications into an observable stream. In this way, components can easily subscribe and unsubscribe to this stream and then do whatever they need by operators.
Complete code for my ScrollSpyGroupManagerProvider
is here.
Remember last state when navigation back to Menu
Cause I need to get the last emitted value from the combine$
. I choose BehaviorSubject
instead of Subject
. It’s a variant of Subject
that requires an initial value and emits its current value whenever it is subscribed.
Reusable
The ScrollSpyGroupManagerProvider
can spy not only one single list. If there are two list to spy, I can make them into two groups by providing groupName
when using its API.
Smooth scroll animation
Use requestAnimationFrame
to implement the animation for scrolling. This method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.
For example, create a scrollWindowVerticallyTo
utility to handle scrolling window to a anchor:
Complete code for my scrollWindowVerticallyTo
is here. And there is also a scrollElementHorizontallyTo
here.
The Complete Code
The complete source code is in this GitHub repository — https://github.com/wtlin1228/menu-with-anchor. Feel free to clone it then measure the performance yourself.