# Behind the Scenes

note

Reading this is not necessary to use the library; this is just for understanding how ForesightJS works.

## Architecture[​](#architecture "Direct link to 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-parenting
* **`PositionObserver`**: Shopify's library that asynchronously monitors element position changes without polling

The `PositionObserver` uses layered observation:

1. `VisibilityObserver` (built on `IntersectionObserver`) determines if elements are viewport-visible
2. `ResizeObserver` tracks size changes of visible target elements
3. Target-specific `IntersectionObserver` instances with smart rootMargin calculations transform viewport observation into target-specific observation regions

## Keyboard & Mouse Users[​](#keyboard--mouse-users "Direct link to Keyboard & Mouse Users")

### Mouse Prediction[​](#mouse-prediction "Direct link to 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:

1. **History Tracking**: Stores past mouse positions with timestamps
2. **Velocity Calculation**: Calculates average velocity using oldest and newest points in history
3. **Extrapolation**: Projects current position along trajectory using calculated velocity and `trajectoryPredictionTimeInMs` setting

**Intersection Detection**: The `lineSegmentIntersectsRect` function implements the Liang-Barsky line clipping algorithm to check intersections between the predicted mouse path and element rectangles:

1. Line segment defined by current and predicted mouse positions
2. Target rectangle includes element's `hitSlop`
3. Algorithm clips line segment against rectangle's four edges
4. Intersection confirmed if entry parameter ≤ exit parameter

### Keyboard Prediction[​](#keyboard-prediction "Direct link to Keyboard Prediction")

Tab prediction monitors keyboard navigation by detecting Tab key presses and focus changes.

**Event Handling**:

* `keydown`: Detects Tab key presses
* `focusin`: 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:

1. Identify current focused element's index in tabbable elements list
2. Determine tab direction (forward/backward based on Shift key)
3. Calculate prediction range using current index, direction, and `tabOffset`
4. Trigger callbacks for registered elements within predicted range

### Scroll Prediction[​](#scroll-prediction "Direct link to 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-device-users "Direct link to 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[​](#element-lifecycle "Direct link to Element Lifecycle")

1. **Registration**: `ForesightManager.instance.register({ element, callback })` adds the element to the internal Map
2. **Callback Execution**: When user intent is detected, element's callback function triggers
3. **Deactivation**: `isPredicted` set to `true` and the element becomes inactive after callback execution
4. **Reactivation Wait**: Element remains inactive for `reactivateAfter` duration (defaults to infinity)
5. **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[​](#bundle-optimization "Direct link to 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.
