Behind the Scenes
Reading this is not necessary to use the library; this is just for understanding how ForesightJS works.
Architecture
ForesightJS uses a singleton pattern with ForesightManager as the central instance managing all prediction logic, registered elements, and global settings. Elements are stored in a Map where the key is the registered element itself. Multiple registrations of the same element overwrite the existing entry.
Since DOM elements can change position and we need to keep the DOM clean, we require element.getBoundingClientRect() for each element on each update. To avoid triggering reflows, ForesightJS uses observers:
MutationObserver: Detects when registered elements are detached from or reattached to the DOM, so they can be parked (kept registered but inactive) and resumed. This keeps prediction working across detach/reattach and re-parentingPositionObserver: Shopify's library that asynchronously monitors element position changes without polling
The PositionObserver uses layered observation:
VisibilityObserver(built onIntersectionObserver) determines if elements are viewport-visibleResizeObservertracks size changes of visible target elements- Target-specific
IntersectionObserverinstances with smart rootMargin calculations transform viewport observation into target-specific observation regions
Keyboard & Mouse Users
Mouse Prediction
Mouse prediction tracks cursor movement patterns to anticipate click targets by analyzing velocity and trajectory.
Event Handling: mousemove events record clientX and clientY coordinates. Position history is limited by positionHistorySize setting.
Prediction Algorithm: The predictNextMousePosition function implements linear extrapolation:
- History Tracking: Stores past mouse positions with timestamps
- Velocity Calculation: Calculates average velocity using oldest and newest points in history
- Extrapolation: Projects current position along trajectory using calculated velocity and
trajectoryPredictionTimeInMssetting
Intersection Detection: The lineSegmentIntersectsRect function implements the Liang-Barsky line clipping algorithm to check intersections between the predicted mouse path and element rectangles:
- Line segment defined by current and predicted mouse positions
- Target rectangle includes element's
hitSlop - Algorithm clips line segment against rectangle's four edges
- Intersection confirmed if entry parameter ≤ exit parameter
Keyboard Prediction
Tab prediction monitors keyboard navigation by detecting Tab key presses and focus changes.
Event Handling:
keydown: Detects Tab key pressesfocusin: Fires when elements gain focus
When focusin follows a Tab keydown, ForesightJS identifies this as tab navigation.
Tab Navigation Logic: Uses the tabbable library to determine tab order. Results are cached for performance since tabbable() calls getBoundingClientRect() internally. Cache invalidates on DOM mutations.
Prediction process:
- Identify current focused element's index in tabbable elements list
- Determine tab direction (forward/backward based on Shift key)
- Calculate prediction range using current index, direction, and
tabOffset - Trigger callbacks for registered elements within predicted range
Scroll Prediction
Leverages existing observer infrastructure without additional event listeners. The PositionObserver callback provides elements that have moved. By analyzing the delta between previous and new boundingClientRect for viewport-remaining elements, ForesightJS infers scroll direction.
Note: This also triggers during animations or dynamic layout changes.
Touch Device Users
Touch devices have no continuous cursor, so prediction works differently. The touchDeviceStrategy setting picks one of two active strategies (a third, "none", turns touch prediction off entirely):
Viewport Entry: Uses IntersectionObserver to detect when elements enter the viewport. Callbacks trigger when elements become visible during scrolling, providing anticipatory loading for content that will soon be in view.
Touch Start: Listens for touchstart events on registered elements. Callbacks fire immediately when users begin touching an element, before the actual click event occurs, providing a performance advantage for touch interactions.
Element Lifecycle
- Registration:
ForesightManager.instance.register({ element, callback })adds the element to the internal Map - Callback Execution: When user intent is detected, element's callback function triggers
- Deactivation:
isPredictedset totrueand the element becomes inactive after callback execution - Reactivation Wait: Element remains inactive for
reactivateAfterduration (defaults to infinity) - Reactivation: Element becomes active again after duration elapses
Cleanup: Elements are removed via ForesightManager.instance.unregister(element). Detaching an element from the DOM does not unregister it. The MutationObserver parks it (keeps it registered but inactive) and resumes it when it reattaches.
Bundle Optimization
The full library is ~32 KB minified, but you rarely load all of it. Code splitting lazy-loads handlers and predictors based on device type and which features are enabled, so your initial bundle only contains what you actually use:
| Chunk | Size | Loaded When |
|---|---|---|
| Core | ~14 KB | Always (initial load) |
| DesktopHandler + MousePredictor | ~15 KB | Desktop/mouse users |
| TouchDeviceHandler | ~2 KB | Touch device users |
| TabPredictor + tabbable | ~7 KB | enableTabPrediction: true |
| ScrollPredictor | ~1.5 KB | enableScrollPrediction: true |
| Touch predictors | ~1.5 KB | Touch device (based on strategy) |
Result: a touch device loads only the core, the touch handler, and one touch predictor. A desktop user loads the core, the desktop handler, and whichever predictors they have enabled (mouse, tab, scroll). You never ship the strategies you don't use.