
ForesightJS is a lightweight JavaScript library that analyzes mouse movements to predict user intent on your web applications.
Most developers rely on hover events to trigger prefetching, but by then, valuable milliseconds have been wasted.
This library analyzes cursor trajectories in real-time to predict where users are heading, and triggers actions 80- 150ms before they even hover, resulting a significant improvement in perceived performance.
See It In Action:
Key Features:
- Mouse Trajectory Prediction: Uses velocity and direction to predict where cursors are heading
- Expandable Hit Areas: Custom “hit slop” regions that extend an element’s interactive area
- Predictive Prefetching: Starts loading resources before users hover or click
- Framework Agnostic: Works with any JavaScript framework or vanilla JS
- Visual Debugging: Built-in visualization tools to fine-tune predictions
- Customizable Settings: Adjust prediction timing, hit areas, and tracking sensitivity
Use Cases:
- Faster Navigation: Prefetch linked pages before users click on navigation items, making transitions feel instant
- Smart Resource Loading: Load images, data, or components when users move toward interactive elements, not after they hover
- Reduced Viewport Prefetching Waste: Replace aggressive viewport-based prefetching with intent-based predictions
- Enhanced Image Galleries: Preload high-resolution versions only when users show intent to interact with thumbnails
How to use it:
1. Install ForesightJS and import it into your web project.
# Yarn $ yarn add js.foresight # NPM $ npm install js.foresight # PNPM $ pnpm install js.foresight
import { ForesightManager } from "foresightjs"2. Basic usage
// Optional: Initialize once at app startup for custom global settings.
// If you're fine with defaults, you can skip this.
ForesightManager.initialize({
defaultHitSlop: { top: 30, left: 30, bottom: 80, right: 30 }, // Default invisible margin
// trajectoryPredictionTime: 80, // How far (ms) to predict ahead
});
// Get the element you want to track
const myButton = document.getElementById("my-button");
if (myButton) {
// Register the element
const { isTouchDevice, unregister } = ForesightManager.instance.register({
element: myButton,
callback: () => {
console.log("ForesightJS: User likely to interact with my-button. Prefetching now!");
// Your prefetching logic or other action here
},
hitSlop: 20, // Optional: Element-specific hit area expansion in pixels.
// Can be a number (all sides) or an object {top, left, bottom, right}.
name: "My Special Button", // Useful for debugging
unregisterOnCallback: true, // Default: true. Unregisters after first callback. Set false for multiple triggers.
});
// For touch devices, ForesightJS won't predict. Use isTouchDevice for fallback.
if (isTouchDevice) {
console.log("This is a touch device. Implement alternative prefetching/interaction logic.");
// e.g., myButton.addEventListener('touchstart', () => { /* prefetch on touch */ });
}
// IMPORTANT: Clean up when the element is no longer needed (e.g., component unmounts)
// If unregisterOnCallback is true, this happens automatically after the first callback.
// Otherwise, or if you need to unregister before callback, call unregister().
// For example, in a React component:
// useEffect(() => {
// // ... registration logic ...
// return () => unregister();
// }, []);
}3. Global options:
enableMousePrediction(boolean, default:true): Main switch for prediction.positionHistorySize(number, default:8): How many mouse positions to track for velocity.trajectoryPredictionTime(number, default:120): Milliseconds to predict into the future.defaultHitSlop(number | Rect, default:{top:0, left:0, right:0, bottom:0}): Default invisible padding around elements.scrollMargin(number, default: 150): Sets the pixel distance to check from the mouse position in the scroll direction callback.enableTabPrediction(boolean, default:true): Toggles whether keyboard prediction is on.enableScrollPrediction(boolean, default: true): Toggles whether scroll prediction is on on.tabOffset(number, default:2): Tab stops away from an element to trigger callback.touchDeviceStrategy(string, default:"onTouchStart"): Strategy to use for touch devices (mobile / pen users): “none”, “viewport”, “onTouchStart”.enableManagerLogging(boolean, default:false): Logs basic information about the ForesightManager and its handlers that is not available through events.minimumConnectionType(string, default:3g): slow-2g, 2g, 3g, and 4gonAnyCallbackFired: Executes after every individual element callback fires, regardless of which element triggered it.
ForesightManager.initialize({
enableMousePrediction: true,
positionHistorySize: 8,
trajectoryPredictionTime: 120,
defaultHitSlop: 10,
scrollMargin: 150,
enableTabPrediction: true,
enableScrollPrediction: true,
tabOffset: 3,
touchDeviceStrategy: "viewport",
enableManagerLogging: false,
minimumConnectionType: "3g",
onAnyCallbackFired: (elementData: ForesightElementData, managerData: ForesightManagerData) => {
console.log(`Callback hit from: ${elementData.name}`)
console.log(`Total tab hits: ${managerData.globalCallbackHits.tab}`)
console.log(`total mouse hits ${managerData.globalCallbackHits.mouse}`)
},
})4. Element-Specific register() Parameters:
element(HTMLElement, Required): The DOM element.callback(function, Required): Executed on predicted interaction.hitSlop(number | Rect, Optional): OverridesdefaultHitSlopfor this element. This is key for tuning. A small button might need a larger relativehitSlopthan a big banner.name(string, Optional): Descriptive name for debug mode.unregisterOnCallback(boolean, default:true): Iftrue,unregister()is called automatically after the callback executes once. Set tofalseif you need the callback to fire multiple times for the same element.
ForesightManager.instance.register({
element: myElement,
callback: prefetchData,
hitSlop: { top: 10, left: 50, right: 50, bottom: 100 }, // Element-specific hit area
name: "Product Card", // Useful in debug mode
unregisterOnCallback: false, // Allow callback to run multiple times
})Comparison with Alternatives
Traditional Hover-Based Prefetching (e.g., onmouseover):
- This is the old way. Simple to implement.
- The big downside is the delay. Prefetching only starts after the hover event, which itself can be 200-300ms after the user decided to move towards the element.
- ForesightJS aims to capture that 80-150ms window before the hover, making it better for truly time-sensitive prefetching.
Viewport-Based Prefetching (e.g., Next.js <Link> default, IntersectionObserver for links):
- Libraries like
quicklinkor the default behavior in some frameworks prefetch resources for all links that enter the viewport. - This is easy to set up and ensures resources are ready if the user eventually clicks something visible.
- The con is potential over-fetching. If a user scrolls past many links without intending to click any, you’ve wasted bandwidth and server resources. I’ve seen this firsthand on content-heavy pages.
- ForesightJS is more precise by focusing on active mouse intent, reducing unnecessary requests. It’s a good choice when bandwidth or server load from speculative prefetching is a concern.
- This library often preloads on
mousedownortouchstart, or sometimes on a brief hover. It’s very effective for making clicks feel instant. - ForesightJS differs by predicting before any click or even a full hover, based purely on mouse movement. It could potentially be used in conjunction with something like
instant.pageifinstant.pageis configured formousedown, with ForesightJS handling the “approach” andinstant.pagehandling the “commitment.”
FAQs
Q: How much overhead does ForesightJS add? Is it bad for performance?
A: It’s designed to be lightweight. The main work happens on mousemove, which can be frequent. However, calculations are generally simple (vector math on a small history). The resizeScrollThrottleDelay also helps. In my experience, the performance gain from earlier prefetching and smoother perceived interactions usually outweighs the minor computational cost. As always, profile in your specific app.
Q: What’s the best way to determine hitSlop values?
A: Start with the debug: true mode. This visualizes the hit areas. For critical interactive elements, a hitSlop of 10-50px might be a good starting point, depending on element size and density of your UI. For larger, more isolated elements, you might use a larger hitSlop. It’s an iterative process: observe user interaction patterns (or your own) and adjust.
Q: So, ForesightJS doesn’t do anything for touch devices?
A: Correct. It focuses on mouse movement. The register() method returns isTouchDevice, which you should use to implement a fallback. For touch, common strategies include prefetching on touchstart for links, or using IntersectionObserver for viewport-based preloading. The ForesightJS docs have examples for Next.js and React Router integrations that show how to handle this.
Q: Can I use ForesightJS with React, Vue, Angular, etc.?
A: Absolutely. It’s framework-agnostic since it operates on DOM elements. In React, you’d typically register an element in a useEffect hook (getting a ref to the DOM element) and return the unregister function from the useEffect for cleanup. Similar patterns apply to Vue (mounted/beforeUnmount) and Angular (ngOnInit/ngOnDestroy).
Q: What if the user’s mouse movements are chaotic or they just wiggle the mouse over an area?
A: The prediction is based on a recent, short trajectory. If the mouse movement is truly random without clear direction towards an element, it’s less likely to trigger a callback unless the cursor happens to pass through a hit area with sufficient projected velocity. The trajectoryPredictionTime and positionHistorySize settings influence its sensitivity. If unregisterOnCallback is false, rapid movements over an element could trigger the callback multiple times if intent is repeatedly predicted.
Q: Is there a risk of triggering too many callbacks if unregisterOnCallback is false?
A: Yes, potentially. If a user hovers near an element or moves their mouse back and forth over its “approach path,” the callback could fire multiple times. This is why unregisterOnCallback: true is the default and suitable for most prefetching scenarios (prefetch once). If you set it to false, ensure your callback is idempotent or designed to handle multiple calls gracefully.
Changelog:
v3.4.0 (01/18/2026)
- Added lazy loading to the predictors.
- Added loadedModules to getManagerData()
v3.3.4 (11/18/2025)
- Update
v3.3.2 (08/02/2025)
- Added support for configuring the minimum connection speed with the minimumConnectionType prop. Options include slow-2g, 2g, 3g, and 4g (default: 3g). This replaces the previous fixed value of slow-2g.
v3.3.0 (08/02/2025)
- Added a new enableManagerLogging prop for the ForesightManager.
v3.3.0 (08/02/2025)
- Added first-party touch device support.
- Added currentDeviceStrategy and activeElementCount to ForesightManager.instance.getManagerData.
- Added new deviceStrategyChanged event.
- Added wasLastActiveElement to callbackCompleted event.
- Added wasLastRegisteredElement to elementUnregistered event.
- Now removes all handlers when there are no active elements anymore.
- Removed trajectoryHitData from ForesightElementData since it is not needed anymore for calculations.
v3.2.0 (07/22/2025)
- Changed the lifecycle of an registered element.
v3.1.3 (07/15/2025)
- Added new meta prop when registering an element as Record<string, unknown>.
v3.1.2 (07/14/2025)
- When all elements are unregistered automatically turn of all event listeners
- Only turn on event listeners after the first element has been registered
- Smarter event listener setup. e.g. if enableTabPrediction is off dont listen to keydown and focusin instead of instantly returning.
- Added wasLastElement boolean to elementUnregistered event
- Added circularbuffer for faster array manipulations
v3.1.0 (07/11/2025)
- Event logging and DevTools redesigned/rewritten
v3.0.0 (07/01/2025)
- New Standalone Development Tools Package
- New Event System
v2.2.3 (06/13/2025)
- The minimized debug control panel now displays the count of visible and total elements on the page.
- The mouse and scroll indicator in debug mode now look a bit more modern
- ForesightJS will now respect the user’s data-saver settings by not registering the element if enabled
- ForesightJS will now not register elements if the user is on 2G internet connection
- ForesightManager.instance.register() now returns isLimitedConnection and isRegistered next to the previous isTouchDevice for if an element is not registered
v2.2 (06/13/2025)
- added a margin in the direction of the scroll in which prefetches will happen Playground
- added enableScrollPrediction to toggle whether we should prefetch based on scroll
- added scrollMargin to set the amount of px in scroll margin
- add Debugger Features
- Tab Prediction got a big performance boost by caching tabbable elements and optimizing the index lookup algorithm
- Debugger info explanations are now a lot more detailed.
- globalCallbackHits in the ForesightManager.instance.getManagerData static property now show total callback execution counts by interaction type (mouse/tab/scroll) and by subtype (hover/trajectory for mouse, forwards/reverse for tab, direction for scroll)
- Renamed UpdateForsightManagerProps type to UpdateForsightManagerSettings
- Now use translate3d instead of setting ‘top’ ‘left’ etc on debug elements to avoid reflow
- Bug Fixes
v2.1 (06/09/2025)
- Added onAnyCallbackFired to the ForesightManager, this function will run when ANY callback function is hit.
- Added new debuggerSettings.showNameTags to show or hide name tags on startup (true by default)
- Added toggle to hide/show name tags in debugger
- The debugger now shows an icon 👁 for if a registered element is in the viewport and thus being tracked
- The debugger now shows total amount of visible elements and total callback hitcount (split by mouse and tab)
- Added new Static Property Foresightmanager.instance.getManagerData
- Removed ResizeObserver, scroll events and resize events. Replaced it with PositionObserver. Read more about why.
- Added IntersectionObserver to only observe elements currently in the viewport.
- Registered element’s name will now be backfilled by ID if there is no name, still defaults to “” if both are missing.
- Debug overlays are now being calculated using transform instead of setting their bounds for performance. This removes any Cumulative Layout Shift (CLS) while moving the mouse in debug mode
- Renamed ForesightManagerProps type to ForesightManagerSettings
- Renamed UpdateForsightManagerProps type to UpdateForsightManagerSettings
v2.0.4 (06/07/2025)
- Added ForesightManager.instance.registeredElements readonly prop to get all registered elements and their settings
- Made all static properties read-only so editors can provide type errors.
- New color on the mouse overlay for the debugger
v2.0.3 (06/06/2025)
- Added ForesightManager.instance.isInitiated static prop to check if the ForesightManager has been initialized
- Added ForesightManager.instance.globalSettings static prop to check which global settings are currently configured in the ForesightManager
Changes - Increased default value of trajectoryPredictionTime from 80 to 120
- Decreased default value of tabOffset from 3 to 2
- Decreased max positionHistorySize from 50 to 30
- Fixed bug where the ForesightManagerwould always initialize with default values
- Fixed bug where if you decreased positionHistorySize the leftover positions weren’t cleared from the array.
- Fixed visual bug where on FireFox the debugger would overflow
v2.0.1 (06/04/2025)
- added clamped values to global settings
v2.0 (06/02/2025)
- Full keyboard support
- Added new features
- Fixed bugs






