# ForesightJS > Comprehensive guide to ForesightJS, the most modern way to prefetch your data. Check out llms-full.txt for the full txt and llms.txt for an overview of the docs. - [ForesightJS](/index.md) ## search - [Search the documentation](/search.md) ## docs ### 3.5 - [AI Context](/docs/3.5/ai-context.md): How to use ForesightJS's llms.txt file with AI tools and LLMs - [Angular](/docs/3.5/angular.md): Integration details to add ForesightJS to your Angular projects - [Behind the Scenes](/docs/3.5/Behind_the_Scenes.md): A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements using linear extrapolation and the Liang-Barsky algorithm, and how it predicts tab navigation. - [Element Settings](/docs/3.5/configuration/element-settings.md): Configure individual element settings when registering with ForesightManager - [Global Settings](/docs/3.5/configuration/global-settings.md): Configure global ForesightJS settings that apply to the entire ForesightManager instance - [Development Tools](/docs/3.5/debugging/devtools.md): Documentation on how to use the ForesightJS debugger - [Static Properties](/docs/3.5/debugging/static-properties.md): Static properties exposed by the Foresight Manager - [Events](/docs/3.5/events.md): Documentation on how to use the built-in js.foresight events - [Initialize the Manager](/docs/3.5/getting-started/initialize-the-manager.md): Learn how to initialize the ForesightManager with custom global settings (optional step) - [TypeScript](/docs/3.5/getting-started/typescript.md): Typescript helpers for the ForesightJS library - [What is ForesightJS](/docs/3.5/getting-started/what-is-foresightjs.md): Introduction to ForesightJS, an lightweight JavaScript library with full TypeScript support that predicts user intent based on mouse movements and keyboard navigation - [Your First Element](/docs/3.5/getting-started/your-first-element.md): Learn how to register your first element with ForesightJS and start predicting user intent - [Hook](/docs/3.5/react/hook.md): React hook for ForesightJS integration - [Next.js](/docs/3.5/react/nextjs.md): Integration details to add ForesightJS to your Next.js projects - [React Router](/docs/3.5/react/react-router.md): Integration details to add ForesightJS to your React Router projects - [Composable](/docs/3.5/vue/composable.md): Vue composable for ForesightJS integration - [Directive](/docs/3.5/vue/directive.md): Vue directive for ForesightJS integration ### ai-context How to use ForesightJS's llms.txt file with AI tools and LLMs - [AI Context](/docs/ai-context.md): How to use ForesightJS's llms.txt file with AI tools and LLMs ### angular - [AI Context](/docs/angular/ai-context.md): How to use ForesightJS's llms.txt file with AI tools and LLMs - [Behind the Scenes](/docs/angular/behind-the-scenes.md): A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements, and how it predicts tab navigation. - [Global Settings](/docs/angular/configuration/global-settings.md): Configure global ForesightJS settings in an Angular app - [Registration Options](/docs/angular/configuration/registration-options.md): The options you pass to the fsForesight directive, ForesightService, and ForesightComponent - [Development Tools](/docs/angular/devtools.md): Use the ForesightJS development tools in an Angular app - [fsForesight](/docs/angular/directive.md): The fsForesight Angular directive from @foresightjs/angular - [Events](/docs/angular/events.md): Listen to the built-in ForesightJS events from Angular - [ForesightComponent](/docs/angular/foresight-component.md): Register a host element with the Foresight Angular component - [ForesightService](/docs/angular/foresight-service.md): Manually register elements and listen to ForesightManager events with ForesightService - [Initialize the Manager](/docs/angular/initialize-the-manager.md): Initialize the ForesightManager with custom global settings in an Angular app (optional step) - [injectForesightEvent](/docs/angular/injectForesightEvent.md): Listen to ForesightManager events from an Angular injection context - [Installation](/docs/angular/installation.md): Install the official Angular bindings for ForesightJS - [Migrating to v4](/docs/angular/migrating-to-v4.md): Moving from the v3 copy-paste Angular directive to the official @foresightjs/angular package - [Quick Start](/docs/angular/quick-start.md): Register your first element with ForesightJS in Angular using the fsForesight directive - [Static Properties](/docs/angular/static-properties.md): Static properties exposed by the Foresight Manager - [TypeScript](/docs/angular/typescript.md): Typescript helpers for ForesightJS in Angular - [What is ForesightJS](/docs/angular/what-is-foresightjs.md): Introduction to ForesightJS for Angular, a lightweight library that predicts user intent based on mouse movements and keyboard navigation ### astro - [AI Context](/docs/astro/ai-context.md): How to use ForesightJS's llms.txt file with AI tools and LLMs - [Behind the Scenes](/docs/astro/behind-the-scenes.md): A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements, and how it predicts tab navigation. - [Global Settings](/docs/astro/configuration/global-settings.md): Configure global ForesightJS settings in an Astro app - [Registration Options](/docs/astro/configuration/registration-options.md): How the core registration options map to the Astro integration - [Data Attributes](/docs/astro/data-attributes.md): Configure individual links with data-foresight-* attributes - [Development Tools](/docs/astro/devtools.md): Use the ForesightJS development tools in an Astro app - [Events](/docs/astro/events.md): Listen to the built-in ForesightJS events from Astro - [ForesightLink](/docs/astro/foresight-link.md): The ForesightLink Astro component from @foresightjs/astro - [Initialize the Manager](/docs/astro/initialize-the-manager.md): Configure the ForesightManager with custom global settings through the Astro integration (optional step) - [Installation](/docs/astro/installation.md): Install the official Astro integration for ForesightJS - [foresight()](/docs/astro/integration.md): All options of the foresight() Astro integration and how it composes with Astro's built-in prefetch - [Quick Start](/docs/astro/quick-start.md): Prefetch your first links with ForesightJS in Astro - [registerForesight](/docs/astro/register-foresight.md): Register elements with custom callbacks from Astro script tags using registerForesight - [Static Properties](/docs/astro/static-properties.md): Static properties exposed by the Foresight Manager - [TypeScript](/docs/astro/typescript.md): Typescript helpers for ForesightJS in Astro - [What is ForesightJS](/docs/astro/what-is-foresightjs.md): Introduction to ForesightJS for Astro, a lightweight library that predicts user intent based on mouse movements and keyboard navigation ### Behind_the_Scenes A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements using linear extrapolation and the Liang-Barsky algorithm, and how it predicts tab navigation. - [Behind the Scenes](/docs/Behind_the_Scenes.md): A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements using linear extrapolation and the Liang-Barsky algorithm, and how it predicts tab navigation. ### configuration - [Global Settings](/docs/configuration/global-settings.md): Configure global ForesightJS settings that apply to the entire ForesightManager instance - [Registration Options](/docs/configuration/registration-options.md): The options you pass when registering an element with ForesightManager ### debugging - [Development Tools](/docs/debugging/devtools.md): Documentation on how to use the ForesightJS debugger - [Static Properties](/docs/debugging/static-properties.md): Static properties exposed by the Foresight Manager ### events Documentation on how to use the built-in js.foresight events - [Events](/docs/events.md): Documentation on how to use the built-in js.foresight events ### getting-started - [Initialize the Manager](/docs/getting-started/initialize-the-manager.md): Learn how to initialize the ForesightManager with custom global settings (optional step) - [Quick Start](/docs/getting-started/quick-start.md): Learn how to register your first element with ForesightJS and start predicting user intent - [TypeScript](/docs/getting-started/typescript.md): Typescript helpers for the ForesightJS library - [What is ForesightJS](/docs/getting-started/what-is-foresightjs.md): Introduction to ForesightJS, an lightweight JavaScript library with full TypeScript support that predicts user intent based on mouse movements and keyboard navigation ### migrating-to-v4 Moving from the v3 copy-paste integrations to the official v4 React, Vue, and Angular packages - [Migrating to v4](/docs/migrating-to-v4.md): Moving from the v3 copy-paste integrations to the official v4 React, Vue, and Angular packages ### other-frameworks Use ForesightJS in frameworks without an official package and build reactive bindings with the subscribe pattern - [Other Frameworks](/docs/other-frameworks.md): Use ForesightJS in frameworks without an official package and build reactive bindings with the subscribe pattern ### react - [AI Context](/docs/react/ai-context.md): How to use ForesightJS's llms.txt file with AI tools and LLMs - [Behind the Scenes](/docs/react/behind-the-scenes.md): A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements using linear extrapolation and the Liang-Barsky algorithm, and how it predicts tab navigation. - [Global Settings](/docs/react/configuration/global-settings.md): Configure global ForesightJS settings in a React app - [Registration Options](/docs/react/configuration/registration-options.md): The options you pass to the useForesight React hook - [Development Tools](/docs/react/devtools.md): Use the ForesightJS development tools in a React app - [Events](/docs/react/events.md): Listen to the built-in ForesightJS events from React - [Foresight component](/docs/react/foresight-component.md): Register elements with the Foresight component, including dynamic lists - [Initialize the Manager](/docs/react/initialize-the-manager.md): Initialize the ForesightManager with custom global settings in a React app (optional step) - [Installation](/docs/react/installation.md): Install the official React bindings for ForesightJS - [Migrating to v4](/docs/react/migrating-to-v4.md): Moving from the v3 copy-paste React hook to the official @foresightjs/react package - [Next.js](/docs/react/nextjs.md): Integration details to add ForesightJS to your Next.js projects - [Quick Start](/docs/react/quick-start.md): Register your first element with ForesightJS in React using the useForesight hook - [React Router](/docs/react/react-router.md): Integration details to add ForesightJS to your React Router projects - [Static Properties](/docs/react/static-properties.md): Static properties exposed by the Foresight Manager - [TypeScript](/docs/react/typescript.md): Typescript helpers for ForesightJS in React - [useForesight](/docs/react/useForesight.md): The useForesight React hook from @foresightjs/react - [useForesightEvent](/docs/react/useForesightEvent.md): Listen to ForesightManager events from a React component - [What is ForesightJS](/docs/react/what-is-foresightjs.md): Introduction to ForesightJS for React, a lightweight library that predicts user intent based on mouse movements and keyboard navigation ### vue - [AI Context](/docs/vue/ai-context.md): How to use ForesightJS's llms.txt file with AI tools and LLMs - [Behind the Scenes](/docs/vue/behind-the-scenes.md): A technical deep-dive into the internal workings of ForesightJS, explaining its architecture, how it predicts mouse movements using linear extrapolation and the Liang-Barsky algorithm, and how it predicts tab navigation. - [Global Settings](/docs/vue/configuration/global-settings.md): Configure global ForesightJS settings in a Vue app - [Registration Options](/docs/vue/configuration/registration-options.md): The options you pass to the v-foresight directive and useForesight composable - [Development Tools](/docs/vue/devtools.md): Use the ForesightJS development tools in a Vue app - [v-foresight](/docs/vue/directive.md): The v-foresight Vue directive from @foresightjs/vue - [Events](/docs/vue/events.md): Listen to the built-in ForesightJS events from Vue - [Foresight component](/docs/vue/foresight-component.md): Register elements with the Foresight Vue component, including dynamic lists - [Initialize the Manager](/docs/vue/initialize-the-manager.md): Initialize the ForesightManager with custom global settings in a Vue app (optional step) - [Installation](/docs/vue/installation.md): Install the official Vue bindings for ForesightJS - [Migrating to v4](/docs/vue/migrating-to-v4.md): Moving from the v3 copy-paste Vue directive and composable to the official @foresightjs/vue package - [Quick Start](/docs/vue/quick-start.md): Register your first element with ForesightJS in Vue using the v-foresight directive or the useForesight composable - [Static Properties](/docs/vue/static-properties.md): Static properties exposed by the Foresight Manager - [TypeScript](/docs/vue/typescript.md): Typescript helpers for ForesightJS in Vue - [useForesight](/docs/vue/useForesight.md): The useForesight Vue composable from @foresightjs/vue - [useForesightEvent](/docs/vue/useForesightEvent.md): Listen to ForesightManager events from a Vue component - [What is ForesightJS](/docs/vue/what-is-foresightjs.md): Introduction to ForesightJS for Vue, a lightweight library that predicts user intent based on mouse movements and keyboard navigation --- # Full Documentation Content [Skip to main content](#__docusaurus_skipToContent_fallback) [![ForesightJS Logo](/img/logo.svg)![ForesightJS Logo](/img/logo.svg)](/index.md) [**ForesightJS**](/index.md)[Documentation](/docs/getting-started/what-is-foresightjs.md)[Playground](https://foresightjs.com/#playground) [4.x](/docs/getting-started/what-is-foresightjs.md) * [4.x](/docs/getting-started/what-is-foresightjs.md) * [3.5](/docs/3.5/getting-started/what-is-foresightjs.md) [GitHub](https://github.com/spaansba/ForesightJS) Search # Search the documentation Type your search here Powered by[](https://www.algolia.com/) Copyright © 2026 ForesightJS, Inc. Built with Docusaurus. --- # AI Context ForesightJS provides an `llms.txt` file following the [llms.txt specification](https://llmstxt.org/) to help LLMs understand and work with the library effectively. ## What is llms.txt?[​](#what-is-llmstxt "Direct link to What is llms.txt?") The `llms.txt` file is a standardized way to provide LLM-friendly information about a project. It contains: * A concise overview of what ForesightJS does * Links to key documentation sections in markdown format * Essential information for understanding the API and usage patterns ## Using ForesightJS's llms.txt[​](#using-foresightjss-llmstxt "Direct link to Using ForesightJS's llms.txt") ### Quick Access[​](#quick-access "Direct link to Quick Access") * **Main file:** [foresightjs.com/llms.txt](https://foresightjs.com/llms.txt) * **Full context:** [foresightjs.com/llms-full.txt](https://foresightjs.com/llms-full.txt) ### Example Prompt[​](#example-prompt "Direct link to Example Prompt") ``` Read the ForesightJS documentation from: https://foresightjs.com/llms.txt I need to implement ForesightJS predictive prefetching in my Next.js application. I want to: 1. Use the useForesight React hook for my custom link components 2. Integrate with Next.js router prefetching using ForesightLink 3. Use the default configuration, except that I want mobile prefetching to be on touch start ``` ## Individual Page Markdown[​](#individual-page-markdown "Direct link to Individual Page Markdown") All documentation pages are available as markdown by adding `.md` to any URL: * `https://foresightjs.com/docs/getting-started/your-first-element.md` * `https://foresightjs.com/docs/configuration/global-settings.md` * `https://foresightjs.com/docs/react/nextjs.md` --- # Angular Community Contribution Welcome The maintainer of ForesightJS doesn't use Angular, so this documentation is minimal. If you're using ForesightJS with Angular and would like to help improve these docs with more detailed examples and best practices, PRs are greatly appreciated! ``` npm install js.foresight ngx-foresight npm install -D js.foresight-devtools # or pnpm add js.foresight js.foresight-devtools ngx-foresight pnpm add -D js.foresight-devtools ``` After that import the `ForesightjsDirective` to the components with `href` and `routerLink`, and use the `ForesightjsStrategy` as `preloadingStrategy` in the router's configuration. For example: ``` import { ForesightManager } from "js.foresight" import { ForesightDevtools } from "js.foresight-devtools" import { ForesightjsDirective } from "ngx-foresight" ForesightManager.initialize({ enableMousePrediction: true, positionHistorySize: 8, trajectoryPredictionTime: 80, defaultHitSlop: 10, enableTabPrediction: true, tabOffset: 3, enableScrollPrediction: true, scrollMargin: 150, }) ForesightDevtools.initialize({ showDebugger: true, isControlPanelDefaultMinimized: true, // optional setting which allows you to minimize the control panel on default showNameTags: true, // optional setting which shows the name of the element sortElementList: "visibility", // optional setting for how the elements in the control panel are sorted }) ``` ```
Feature 1 Feature 2 Feature 3
``` ``` // configure preloading strategy as per routes ;(provideRouter(routes, withPreloading(ForesightjsStrategy)), // for older versions RouterModule.forRoot(routes, { preloadingStrategy: ForesightjsStrategy })) ``` --- # 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 require different prediction strategies due to the lack of continuous cursor movement. ForesightJS provides two strategies via the `touchDeviceStrategy` prop: **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, options)` adds element to internal Map 2. **Callback Execution**: When user intent is detected, element's callback function triggers 3. **Deactivation**: `isCallbackActive` set to `false` after callback execution 4. **Reactivation Wait**: Element remains deactivated for `reactivateAfter` duration (defaults to infinity) 5. **Reactivation**: `isCallbackActive` reset to `true` 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") ForesightJS is designed to be lightweight. The entire library with all features is only **\~32 KB** minified. But we go further with **code splitting** to minimize your initial bundle even more. Both handlers and predictors are lazy-loaded based on device type and enabled features: | 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**: * Touch devices load only **\~19 KB** (core + touch handler + touch predictor) * Desktop with mouse-only loads **\~32 KB** (core + desktop handler + mouse predictor) * Additional predictors (Tab, Scroll) load on-demand when enabled --- # Element Settings When registering elements with the `ForesightManager`, you can provide configuration specific to each element. This allows you to fine-tune prediction behavior on a per-element basis. ## Basic Element Registration[​](#basic-element-registration "Direct link to Basic Element Registration") ``` const myElement = document.getElementById("my-element") const { isTouchDevice, isLimitedConnection, isRegistered } = ForesightManager.instance.register({ element: myElement, // Required: The element to monitor callback: () => { // Required: Function that executes when interaction is predicted console.log("prefetching") }, hitSlop: 50, // slop around the element, making its hitbox bigger name: "My Foresight button!", // name visible in the debug tools meta: { route: "/about", }, // your custom meta data for analytics reactivateAfter: 5 * 60 * 1000, // time for the element to reactivate after the callback has been hit }) ``` ## Registration Parameters[​](#registration-parameters "Direct link to Registration Parameters") **TypeScript Type:** `ForesightRegisterOptions` or `ForesightRegisterOptionsWithoutElement` if you want to omit the `element` ### Required Parameters[​](#required-parameters "Direct link to Required Parameters") #### `element`[​](#element "Direct link to element") * **Type:** `element | NodeListOf` * **Required:** Yes * **Description:** The DOM element (or a `NodeList` of elements) to monitor for user interactions. When a `NodeList` is passed, all elements are registered with the same callback and settings. ``` // Single element const button = document.querySelector("#my-button") ForesightManager.instance.register({ element: button, callback: () => { /* prefetch logic */ }, }) // Multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.register({ element: navLinks, callback: elementData => { /* elementData identifies which element triggered */ }, }) ``` *** #### `callback`[​](#callback "Direct link to callback") * **Type:** `(elementData: ForesightElementData) => void` * **Required:** Yes * **Description:** Function that executes when interaction is predicted or occurs. This is where your prefetching logic goes. The callback receives the [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) of the element that triggered it, which is especially useful when registering multiple elements with a shared callback. * **Note:** If you await your prefetch logic the `callbackCompleted` [event](/docs/3.5/events.md#callbackcompleted) will show you how long your prefetch took to run. ``` ForesightManager.instance.register({ element: myElement, callback: elementData => { console.log(`Triggered ${elementData.element}`) // prefetch logic }, }) ``` *** ### Optional Parameters[​](#optional-parameters "Direct link to Optional Parameters") #### `hitSlop`[​](#hitslop "Direct link to hitslop") * **Type:** `number | Rect` * **Required:** No * **Default:** Uses `defaultHitSlop` from global settings (which is 0 if not set) * **Description:** Fully invisible "slop" around the element that increases the hover hitbox. ``` // Uniform hit slop (20px on all sides) ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: 20, }) // Custom hit slop for each side ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: { top: 10, left: 50, right: 50, bottom: 100, }, }) ``` *** #### `name`[​](#name "Direct link to name") * **Type:** `string` * **Required:** No * **Default:** `element.id` or `"unnamed"` if no id * **Description:** A descriptive name for the element, useful for development tools and debugging. ``` ForesightManager.instance.register({ element: myElement, callback: () => { /* logic */ }, name: "Product Navigation Button", // Helpful for debugging }) ``` *** #### `reactivateAfter`[​](#reactivateafter "Direct link to reactivateafter") * **Type:** `number` * **Required:** No * **Default:** `Infinity` * **Description:** Time in milliseconds after which the callback can be fired again. Set to `Infinity` to prevent callback from firing again after first execution. * **Note:** Even though `ForesightJS` doesn't store any data, you can set this to the same time as your data would become `stale` normally. In most cases you can just leave this on the default value, as it will get reset on page refresh/rerouting anyways. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, reactivateAfter: 5 * 60 * 1000, // 5 minutes }) ``` *** #### `meta`[​](#meta "Direct link to meta") * **Type:** `Record` * **Required:** No * **Default:** `{}` * **Description:** Stores additional information about the registered element. Visible in all element-related [events](/docs/3.5/events.md) and in the [devtools](/docs/3.5/debugging/devtools.md). ``` ForesightManager.instance.register({ element: productLink, callback: () => {}, meta: { route: "/about", section: "main", category: "content", }, }) ``` ## Registration Return Value[​](#registration-return-value "Direct link to Registration Return Value") The `register()` method returns useful information about the registration. When a single element is passed, it returns a `ForesightRegisterResult`. When a `NodeList` is passed, it returns `ForesightRegisterResult[]`. **TypeScript Type:** `ForesightRegisterResult` or `ForesightRegisterResult[]` ``` // Single element returns ForesightRegisterResult const { isTouchDevice, isLimitedConnection, isRegistered } = ForesightManager.instance.register({ element: myElement, callback: () => {}, }) // NodeList returns ForesightRegisterResult[] const results = ForesightManager.instance.register({ element: document.querySelectorAll(".my-elements"), callback: () => {}, }) ``` *** ### Return Properties[​](#return-properties "Direct link to Return Properties") #### `isTouchDevice`[​](#istouchdevice "Direct link to istouchdevice") * **Type:** `boolean` * **Description:** Indicates whether the current device is a touch device. Deprecated As of v3.3, ForesightJS handles touch devices internally with dedicated touch strategies. You no longer need to check this value for fallback prefetching logic. *** #### `isLimitedConnection`[​](#islimitedconnection "Direct link to islimitedconnection") * **Type:** `boolean` * **Description:** Is `true` when the user's connection is slower than the configured `minimumConnectionType` setting (defaults to "3g") or data saver is enabled. The element is still **registered**, but stays **inactive** (it is excluded from prediction and never fires its callback), so no data is consumed. See [Global Settings](/docs/3.5/configuration/global-settings.md#minimumconnectiontype) for details. * **Note:** This feature relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS treats the connection as unlimited. *** #### `isRegistered`[​](#isregistered "Direct link to isregistered") * **Type:** `boolean` * **Description:** Is `true` once the element is registered with the manager, independent of connection. It only becomes `false` after the element is unregistered manually. Detaching the element from the DOM does **not** unregister it. It is parked (kept registered but inactive) and resumes when it reattaches, so it survives detach/reattach and re-parenting. On a limited connection the element is likewise registered but inactive, so use [`isLimitedConnection`](#islimitedconnection) / `isActive` to detect inactivity, not `isRegistered`. --- # Global Settings Global settings are specified when initializing the `ForesightManager` and apply to all registered elements. This should be done once at your application's entry point. tip If you want the default global options you don't need to initialize the ForesightManager explicitly. ## Basic Configuration[​](#basic-configuration "Direct link to Basic Configuration") ``` import { ForesightManager } from "js.foresight" // Initialize the manager once at the top of your app if you want custom global settings // ALL SETTINGS ARE OPTIONAL ForesightManager.initialize({ enableMousePrediction: true, positionHistorySize: 8, trajectoryPredictionTime: 80, defaultHitSlop: 10, enableTabPrediction: true, tabOffset: 3, enableScrollPrediction: true, scrollMargin: 150, touchDeviceStrategy: "viewport", enableManagerLogging: false, minimumConnectionType: "3g", }) ``` ## Available Settings[​](#available-settings "Direct link to Available Settings") **TypeScript Type:** `ForesightManagerSettings` note All numeric settings are clamped to their specified Min/Max values to prevent invalid configurations. ### Mouse Prediction Settings[​](#mouse-prediction-settings "Direct link to Mouse Prediction Settings") #### `enableMousePrediction`[​](#enablemouseprediction "Direct link to enablemouseprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether trajectory prediction is active. If `false`, only direct hovers will trigger the callback for mouse users. * **Note:** When this is turned off `ForesightJS` will still trigger `callbacks` while hovering over the element's extended range (`rect` + `hitslop`) ``` ForesightManager.initialize({ enableMousePrediction: false, // Disable trajectory prediction }) ``` *** #### `positionHistorySize`[​](#positionhistorysize "Direct link to positionhistorysize") * **Type:** `number` * **Default:** `8` * **Clamped between:** `2-30` * **Description:** Number of mouse positions to keep in history for velocity calculations. Higher values provide smoother predictions but use more memory. ``` ForesightManager.initialize({ positionHistorySize: 12, // Keep more position history for smoother predictions }) ``` *** #### `trajectoryPredictionTime`[​](#trajectorypredictiontime "Direct link to trajectorypredictiontime") * **Type:** `number` * **Default:** `120` * **Clamped between:** `10-200` milliseconds * **Description:** How far ahead (in milliseconds) to predict the mouse trajectory. Larger values trigger callbacks sooner but may reduce accuracy. ``` ForesightManager.initialize({ trajectoryPredictionTime: 100, // Predict 100ms into the future }) ``` *** ### Keyboard Prediction Settings[​](#keyboard-prediction-settings "Direct link to Keyboard Prediction Settings") #### `enableTabPrediction`[​](#enabletabprediction "Direct link to enabletabprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether keyboard prediction is active for tab navigation. ``` ForesightManager.initialize({ enableTabPrediction: false, // Disable keyboard prediction }) ``` *** #### `tabOffset`[​](#taboffset "Direct link to taboffset") * **Type:** `number` * **Default:** `2` * **Clamped between:** `0-20` * **Description:** Number of tab stops away from an element to trigger its callback. Only works when `enableTabPrediction` is `true`. ``` ForesightManager.initialize({ tabOffset: 1, // Trigger callback when 1 tab stop away }) ``` *** ### Scroll Prediction Settings[​](#scroll-prediction-settings "Direct link to Scroll Prediction Settings") #### `enableScrollPrediction`[​](#enablescrollprediction "Direct link to enablescrollprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether scroll prediction is active. ``` ForesightManager.initialize({ enableScrollPrediction: false, // Disable scroll prediction }) ``` *** #### `scrollMargin`[​](#scrollmargin "Direct link to scrollmargin") * **Type:** `number` * **Default:** `150` * **Clamped between:** `30-300` pixels * **Description:** Sets the pixel distance to check from the mouse position in the scroll direction for triggering callbacks. ``` ForesightManager.initialize({ scrollMargin: 200, // Check 200px ahead in scroll direction }) ``` *** ### Hit Slop Settings[​](#hit-slop-settings "Direct link to Hit Slop Settings") #### `defaultHitSlop`[​](#defaulthitslop "Direct link to defaulthitslop") * **Type:** `number | Rect` * **Default:** `{top: 0, left: 0, right: 0, bottom: 0}` * **Clamped between:** `0-2000` pixels (per side) * **Description:** Default fully invisible "slop" around elements for all registered elements. Basically increases the hover hitbox. * **Note:** This can be overwritten on an element basis by setting its `hitslop`. ``` // Uniform hit slop ForesightManager.initialize({ defaultHitSlop: 20, // 20px on all sides }) // Custom hit slop for each side ForesightManager.initialize({ defaultHitSlop: { top: 10, left: 20, right: 20, bottom: 15, }, }) ``` *** ### Touch Device Settings (v3.3.0+)[​](#touch-device-settings-v330 "Direct link to Touch Device Settings (v3.3.0+)") #### `touchDeviceStrategy`[​](#touchdevicestrategy "Direct link to touchdevicestrategy") * **Type:** `TouchDeviceStrategy` * **Default:** `"onTouchStart"` * **Options:** `"none"`, `"viewport"`, `"onTouchStart"` * **Description:** Strategy to use for touch devices (mobile / pen users). ``` ForesightManager.initialize({ touchDeviceStrategy: "viewport", }) ``` *** ### Minimum Connection Settings[​](#minimum-connection-settings "Direct link to Minimum Connection Settings") #### `minimumConnectionType`[​](#minimumconnectiontype "Direct link to minimumconnectiontype") * **Type:** `MinimumConnectionType` * **Default:** `"3g"` * **Options:** `"slow-2g"`, `"2g"`, `"3g"`, `"4g"` * **Description:** The minimum connection speed required to register elements. Elements will not be registered when the user's connection is slower than this threshold. * **Note:** This feature relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS will register elements regardless of connection speed. ``` ForesightManager.initialize({ minimumConnectionType: "3g", }) ``` *** ### Other Settings[​](#other-settings "Direct link to Other Settings") #### `enableManagerLogging`[​](#enablemanagerlogging "Direct link to enablemanagerlogging") * **Type:** `boolean` * **Default:** `false` * **Description:** Logs basic information about the `ForesightManager` and its handlers that is not available through [events](/docs/3.5/events.md). Mostly used by the maintainers of `ForesightJS` to debug the manager, but might be useful for implementers aswell. * **Note:** Examples of logs are: Initializing the manager, switching from device strategy (e.g. Mouse to pen), aborting controllers and invalidating cache. ``` ForesightManager.initialize({ enableManagerLogging: true, }) ``` ## Runtime Configuration Changes[​](#runtime-configuration-changes "Direct link to Runtime Configuration Changes") You can update global settings at runtime using `alterGlobalSettings()`. This is not adviced for regular use and is mainly used for developers creating tools on top of `Foresight`. For regular use, you should set global settings during initialization with `ForesightManager.initialize()`. ``` // Change settings after initialization ForesightManager.instance.alterGlobalSettings({ trajectoryPredictionTime: 100, enableScrollPrediction: false, }) ``` This is particularly useful when integrating with development tools or when you need to adjust settings based on user preferences or device capabilities. ## Development Tools Integration[​](#development-tools-integration "Direct link to Development Tools Integration") Development Tools Visual development tools are available as a separate package. See the [development tools documentation](/docs/3.5/debugging/devtools.md) for details on installing and configuring the `js.foresight-devtools` package. The development tools provide a real-time interface for adjusting these global settings and seeing their immediate effects on prediction behavior. --- # Development Tools [![npm version](https://img.shields.io/npm/v/js.foresight-devtools.svg)](https://www.npmjs.com/package/js.foresight-devtools) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) `ForesightJS` offers dedicated [Development Tools](https://github.com/spaansba/ForesightJS/tree/main/packages/js.foresight-devtools), written in [Lit](https://lit.dev/), to help you better understand and fine-tune how `ForesightJS` works within your application. You can see the development tools in action on the [playground page](https://foresightjs.com/#playground), which includes visual trajectory indicators, element boundaries, and a control panel in the bottom-right corner. These tools are built entirely using `ForesightJS`'s [built-in events](/docs/3.5/events.md), demonstrating how you can create your own monitoring and debugging tools using the same event system. ## Installation[​](#installation "Direct link to Installation") To install the `ForesightJS` Development Tools package, use your preferred package manager: ``` pnpm add -D js.foresight-devtools # or npm install -D js.foresight-devtools # or yarn add -D js.foresight-devtools ``` ## Enabling Development Tools[​](#enabling-development-tools "Direct link to Enabling Development Tools") ``` import { ForesightManager } from "js.foresight" import { ForesightDevtools } from "js.foresight-devtools" // Initialize ForesightJS ForesightManager.initialize({}) // Initialize the development tools (all options are optional) ForesightDevtools.initialize({ showDebugger: true, isControlPanelDefaultMinimized: false, // optional setting which allows you to minimize the control panel on default showNameTags: true, // optional setting which shows the name of the element sortElementList: "visibility", // optional setting for how the elements in the control panel are sorted logging: { logLocation: "controlPanel", // Where to log the Foresight Events callbackCompleted: true, callbackInvoked: true, elementRegistered: false, elementUnregistered: false, managerSettingsChanged: true, mouseTrajectoryUpdate: false, // dont log this to the devtools scrollTrajectoryUpdate: false, // dont log this to the devtools deviceStrategyChanged: true, }, }) ``` ## Development Tools Features[​](#development-tools-features "Direct link to Development Tools Features") Once enabled, the `ForesightJS` Development Tools add several visual layers to your application, including mouse and scroll trajectories and element hitboxes. A control panel also appears in the bottom-right corner of the screen. ### Control Panel[​](#control-panel "Direct link to Control Panel") The control panel provides three main tabs for debugging and configuration. Each tab serves a specific purpose in understanding and tuning ForesightJS behavior. #### Settings Tab[​](#settings-tab "Direct link to Settings Tab") The Settings tab provides real-time controls for all [Global Configurations](/docs/3.5/configuration/global-settings.md). Changes made through these controls immediately affect the `ForesightManager` configuration, allowing you to see how different settings impact your app without fiddling in your code. #### Elements Tab[​](#elements-tab "Direct link to Elements Tab") The Elements tab displays a overview of all currently registered elements within the `ForesightManage`r, grouped into **Active** and **Inactive** sections. Each element entry shows its current status through a color-coded indicator: * 🟢 **Green** - Active visible elements in desktop mode * ⚫ **Grey** - Active invisible elements in desktop mode * 🟣 **Purple** - Active elements while in touch device mode (all elements, we dont track visibility in this mode) * 🟡 **Yellow** - Elements which callbacks are currently executing * 🔘 **Light Gray** - Inactive elements Inactive elements also carry one or more **reason badges** explaining *why* they are inactive (an element can have several at once, e.g. `disabled` while `detached`): * `disabled` - turned off via the [`enabled`](/docs/3.5/configuration/element-settings.md) option * `detached` - removed from the DOM; the element is [parked](/docs/3.5/configuration/element-settings.md#isregistered) (registered but inactive) and resumes when it reconnects * `limited` - on a [limited connection](/docs/3.5/configuration/element-settings.md#islimitedconnection) (data saver / slow network) * `fired` - already ran its callback and is waiting on `reactivateAfter` (with the default `Infinity` it stays fired) Each element can also be expanded to reveal its [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) information including settings, callback status, and metadata. For `fired` elements with a finite `reactivateAfter`, a countdown timer appears for their reactivation cooldown period; clicking this timer will instantly reactivate the element. #### Log Tab[​](#log-tab "Direct link to Log Tab") The Log tab displays real-time [events](/docs/3.5/events.md) emitted by `ForesightJS`. You can see callback execution times, the full element's lifecycle and other system events. Events can be filtered through the devtools initialization configuration or in the control panel itself. You can also print out the complete [`ForesightManager.instance.getManagerData`](/docs/3.5/debugging/static-properties.md#foresightmanagerinstancegetmanagerdata) state without having to call it from your code. caution Avoid logging frequently emitted events to the browser console, as it can noticeably slow down your development environment. Use the control panel for this instead. note Element overlay visualization and visibility sorting in the control panel only work with desktop/mouse prediction strategies. When debugging `touchDeviceStrategy` configurations, these features are not available as touch strategies don't track the same positioning data. --- # Static Properties The ForesightManager exposes several static properties for accessing and checking the manager state. ***All properties are read-only*** ## ForesightManager.instance[​](#foresightmanagerinstance "Direct link to ForesightManager.instance") Gets the singleton instance of ForesightManager, initializing it if necessary. This is the primary way to access the manager throughout your application. **Returns:** `ForesightManager` **Example:** ``` const manager = ForesightManager.instance // Register a single element manager.register({ element: myButton, callback: () => console.log("Predicted interaction!"), }) // Register multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") manager.register({ element: navLinks, callback: elementData => console.log(`Predicted: ${elementData.name}`), }) ``` ## ForesightManager.instance.registeredElements[​](#foresightmanagerinstanceregisteredelements "Direct link to ForesightManager.instance.registeredElements") Gets a Map of all currently registered elements and their associated data. This is useful for debugging or inspecting the current state of registered elements. **Returns:** `ReadonlyMap` ## ForesightManager.instance.isInitiated[​](#foresightmanagerinstanceisinitiated "Direct link to ForesightManager.instance.isInitiated") Checks whether the ForesightManager has been initialized. **Returns:** `Readonly` ## ForesightManager.instance.reactivate(element)[​](#foresightmanagerinstancereactivateelement "Direct link to ForesightManager.instance.reactivate(element)") Manually reactivates an element (or all elements in a `NodeList`), allowing its callback to be triggered again. This is useful when you want to re-enable prefetching for an element before its `reactivateAfter` timeout expires. **Parameters:** * `element` - The DOM element or `NodeList` to reactivate **Example:** ``` // Single element const myButton = document.getElementById("my-button") ForesightManager.instance.reactivate(myButton) // Multiple elements const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.reactivate(navLinks) ``` ## ForesightManager.instance.unregister(element, reason?)[​](#foresightmanagerinstanceunregisterelement-reason "Direct link to ForesightManager.instance.unregister(element, reason?)") Removes an element (or all elements in a `NodeList`) from ForesightManager's tracking. Frameworks handle this for you The official React and Vue integrations call `unregister` automatically when a component unmounts, so you rarely call it yourself there. Detaching an element from the DOM does **not** unregister it. It is parked (kept registered but inactive) and resumes when it reattaches, so it survives detach/reattach and re-parenting. Unregistering only happens when you call this method explicitly. If you register elements yourself (vanilla JS) and permanently remove one from the DOM, call `unregister` to stop tracking it. Otherwise it stays parked and keeps a reference to the detached element, which prevents it from being garbage collected. **Parameters:** * `element` - The DOM element or `NodeList` to unregister * `reason` (optional) - A string describing why the element was unregistered (useful for debugging via events) **Example:** ``` // Single element const myButton = document.getElementById("my-button") ForesightManager.instance.unregister(myButton, "user-navigation") // Multiple elements const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.unregister(navLinks) ``` ## ForesightManager.instance.getManagerData[​](#foresightmanagerinstancegetmanagerdata "Direct link to ForesightManager.instance.getManagerData") Snapshot of the current `ForesightManager` state, including all [global settings](/docs/3.5/configuration/global-settings.md), registered elements, position observer data, and interaction statistics. This is primarily used for debugging, monitoring, and development purposes. **Properties:** * `registeredElements` - `Map` of all currently registered elements and their associated data * `eventListeners` - `Map` of all event listeners listening to [ForesightManager Events](/docs/3.5/events.md). * `globalSettings` - Current [global configuration](/docs/3.5/configuration/global-settings.md) settings * `globalCallbackHits` - Total `callback` execution counts by interaction type (mouse/tab/scroll/viewport/touch) and by subtype (hover/trajctory for mouse, forwards/reverse for tab, direction for scroll) * `currentDeviceStrategy` - Which strategy is being used. Can be either `touch` or `mouse`, this changes dynamically * `activeElementCount` - Amount of elements currently active (not the same as registered) * `loadedModules` - Shows which handlers and predictors have been [lazy loaded](/docs/3.5/Behind_the_Scenes.md#bundle-optimization) **Returns:** `Readonly` The return will look something like this: ``` { "registeredElements": { "size": 7, "entries": "" }, "activeElementCount": 5, "currentDeviceStrategy": "mouse", "eventListeners": { "0": { "elementRegistered": [] }, "1": { "elementUnregistered": [] }, "2": { "mouseTrajectoryUpdate": [] }, "3": { "scrollTrajectoryUpdate": [] }, "4": { "managerSettingsChanged": [] }, "5": { "callbackInvoked": [] }, "6": { "callbackCompleted": [] } }, "globalSettings": { "defaultHitSlop": { "bottom": 10, "left": 10, "right": 10, "top": 10 }, "enableMousePrediction": true, "enableScrollPrediction": true, "enableTabPrediction": true, "positionHistorySize": 10, "scrollMargin": 150, "tabOffset": 2, "trajectoryPredictionTime": 100 }, "globalCallbackHits": { "mouse": { "hover": 0, "trajectory": 3 }, "scroll": { "down": 2, "left": 0, "right": 0, "up": 0 }, "tab": { "forwards": 3, "reverse": 0 }, "touch": 0, "viewport": 0, "total": 8 }, "loadedModules": { "desktopHandler": true, "touchHandler": false, "predictors": { "mouse": true, "tab": true, "scroll": true, "viewport": false, "touchStart": false } } } ``` --- # Events ForesightManager emits various events during to provide insight into element registration, prediction activities, and callback executions. These events are primarily used by the [ForesightJS DevTools](/docs/3.5/debugging/devtools.md) for visual debugging and monitoring, but can also be leveraged for telemetry, analytics, and performance monitoring in your applications. ## Usage[​](#usage "Direct link to Usage") All events are visible in the logs tab of the [devtools](/docs/3.5/debugging/devtools.md). However for tracking/analytics in production, implementing them in your own code is straightforward with the standard `addEventListener` pattern. ``` import { ForesightManager } from "js.foresight" // Define handler as const for removal const handleCallbackInvoked = event => { console.log( `Callback executed for ${event.elementData.name} in ${event.hitType.kind} mode, which took ${event.elapsed} ms` ) } // Add the event ForesightManager.instance.addEventListener("callbackInvoked", handleCallbackInvoked) // Later, remove the listener using the same reference ForesightManager.instance.removeEventListener("callbackInvoked", handleCallbackInvoked) ``` ### AbortController support[​](#abortcontroller-support "Direct link to AbortController support") Event listeners support [AbortController signals](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) for easy cleanup. ``` const controller = new AbortController() manager.addEventListener("callbackInvoked", handleCallbackInvoked, { signal: controller.signal }) controller.abort() ``` ## Available Events[​](#available-events "Direct link to Available Events") ### Interaction Events[​](#interaction-events "Direct link to Interaction Events") Events fired when user interactions trigger callbacks. *** #### `callbackInvoked`[​](#callbackinvoked "Direct link to callbackinvoked") Fired **before** an element's callback is executed ``` type CallbackInvokedEvent = { type: "callbackInvoked" timestamp: number elementData: ForesightElementData hitType: CallbackHitType } ``` **Related Types:** [`CallbackHitType`](/docs/3.5/getting-started/typescript.md#callbackhittype) • [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) *** #### `callbackCompleted`[​](#callbackcompleted "Direct link to callbackcompleted") Fired **after** an element's callback is executed ``` type CallbackCompletedEvent = { type: "callbackCompleted" timestamp: number elementData: ForesightElementData hitType: CallbackHitType elapsed: number // Time between callbackInvoked and callbackCompleted status: "success" | "error" | undefined errorMessage: string | undefined | null wasLastActiveElement: boolean } ``` **Related Types:** [`CallbackHitType`](/docs/3.5/getting-started/typescript.md#callbackhittype) • [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) ### Element Lifecycle Events[​](#element-lifecycle-events "Direct link to Element Lifecycle Events") Events fired during element registration, updates, and cleanup. *** #### `elementRegistered`[​](#elementregistered "Direct link to elementregistered") Fired when an element is successfully registered with `ForesightManager` using `ForesightManager.instance.register(element)`. ``` type ElementRegisteredEvent = { type: "elementRegistered" timestamp: number elementData: ForesightElementData } ``` **Related Types:** [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) *** #### `elementUnregistered`[​](#elementunregistered "Direct link to elementunregistered") Fired when an element is removed from `ForesightManager`'s tracking via `ForesightManager.instance.unregister(element)`. Detaching an element from the DOM no longer unregisters it. It is parked (kept registered but inactive) and resumed on reattach, so no `elementUnregistered` event fires for that. The `"disconnected"` reason is therefore no longer emitted. ``` type ElementUnregisteredEvent = { type: "elementUnregistered" timestamp: number elementData: ForesightElementData unregisterReason: "disconnected" | "apiCall" | "devtools" | (string & {}) wasLastRegisteredElement: boolean } ``` **Related Types:** [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) *** ### Prediction Events[​](#prediction-events "Direct link to Prediction Events") Events fired during movement prediction calculations. *** #### `mouseTrajectoryUpdate`[​](#mousetrajectoryupdate "Direct link to mousetrajectoryupdate") Fired during mouse movement for trajectory calculations ``` type MouseTrajectoryUpdateEvent = { type: "mouseTrajectoryUpdate" trajectoryPositions: { currentPoint: { x: number; y: number } predictedPoint: { x: number; y: number } } predictionEnabled: boolean } ``` *** #### `scrollTrajectoryUpdate`[​](#scrolltrajectoryupdate "Direct link to scrolltrajectoryupdate") Fired during scroll events when scroll prediction is active ``` type ScrollTrajectoryUpdateEvent = { type: "scrollTrajectoryUpdate" currentPoint: Point // { x: number; y: number } predictedPoint: Point // { x: number; y: number } scrollDirection: ScrollDirection // "down" | "up" | "left" | "right" } ``` *** ### Configuration Events[​](#configuration-events "Direct link to Configuration Events") Events fired when ForesightManager configuration changes. *** #### `managerSettingsChanged`[​](#managersettingschanged "Direct link to managersettingschanged") Fired when [global](/docs/3.5/configuration/global-settings.md) `ForesightManager` settings are updated via the [devtools](/docs/3.5/debugging/devtools.md) or via `ForesightManager.instance.alterGlobalSettings()`. ``` type ManagerSettingsChangedEvent = { type: "managerSettingsChanged" timestamp: number managerData: Readonly updatedSettings: UpdatedManagerSetting[] } ``` #### Extra Type Info[​](#extra-type-info "Direct link to Extra Type Info") [`ForesightElementData`](/docs/3.5/getting-started/typescript.md#foresightelementdata) *** #### `deviceStrategyChanged`[​](#devicestrategychanged "Direct link to devicestrategychanged") Fired when user switches between input methods (mouse, touch, or pen). ``` type DeviceStrategyChangedEvent = { type: "deviceStrategyChanged" timestamp: number newStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" oldStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" } ``` *** --- # Initialize the Manager Optional Step This step is **not needed** if you don't want to configure ForesightJS. The manager will initialize automatically with default settings when you register your first element. If you don't want to configure anything, skip to [Your First Element](/docs/3.5/getting-started/your-first-element.md). If you want to customize ForesightJS's global behavior, you can initialize the manager with your preferred settings at your application's entry point. ## Optional Settings[​](#optional-settings "Direct link to Optional Settings") You can configure any of these [global settings](/docs/3.5/configuration/global-settings.md). ``` ForesightManager.initialize({ // Mouse prediction settings enableMousePrediction: true, // Enable/disable mouse trajectory prediction trajectoryPredictionTime: 120, // How far ahead to predict (10-200ms) positionHistorySize: 8, // Mouse positions to track (2-30) // Keyboard prediction settings enableTabPrediction: true, // Enable/disable keyboard navigation prediction tabOffset: 2, // Tab stops ahead to trigger (0-20) // Scroll prediction settings enableScrollPrediction: true, // Enable/disable scroll prediction scrollMargin: 150, // Pixel distance for scroll detection (30-300) // Touch device settings touchDeviceStrategy: "viewport", // "none", "viewport", or "onTouchStart" // Connection settings minimumConnectionType: "3g", // "slow-2g", "2g", "3g", "4g" // Default element settings defaultHitSlop: 0, // Default hit slop for all elements (number or {top, right, bottom, left}) }) ``` ## Good to know[​](#good-to-know "Direct link to Good to know") The `ForesightManager` is a singleton, meaning it can only be initialized once. If you call `ForesightManager.initialize()` multiple times, only the first call will take effect. Any subsequent calls will simply return the already initialized instance. The manager also provides several static methods you can use to get information about which elements are registered, how many callbacks have been ran and all the listening [event](/docs/3.5/events.md) listeners. You can find a list of these methods [here](/docs/3.5/debugging/static-properties.md). --- # TypeScript ForesightJS is fully written in `TypeScript` to make sure your development experience is as good as possible. ## Most Used Internal types[​](#most-used-internal-types "Direct link to Most Used Internal types") ### ForesightElementData[​](#foresightelementdata "Direct link to ForesightElementData") This is the main type used in ForesightJS as it gathers all information about a registered element. `ForesightElementData` is returned in most [events](/docs/3.5/events.md) as `elementData` ``` type ForesightElementData = { callback: ForesightCallback // (elementData: ForesightElementData) => void name: string id: string // only for internals elementBounds: { originalRect: DOMRectReadOnly hitSlop: Exclude expandedRect: Readonly // originalRect + hitSlop } trajectoryHitData: TrajectoryHitData // only for internals isIntersectingWithViewport: boolean element: ForesightElement registerCount: number //Amount of times this element has been (re)registered. meta: Record callbackInfo: { callbackFiredCount: number lastCallbackInvokedAt: number | undefined lastCallbackCompletedAt: number | undefined lastCallbackRuntime: number | undefined lastCallbackStatus: callbackStatus lastCallbackErrorMessage: string | undefined | null reactivateAfter: number isCallbackActive: boolean isRunningCallback: boolean reactivateTimeoutId?: ReturnType } } ``` ### CallbackHitType[​](#callbackhittype "Direct link to CallbackHitType") ``` type CallbackHitType = | { kind: "mouse"; subType: "hover" | "trajectory" } | { kind: "tab"; subType: "forwards" | "reverse" } | { kind: "scroll"; subType: "up" | "down" | "left" | "right" } | { kind: "touch"; subType?: string } | { kind: "viewport"; subType?: string } ``` ### CurrentDeviceStrategy[​](#currentdevicestrategy "Direct link to CurrentDeviceStrategy") Represents the current input method being used. ForesightJS automatically detects and switches between strategies. ``` type CurrentDeviceStrategy = "mouse" | "touch" | "pen" ``` ### ForesightManagerData[​](#foresightmanagerdata "Direct link to ForesightManagerData") Snapshot of the current ForesightManager state, including all [global settings](/docs/3.5/configuration/global-settings.md), registered elements, position observer data, and interaction statistics. This is primarily used for debugging, monitoring, and development purposes. This data is returned in the [`managerSettingsChanged`](/docs/3.5/events.md) event or by calling [`ForesightManager.instance.getManagerData`](/docs/3.5/debugging/static-properties.md#foresightmanagerinstancegetmanagerdata) manually. ``` type ForesightManagerData = { registeredElements: ReadonlyMap // See above for ForesightElementData globalSettings: Readonly // See configuration for global settings globalCallbackHits: Readonly // See above for CallbackHitType, this is that but all of them combined eventListeners: ReadonlyMap // All event listeners currently listening } ``` ## Helper Types[​](#helper-types "Direct link to Helper Types") ### ForesightCallback[​](#foresightcallback "Direct link to ForesightCallback") The callback function type used when registering elements. Receives the `ForesightElementData` of the element that triggered the callback. ``` type ForesightCallback = (elementData: ForesightElementData) => void ``` This is useful when registering multiple elements with a shared callback, as you can identify which element triggered it: ``` import type { ForesightCallback } from "js.foresight" const myCallback: ForesightCallback = elementData => { console.log(`Triggered by: ${elementData.name}`) } ``` ### ForesightRegisterOptionsWithoutElement[​](#foresightregisteroptionswithoutelement "Direct link to ForesightRegisterOptionsWithoutElement") Useful for if you want to create a custom button component in a modern framework (for example React). And you want to have the `ForesightRegisterOptions` used in `ForesightManager.instance.register({})` without the element as the element will be the ref of the component. This type is used in the [`useForesight`](/docs/3.5/react/hook.md) hook for React. ``` type ForesightButtonProps = { registerOptions: ForesightRegisterOptionsWithoutElement } ``` --- # What is ForesightJS [![npm version](https://img.shields.io/npm/v/js.foresight.svg)](https://www.npmjs.com/package/js.foresight) [![npm downloads](https://img.shields.io/npm/dt/js.foresight.svg)](https://www.npmjs.com/package/js.foresight) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/js.foresight)](https://bundlephobia.com/package/js.foresight) [![GitHub last commit](https://img.shields.io/github/last-commit/spaansba/ForesightJS)](https://github.com/spaansba/ForesightJS/commits) [![GitHub stars](https://img.shields.io/github/stars/spaansba/ForesightJS.svg?style=social\&label=Star)](https://github.com/spaansba/ForesightJS) [![Best of JS](https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=spaansba%2FForesightJS%26since=daily)](https://bestofjs.org/projects/foresightjs) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-live-blue)](https://foresightjs.com#playground) ForesightJS is a lightweight JavaScript library that predicts user intent to prefetch content before it's needed. It works completely out of the box without configuration, supporting both desktop and mobile devices with different prediction strategies. ## Understanding ForesightJS's Role[​](#understanding-foresightjss-role "Direct link to Understanding ForesightJS's Role") When you over simplify prefetching it exists of three parts: * **What** resource or data to load * **How** the loading method and caching strategy is * **When** the optimal moment to start fetching is ForesightJS takes care of the **When** by predicting user intent with mouse trajectory and tab navigation. You supply the **What** and **How** inside your `callback` when you register an element. ## Download[​](#download "Direct link to Download") ``` pnpm add js.foresight # or npm install js.foresight # or yarn add js.foresight ``` ## Prediction Strategies[​](#prediction-strategies "Direct link to Prediction Strategies") ForesightJS uses different prediction strategies depending on the device type. For limited connections (slower than the minimum connection type - default: 3G - or [data-saver mode](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData)), we respect the user's preference to minimize data usage: the element is still registered but stays inactive, so it is never predicted and never prefetches. Foresight automatically detects mouse, pen, or touch input, and updates its strategy if the input method changes. ### Keyboard/Mouse Users[​](#keyboardmouse-users "Direct link to Keyboard/Mouse Users") Pick and choose multiple prediction strategies. All are enabled by default, but you can disable any of them when [initializing](/docs/3.5/getting-started/initialize-the-manager.md) the `ForesightManager`. * **Mouse Trajectory** default - Analyzes cursor movement patterns to predict which links users are heading towards and prefetches content before they arrive * **Keyboard Navigation** default - Tracks tab key usage to prefetch when the user is N tab stops away from your registered element * **Scroll** default - Prefetches content when users scroll towards registered elements, predicting which elements will be reached based on scroll direction ### Touch Devices (v3.3.0+)[​](#touch-devices-v330 "Direct link to Touch Devices (v3.3.0+)") While desktop systems use multiple concurrent prediction strategies, touch devices operate with a single active strategy that can be configured while [initializing](/docs/3.5/getting-started/initialize-the-manager.md) the `ForesightManager`. * **onTouchStart** default - Captures the initial touch event to begin prefetching when users start interacting with registered elements * **Viewport Enter** - Detects when registered elements enter the viewport and prefetches their content based on scroll behavior and visibility * **None** - Disables ForesightJS on touch devices (previous behavior) ## Why ForesightJS?[​](#why-foresightjs "Direct link to Why ForesightJS?") ForesightJS is designed for developers who want to squeeze every drop of performance out of their web applications. It's a specialized tool, not something to drop into every project. ### Which problems does ForesightJS solve?[​](#which-problems-does-foresightjs-solve "Direct link to Which problems does ForesightJS solve?") #### Problem 1: On-Hover Prefetching Still Has Latency[​](#problem-1-on-hover-prefetching-still-has-latency "Direct link to Problem 1: On-Hover Prefetching Still Has Latency") Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires-time that could be used for prefetching. #### Problem 2: Viewport-Based Prefetching is Wasteful[​](#problem-2-viewport-based-prefetching-is-wasteful "Direct link to Problem 2: Viewport-Based Prefetching is Wasteful") Many modern frameworks (like Next.js) automatically prefetch resources for all links that enter the viewport. While well-intentioned, this creates significant overhead since users typically interact with only a small fraction of visible elements. Simply scrolling up and down the Next.js homepage can trigger ***1.59MB*** of unnecessary prefetch requests. #### Problem 3: Hover-Based Prefetching Excludes Keyboard Users[​](#problem-3-hover-based-prefetching-excludes-keyboard-users "Direct link to Problem 3: Hover-Based Prefetching Excludes Keyboard Users") Many routers rely on hover-based prefetching, but this approach completely excludes keyboard users since keyboard navigation never triggers hover events. This means keyboard users miss out on the performance benefits that mouse users get from hover-based prefetching. ## Lightweight & Optimized[​](#lightweight--optimized "Direct link to Lightweight & Optimized") ForesightJS is built to be tiny. The entire library with all features is only **\~32 KB** minified. On top of that, we use code splitting so you only load what you use: touch devices load just **\~19 KB**, and desktop users load **\~32 KB** with predictors lazy-loaded when enabled. See [Behind the Scenes](/docs/3.5/Behind_the_Scenes.md#bundle-optimization) for details. --- # Your First Element This guide will walk you through registering your first element with `ForesightJS` and understanding how the prediction system works. ## Basic Usage Example[​](#basic-usage-example "Direct link to Basic Usage Example") This basic example is in vanilla JS, ofcourse most people will use ForesightJS with a framework. You can read about framework integrations below. ``` import { ForesightManager } from "js.foresight" // Register an element to be tracked const myLink = document.getElementById("my-link") ForesightManager.instance.register({ element: myLink, callback: () => { // This is where your prefetching logic goes console.log("User is likely to interact with this element!") }, // Optional element settings }) ``` Thats it! Remember to unregister If you permanently remove a registered element from the DOM, call `ForesightManager.instance.unregister(element)` to stop tracking it. Detaching an element does not unregister it. It is parked (kept registered but inactive) and resumes if it reattaches, so an element you discard without unregistering stays parked and cannot be garbage collected. The official React and Vue integrations do this for you on unmount. ## Provide element settings[​](#provide-element-settings "Direct link to Provide element settings") However if you want to add a bit more power to your element you can give it the following props: ``` import { ForesightManager } from "js.foresight" const myLink = document.getElementById("my-link") ForesightManager.instance.register({ element: myLink, callback: () => { console.log("User is likely to interact with this element!") }, hitSlop: 50, // slop around the element, making its hitbox bigger name: "My Foresight button!", // name visible in the debug tools meta: { route: "/about", }, // your custom meta data for analytics reactivateAfter: 5 * 60 * 1000, // time for the element to reactivate after the callback has been hit }) ``` ## Integrations[​](#integrations "Direct link to Integrations") Since ForesightJS is framework agnostic, it can be integrated with any JavaScript framework. Ready-to-use implementations are available for: * [Next.js](/docs/3.5/react/nextjs.md) * [React Router](/docs/3.5/react/react-router.md) * [Vue](/docs/3.5/vue/directive.md) * [Angular](/docs/3.5/angular.md) ## Development Tools[​](#development-tools "Direct link to Development Tools") ForesightJS has dedicated [Development Tools](/docs/3.5/debugging/devtools.md) that help you understand and tune how prediction is working in your application: ``` pnpm add js.foresight-devtools # or npm install js.foresight-devtools # or yarn add js.foresight-devtools ``` ``` import { ForesightDevtools } from "js.foresight-devtools" // Initialize development tools ForesightDevtools.initialize({ // optional props }) ``` --- # Hook The `useForesight` hook serves as the base for all ForesightJS usage with any React framework. ## Basic useForesight[​](#basic-useforesight "Direct link to Basic useForesight") ``` import { useRef, useEffect } from "react" import { ForesightManager, type ForesightRegisterOptionsWithoutElement, type ForesightRegisterResult, } from "js.foresight" export default function useForesight( options: ForesightRegisterOptionsWithoutElement ) { const elementRef = useRef(null) const registerResults = useRef(null) useEffect(() => { if (!elementRef.current) return registerResults.current = ForesightManager.instance.register({ element: elementRef.current, ...options, }) }, [options]) return { elementRef, registerResults } } ``` ### Return Values[​](#return-values "Direct link to Return Values") The hook returns an object containing: * `elementRef` - To attach to your target element * [`registerResults`](/docs/3.5/configuration/element-settings.md#return-properties) - Registration details like `isRegistered` **Important:** Due to React's rendering lifecycle, both `elementRef` and `registerResults` will be `null` during the initial render. The element gets registered only after the component mounts and the ref is attached. This means while implementing fallback prefetching logic, don't check if `registerResults` is `null`. Note that `isRegistered` stays `true` on a limited connection (the element is registered but inactive), so to decide whether to run your own fallback prefetch check `isLimitedConnection` (and `isActive`) rather than `isRegistered`. ### Basic Usage[​](#basic-usage "Direct link to Basic Usage") ``` import useForesight from "./useForesight" function MyComponent() { const { elementRef, registerResults } = useForesight({ callback: () => { console.log("Prefetching data...") // Your prefetch logic here }, hitSlop: 10, name: "my-button", }) return } ``` ### Framework Integrations[​](#framework-integrations "Direct link to Framework Integrations") For ready-to-use components built on top of useForesight, see our framework-specific integrations: * [React Router](/docs/3.5/react/react-router.md#foresightlink-component) * [Next.js](/docs/3.5/react/nextjs.md#foresightlink-component) --- # Next.js ## Next.js default prefetching[​](#nextjs-default-prefetching "Direct link to Next.js default prefetching") Next.js's default prefetching method prefetches when links enter the viewport, this is a great user experience but can lead to unnecessary data transfer for bigger websites. For example by scrolling down the [Next.js homepage](https://nextjs.org/) it triggers **\~1.59MB** of prefetch requests as every single link on the page gets prefetched, regardless of user intent. To avoid this, we can wrap the `Link` component and add ForesightJS. The official Next.js [prefetching docs](https://nextjs.org/docs/app/guides/prefetching#extending-or-ejecting-link) mention ForesightJS as an example for custom prefetching strategies. ## ForesightLink Component[​](#foresightlink-component "Direct link to ForesightLink Component") Below is an example of creating an wrapper around the Next.js `Link` component that prefetches with ForesightJS. On mobile devices ForesightJS uses the configured [`touchDeviceStrategy`](/docs/3.5/configuration/global-settings.md#touch-device-settings-v330). This implementation uses the `useForesight` react hook which can be found [here](/docs/3.5/react/hook.md). ``` "use client" import type { LinkProps } from "next/link" import Link from "next/link" import { type ForesightRegisterOptions } from "js.foresight" import useForesight from "../hooks/useForesight" import { useRouter } from "next/navigation" interface ForesightLinkProps extends Omit, Omit { children: React.ReactNode className?: string } export function ForesightLink({ children, className, ...props }: ForesightLinkProps) { const router = useRouter() // import from "next/navigation" not "next/router" const { elementRef } = useForesight({ callback: () => { router.prefetch(props.href.toString()) }, hitSlop: props.hitSlop, name: props.name, meta: props.meta, reactivateAfter: props.reactivateAfter, }) return ( {children} ) } ``` ## Basic Usage[​](#basic-usage "Direct link to Basic Usage") ``` import ForesightLink from "./ForesightLink" export default function Navigation() { return ( Home ) } ``` caution If you dont see the correct prefetching behaviour make sure you are in production. Next.js only prefetches in production and not in development --- # React Router ## React Router's Prefetching[​](#react-routers-prefetching "Direct link to React Router's Prefetching") React Router DOM (v6.4+) uses no prefetching by default. While you can enable prefetching with options like `intent` (hover/focus) or `viewport`, it doesnt have the same flexibility as ForesightJS. To add ForesightJS to React Router you can create a `ForesightLink` component wrapping the `Link` component. ## ForesightLink Component[​](#foresightlink-component "Direct link to ForesightLink Component") Below is an example of creating an wrapper around the React Router `Link` component that prefetches with ForesightJS. On mobile devices ForesightJS uses the configured [`touchDeviceStrategy`](/docs/3.5/configuration/global-settings.md#touch-device-settings-v330). This implementation uses the `useForesight` react hook which can be found [here](/docs/3.5/react/hook.md). ``` import type { ForesightRegisterOptions } from "js.foresight" import { useState } from "react" import { Link, PrefetchPageLinks, type LinkProps } from "react-router" import useForesight from "./useForesight" interface ForesightLinkProps extends Omit, Omit { children: React.ReactNode className?: string } export function ForesightLink({ children, className, ...props }: ForesightLinkProps) { const [shouldPrefetch, setShouldPrefetch] = useState(false) const { elementRef, registerResults } = useForesight({ callback: () => { setShouldPrefetch(true) }, hitSlop: props.hitSlop, name: props.name, meta: props.meta, reactivateAfter: props.reactivateAfter, }) return ( <> {shouldPrefetch && } {children} ) } ``` ### Usage of ForesightLink[​](#usage-of-foresightlink "Direct link to Usage of ForesightLink") ``` export function Navigation() { return ( <> contact about ) } ``` --- # Composable The `useForesight` composable provides a Vue-native way to integrate ForesightJS into your Vue components. Unlike the [`v-foresight` directive](/docs/3.5/vue/directive.md), the composable gives you direct access to [`registerResults`](/docs/3.5/configuration/element-settings.md#registration-return-value) and the element's [`templateRef`](https://vuejs.org/guide/essentials/template-refs), but requires more setup code. ## Basic useForesight[​](#basic-useforesight "Direct link to Basic useForesight") Copy belows code in a `/composables/useForesight.ts` file ``` import { ref, onMounted, useTemplateRef, readonly, type ComponentPublicInstance } from "vue" import { ForesightManager, type ForesightRegisterOptionsWithoutElement, type ForesightRegisterResult, } from "js.foresight" type UseForesightOptions = ForesightRegisterOptionsWithoutElement & { templateRefKey: string } export function useForesight( options: UseForesightOptions ) { const templateRef = useTemplateRef(options.templateRefKey) const registerResults = ref(null) onMounted(() => { if (!templateRef.value) { return } // Extract the underlying HTMLElement if the templateRef is a Vue component const element = templateRef.value instanceof HTMLElement ? templateRef.value : templateRef.value.$el registerResults.value = ForesightManager.instance.register({ element, ...options, }) }) return { templateRef, registerResults: readonly(registerResults), } } ``` ### Return Values[​](#return-values "Direct link to Return Values") The composable returns an object containing: * [`templateRef`](https://vuejs.org/guide/essentials/template-refs) - Template ref to attach to your target element using `ref="yourRefKey"` * [`registerResults`](/docs/3.5/configuration/element-settings.md#registration-return-value) - Registration details like `isRegistered` ### Basic Usage[​](#basic-usage "Direct link to Basic Usage") This is the most basic way to use the composable and is basically the same as using the [`v-foresight` directive](/docs/3.5/vue/directive.md). ``` ``` ### Getting the [`templateRef`](https://vuejs.org/guide/essentials/template-refs)[​](#getting-the-templateref "Direct link to getting-the-templateref") The composable also returns the [`templateRef`](https://vuejs.org/guide/essentials/template-refs), giving you direct access to the underlying element or component instance. ``` ``` note When you want to access the [`templateRef`](https://vuejs.org/guide/essentials/template-refs) of a Vue component, you must pass `InstanceType` as the generic type parameter to get the correct component instance type. This is only necessary when destructuring [`templateRef`](https://vuejs.org/guide/essentials/template-refs) from the return value. --- # Directive The `v-foresight` directive provides a simple, declarative way to add ForesightJS prefetching to your Vue applications. Unlike the [`useForesight` composable](/docs/3.5/vue/composable.md), the directive offers a cleaner API with less setup code, but doesn't provide access to [`registerResults`](/docs/3.5/configuration/element-settings.md#registration-return-value) or the element's `templateRef`. It's ideal for most use cases where you don't need these advanced features. ## Basic v-foresight[​](#basic-v-foresight "Direct link to Basic v-foresight") Copy belows code in a `/directives/vForesight.ts` file ``` import type { Directive } from "vue" import { ForesightManager, type ForesightRegisterOptionsWithoutElement } from "js.foresight" type ForesightDirectiveValue = ForesightRegisterOptionsWithoutElement | (() => void) export const vForesight: Directive = { mounted(element, binding) { const value = binding.value const options: ForesightRegisterOptionsWithoutElement = typeof value === "function" ? { callback: value } : value ForesightManager.instance.register({ element, ...options, }) }, } ``` ### Register the directive globally[​](#register-the-directive-globally "Direct link to Register the directive globally") Register the directive in your `main.ts` like so: ``` import { vForesight } from "@/directives/vForesight" app.directive("foresight", vForesight) ``` ## Basic Usage[​](#basic-usage "Direct link to Basic Usage") The directive can be used in two ways: with a callback function or with a full options object. ### Simple Callback[​](#simple-callback "Direct link to Simple Callback") For basic use cases, pass a callback function directly: ``` ``` ### With Options[​](#with-options "Direct link to With Options") For more control, you can pass an options object with any [Element Settings](/docs/3.5/configuration/element-settings.md). ``` ``` --- # AI Context ForesightJS provides an `llms.txt` file following the [llms.txt specification](https://llmstxt.org/) to help LLMs understand and work with the library effectively. ## What is llms.txt?[​](#what-is-llmstxt "Direct link to What is llms.txt?") The `llms.txt` file is a standardized way to provide LLM-friendly information about a project. It contains: * A concise overview of what ForesightJS does * Links to key documentation sections in markdown format * Essential information for understanding the API and usage patterns ## Using ForesightJS's llms.txt[​](#using-foresightjss-llmstxt "Direct link to Using ForesightJS's llms.txt") ### Quick Access[​](#quick-access "Direct link to Quick Access") * **Main file:** [foresightjs.com/llms.txt](https://foresightjs.com/llms.txt) * **Full context:** [foresightjs.com/llms-full.txt](https://foresightjs.com/llms-full.txt) ### Example Prompt[​](#example-prompt "Direct link to Example Prompt") ``` Read the ForesightJS documentation from: https://foresightjs.com/llms.txt I need to implement ForesightJS predictive prefetching in my Next.js application. I want to: 1. Use the useForesight React hook for my custom link components 2. Integrate with Next.js router prefetching using ForesightLink 3. Use the default configuration, except that I want mobile prefetching to be on touch start ``` ## Individual Page Markdown[​](#individual-page-markdown "Direct link to Individual Page Markdown") All documentation pages are available as markdown by adding `.md` to any URL: * `https://foresightjs.com/docs/getting-started/what-is-foresightjs.md` * `https://foresightjs.com/docs/getting-started/initialize-the-manager.md` * `https://foresightjs.com/docs/getting-started/quick-start.md` * `https://foresightjs.com/docs/getting-started/typescript.md` * `https://foresightjs.com/docs/configuration/global-settings.md` * `https://foresightjs.com/docs/configuration/registration-options.md` * `https://foresightjs.com/docs/events.md` * `https://foresightjs.com/docs/debugging/devtools.md` * `https://foresightjs.com/docs/debugging/static-properties.md` * `https://foresightjs.com/docs/Behind_the_Scenes.md` * `https://foresightjs.com/docs/other-frameworks.md` * `https://foresightjs.com/docs/migrating-to-v4.md` * `https://foresightjs.com/docs/react/what-is-foresightjs.md` * `https://foresightjs.com/docs/react/installation.md` * `https://foresightjs.com/docs/react/initialize-the-manager.md` * `https://foresightjs.com/docs/react/quick-start.md` * `https://foresightjs.com/docs/react/typescript.md` * `https://foresightjs.com/docs/react/useForesight.md` * `https://foresightjs.com/docs/react/foresight-component.md` * `https://foresightjs.com/docs/react/useForesightEvent.md` * `https://foresightjs.com/docs/react/nextjs.md` * `https://foresightjs.com/docs/react/react-router.md` * `https://foresightjs.com/docs/react/configuration/global-settings.md` * `https://foresightjs.com/docs/react/configuration/registration-options.md` * `https://foresightjs.com/docs/react/events.md` * `https://foresightjs.com/docs/react/devtools.md` * `https://foresightjs.com/docs/react/static-properties.md` * `https://foresightjs.com/docs/react/migrating-to-v4.md` * `https://foresightjs.com/docs/react/behind-the-scenes.md` * `https://foresightjs.com/docs/react/ai-context.md` * `https://foresightjs.com/docs/vue/what-is-foresightjs.md` * `https://foresightjs.com/docs/vue/installation.md` * `https://foresightjs.com/docs/vue/initialize-the-manager.md` * `https://foresightjs.com/docs/vue/quick-start.md` * `https://foresightjs.com/docs/vue/typescript.md` * `https://foresightjs.com/docs/vue/directive.md` * `https://foresightjs.com/docs/vue/useForesight.md` * `https://foresightjs.com/docs/vue/foresight-component.md` * `https://foresightjs.com/docs/vue/useForesightEvent.md` * `https://foresightjs.com/docs/vue/configuration/global-settings.md` * `https://foresightjs.com/docs/vue/configuration/registration-options.md` * `https://foresightjs.com/docs/vue/events.md` * `https://foresightjs.com/docs/vue/devtools.md` * `https://foresightjs.com/docs/vue/static-properties.md` * `https://foresightjs.com/docs/vue/migrating-to-v4.md` * `https://foresightjs.com/docs/vue/behind-the-scenes.md` * `https://foresightjs.com/docs/vue/ai-context.md` * `https://foresightjs.com/docs/angular/what-is-foresightjs.md` * `https://foresightjs.com/docs/angular/installation.md` * `https://foresightjs.com/docs/angular/initialize-the-manager.md` * `https://foresightjs.com/docs/angular/quick-start.md` * `https://foresightjs.com/docs/angular/typescript.md` * `https://foresightjs.com/docs/angular/directive.md` * `https://foresightjs.com/docs/angular/foresight-service.md` * `https://foresightjs.com/docs/angular/foresight-component.md` * `https://foresightjs.com/docs/angular/injectForesightEvent.md` * `https://foresightjs.com/docs/angular/configuration/global-settings.md` * `https://foresightjs.com/docs/angular/configuration/registration-options.md` * `https://foresightjs.com/docs/angular/events.md` * `https://foresightjs.com/docs/angular/devtools.md` * `https://foresightjs.com/docs/angular/static-properties.md` * `https://foresightjs.com/docs/angular/migrating-to-v4.md` * `https://foresightjs.com/docs/angular/behind-the-scenes.md` * `https://foresightjs.com/docs/angular/ai-context.md` --- # AI Context ForesightJS provides an `llms.txt` file following the [llms.txt specification](https://llmstxt.org/) to help LLMs understand and work with the library effectively. ## What is llms.txt?[​](#what-is-llmstxt "Direct link to What is llms.txt?") The `llms.txt` file is a standardized way to provide LLM-friendly information about a project. It contains: * A concise overview of what ForesightJS does * Links to key documentation sections in markdown format * Essential information for understanding the API and usage patterns ## Using ForesightJS's llms.txt[​](#using-foresightjss-llmstxt "Direct link to Using ForesightJS's llms.txt") ### Quick Access[​](#quick-access "Direct link to Quick Access") * **Main file:** [foresightjs.com/llms.txt](https://foresightjs.com/llms.txt) * **Full context:** [foresightjs.com/llms-full.txt](https://foresightjs.com/llms-full.txt) ### Example Prompt[​](#example-prompt "Direct link to Example Prompt") ``` Read the ForesightJS documentation from: https://foresightjs.com/llms.txt I need to implement ForesightJS predictive prefetching in my Next.js application. I want to: 1. Use the useForesight React hook for my custom link components 2. Integrate with Next.js router prefetching using ForesightLink 3. Use the default configuration, except that I want mobile prefetching to be on touch start ``` ## Individual Page Markdown[​](#individual-page-markdown "Direct link to Individual Page Markdown") All documentation pages are available as markdown by adding `.md` to any URL: * `https://foresightjs.com/docs/getting-started/what-is-foresightjs.md` * `https://foresightjs.com/docs/getting-started/initialize-the-manager.md` * `https://foresightjs.com/docs/getting-started/quick-start.md` * `https://foresightjs.com/docs/getting-started/typescript.md` * `https://foresightjs.com/docs/configuration/global-settings.md` * `https://foresightjs.com/docs/configuration/registration-options.md` * `https://foresightjs.com/docs/events.md` * `https://foresightjs.com/docs/debugging/devtools.md` * `https://foresightjs.com/docs/debugging/static-properties.md` * `https://foresightjs.com/docs/Behind_the_Scenes.md` * `https://foresightjs.com/docs/other-frameworks.md` * `https://foresightjs.com/docs/migrating-to-v4.md` * `https://foresightjs.com/docs/react/what-is-foresightjs.md` * `https://foresightjs.com/docs/react/installation.md` * `https://foresightjs.com/docs/react/initialize-the-manager.md` * `https://foresightjs.com/docs/react/quick-start.md` * `https://foresightjs.com/docs/react/typescript.md` * `https://foresightjs.com/docs/react/useForesight.md` * `https://foresightjs.com/docs/react/foresight-component.md` * `https://foresightjs.com/docs/react/useForesightEvent.md` * `https://foresightjs.com/docs/react/nextjs.md` * `https://foresightjs.com/docs/react/react-router.md` * `https://foresightjs.com/docs/react/configuration/global-settings.md` * `https://foresightjs.com/docs/react/configuration/registration-options.md` * `https://foresightjs.com/docs/react/events.md` * `https://foresightjs.com/docs/react/devtools.md` * `https://foresightjs.com/docs/react/static-properties.md` * `https://foresightjs.com/docs/react/migrating-to-v4.md` * `https://foresightjs.com/docs/react/behind-the-scenes.md` * `https://foresightjs.com/docs/react/ai-context.md` * `https://foresightjs.com/docs/vue/what-is-foresightjs.md` * `https://foresightjs.com/docs/vue/installation.md` * `https://foresightjs.com/docs/vue/initialize-the-manager.md` * `https://foresightjs.com/docs/vue/quick-start.md` * `https://foresightjs.com/docs/vue/typescript.md` * `https://foresightjs.com/docs/vue/directive.md` * `https://foresightjs.com/docs/vue/useForesight.md` * `https://foresightjs.com/docs/vue/foresight-component.md` * `https://foresightjs.com/docs/vue/useForesightEvent.md` * `https://foresightjs.com/docs/vue/configuration/global-settings.md` * `https://foresightjs.com/docs/vue/configuration/registration-options.md` * `https://foresightjs.com/docs/vue/events.md` * `https://foresightjs.com/docs/vue/devtools.md` * `https://foresightjs.com/docs/vue/static-properties.md` * `https://foresightjs.com/docs/vue/migrating-to-v4.md` * `https://foresightjs.com/docs/vue/behind-the-scenes.md` * `https://foresightjs.com/docs/vue/ai-context.md` * `https://foresightjs.com/docs/angular/what-is-foresightjs.md` * `https://foresightjs.com/docs/angular/installation.md` * `https://foresightjs.com/docs/angular/initialize-the-manager.md` * `https://foresightjs.com/docs/angular/quick-start.md` * `https://foresightjs.com/docs/angular/typescript.md` * `https://foresightjs.com/docs/angular/directive.md` * `https://foresightjs.com/docs/angular/foresight-service.md` * `https://foresightjs.com/docs/angular/foresight-component.md` * `https://foresightjs.com/docs/angular/injectForesightEvent.md` * `https://foresightjs.com/docs/angular/configuration/global-settings.md` * `https://foresightjs.com/docs/angular/configuration/registration-options.md` * `https://foresightjs.com/docs/angular/events.md` * `https://foresightjs.com/docs/angular/devtools.md` * `https://foresightjs.com/docs/angular/static-properties.md` * `https://foresightjs.com/docs/angular/migrating-to-v4.md` * `https://foresightjs.com/docs/angular/behind-the-scenes.md` * `https://foresightjs.com/docs/angular/ai-context.md` --- # 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. Registering the same element again patches its options and increments `registerCount`, preserving its identity, state, and subscriptions. Omitted options keep their current values. 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**: `pointermove` events record `clientX` and `clientY` coordinates for mouse and pen input. 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 `trajectoryPredictionTime` 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 `pointerdown` events on registered HTML elements. Callbacks fire when users begin interacting with 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. --- # Global Settings Global settings apply to all registered elements and are configured once via `ForesightManager.initialize()` at your app's entry point. Import `ForesightManager` from `@foresightjs/angular`. tip If you want the default global options you don't need to initialize the ForesightManager explicitly. ## Basic Configuration[​](#basic-configuration "Direct link to Basic Configuration") ``` // Initialize the manager once at the top of your app if you want custom global settings // ALL SETTINGS ARE OPTIONAL ForesightManager.initialize({ enableMousePrediction: true, positionHistorySize: 8, trajectoryPredictionTime: 80, defaultHitSlop: 10, enableTabPrediction: true, tabOffset: 3, enableScrollPrediction: true, scrollMargin: 150, touchDeviceStrategy: "viewport", enableManagerLogging: false, minimumConnectionType: "3g", setDataAttributes: true, }) ``` ## Available Settings[​](#available-settings "Direct link to Available Settings") **TypeScript Type:** `ForesightManagerSettings` note All numeric settings are clamped to their specified Min/Max values to prevent invalid configurations. ### Mouse Prediction Settings[​](#mouse-prediction-settings "Direct link to Mouse Prediction Settings") #### `enableMousePrediction`[​](#enablemouseprediction "Direct link to enablemouseprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether trajectory prediction is active. If `false`, only direct hovers will trigger the callback for mouse users. * **Note:** When this is turned off `ForesightJS` will still trigger `callbacks` while hovering over the element's extended range (`rect` + `hitSlop`) ``` ForesightManager.initialize({ enableMousePrediction: false, // Disable trajectory prediction }) ``` *** #### `positionHistorySize`[​](#positionhistorysize "Direct link to positionhistorysize") * **Type:** `number` * **Default:** `8` * **Clamped between:** `2-30` * **Description:** Number of mouse positions to keep in history for velocity calculations. Higher values provide smoother predictions but use more memory. ``` ForesightManager.initialize({ positionHistorySize: 12, // Keep more position history for smoother predictions }) ``` *** #### `trajectoryPredictionTime`[​](#trajectorypredictiontime "Direct link to trajectorypredictiontime") * **Type:** `number` * **Default:** `120` * **Clamped between:** `10-200` milliseconds * **Description:** How far ahead (in milliseconds) to predict the mouse trajectory. Larger values trigger callbacks sooner but may reduce accuracy. ``` ForesightManager.initialize({ trajectoryPredictionTime: 100, // Predict 100ms into the future }) ``` *** ### Keyboard Prediction Settings[​](#keyboard-prediction-settings "Direct link to Keyboard Prediction Settings") #### `enableTabPrediction`[​](#enabletabprediction "Direct link to enabletabprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether keyboard prediction is active for tab navigation. ``` ForesightManager.initialize({ enableTabPrediction: false, // Disable keyboard prediction }) ``` *** #### `tabOffset`[​](#taboffset "Direct link to taboffset") * **Type:** `number` * **Default:** `2` * **Clamped between:** `0-20` * **Description:** Number of tab stops away from an element to trigger its callback. Only works when `enableTabPrediction` is `true`. ``` ForesightManager.initialize({ tabOffset: 1, // Trigger callback when 1 tab stop away }) ``` *** ### Scroll Prediction Settings[​](#scroll-prediction-settings "Direct link to Scroll Prediction Settings") #### `enableScrollPrediction`[​](#enablescrollprediction "Direct link to enablescrollprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether scroll prediction is active. ``` ForesightManager.initialize({ enableScrollPrediction: false, // Disable scroll prediction }) ``` *** #### `scrollMargin`[​](#scrollmargin "Direct link to scrollmargin") * **Type:** `number` * **Default:** `150` * **Clamped between:** `30-300` pixels * **Description:** Sets the pixel distance to check from the mouse position in the scroll direction for triggering callbacks. ``` ForesightManager.initialize({ scrollMargin: 200, // Check 200px ahead in scroll direction }) ``` *** ### Hit Slop Settings[​](#hit-slop-settings "Direct link to Hit Slop Settings") #### `defaultHitSlop`[​](#defaulthitslop "Direct link to defaulthitslop") * **Type:** `number | Rect` * **Default:** `{top: 0, left: 0, right: 0, bottom: 0}` * **Clamped between:** `0-2000` pixels (per side) * **Description:** Default fully invisible "slop" around elements for all registered elements. Basically increases the hover hitbox. * **Note:** This can be overridden per element by setting its `hitSlop`. Changing `defaultHitSlop` at runtime does not replace existing elements' hit slop. Use `updateElementOptions` to change it, or `replaceElementOptions` with `hitSlop` omitted to restore the current default. ``` // Uniform hit slop ForesightManager.initialize({ defaultHitSlop: 20, // 20px on all sides }) // Custom hit slop for each side ForesightManager.initialize({ defaultHitSlop: { top: 10, left: 20, right: 20, bottom: 15, }, }) ``` *** ### Touch Device Settings[​](#touch-device-settings "Direct link to Touch Device Settings") #### `touchDeviceStrategy`[​](#touchdevicestrategy "Direct link to touchdevicestrategy") * **Type:** `TouchDeviceStrategy` * **Default:** `"onTouchStart"` * **Options:** `"none"`, `"viewport"`, `"onTouchStart"` * **Description:** Strategy to use for touch devices. Pen input uses the desktop handler with mouse, keyboard, and scroll prediction. ``` ForesightManager.initialize({ touchDeviceStrategy: "viewport", }) ``` *** ### Minimum Connection Settings[​](#minimum-connection-settings "Direct link to Minimum Connection Settings") #### `minimumConnectionType`[​](#minimumconnectiontype "Direct link to minimumconnectiontype") * **Type:** `MinimumConnectionType` * **Default:** `"3g"` * **Options:** `"slow-2g"`, `"2g"`, `"3g"`, `"4g"` * **Description:** The minimum connection speed required for prefetching. When the user's connection is slower than this threshold (or data saver is enabled), elements are still registered but stay inactive: they are excluded from prediction and never fire their callback. * **Note:** This feature relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS treats the connection as unlimited. * **Runtime behavior:** Connection limitations are checked when an element is first registered. Later connection changes or updates to `minimumConnectionType` do not recalculate existing registrations. Unregister and register again to recheck. ``` ForesightManager.initialize({ minimumConnectionType: "3g", }) ``` *** ### Other Settings[​](#other-settings "Direct link to Other Settings") #### `enableManagerLogging`[​](#enablemanagerlogging "Direct link to enablemanagerlogging") * **Type:** `boolean` * **Default:** `false` * **Description:** Logs basic information about the `ForesightManager` and its handlers that is not available through events. Mostly used by the maintainers of `ForesightJS` to debug the manager, but might be useful for implementers aswell. * **Note:** Examples of logs are: Initializing the manager, switching from device strategy (e.g. Mouse to pen), aborting controllers and invalidating cache. ``` ForesightManager.initialize({ enableManagerLogging: true, }) ``` *** ### Data Attribute Settings[​](#data-attribute-settings "Direct link to Data Attribute Settings") #### `setDataAttributes`[​](#setdataattributes "Direct link to setdataattributes") * **Type:** `boolean` * **Default:** `true` * **Description:** When `true`, the manager mirrors every registered element's prediction state onto `data-*` attributes via direct DOM mutation, so plain CSS can style predictions without any framework re-render. The attributes are kept in sync on every state change and removed when an element unregisters. Toggling this at runtime applies or removes the attributes on all currently-registered elements. * `data-predicted` — present while `isPredicted` is `true` * `data-active` — present while `isActive` is `true` (eligible to fire) * `data-callback-running` — present while `isCallbackRunning` is `true` * `data-status` — set to `"success"` or `"error"` once a callback completes ``` ForesightManager.initialize({ setDataAttributes: false, // Opt out of data-* mirroring }) ``` ``` [data-predicted] { outline: 2px solid orange; } ``` ## Runtime Configuration Changes[​](#runtime-configuration-changes "Direct link to Runtime Configuration Changes") You can update global settings at runtime using `alterGlobalSettings()`. This is mainly for developers building tools on top of `Foresight`. For regular use, set your global settings once during initialization with `ForesightManager.initialize()`. ``` // Change settings after initialization ForesightManager.instance.alterGlobalSettings({ trajectoryPredictionTime: 100, enableScrollPrediction: false, }) ``` ## Development Tools Integration[​](#development-tools-integration "Direct link to Development Tools Integration") Development Tools Visual development tools are available as a separate package, `js.foresight-devtools`. See the development tools page in the debugging section for details on installing and configuring it. The development tools provide a real-time interface for adjusting these global settings and seeing their immediate effects on prediction behavior. --- # Registration Options In Angular, registration options are what you pass to the [`[fsForesight]`](/docs/angular/directive.md) directive, [`ForesightService.register`](/docs/angular/foresight-service.md), or [`ForesightComponent`](/docs/angular/foresight-component.md). The directive, service, or component takes care of the element, so there is no `element` option. `callback` is the only required field. ``` import { Component } from "@angular/core" import { ForesightDirective } from "@foresightjs/angular" @Component({ standalone: true, imports: [ForesightDirective], template: ` About `, }) export class AboutLinkComponent { readonly prefetchAbout = () => { void fetch("/api/about") } } ``` When options change, the Angular integration updates the existing registration in place, resetting omitted fields to their defaults. ## Registration Parameters[​](#registration-parameters "Direct link to Registration Parameters") **TypeScript Type:** `ForesightRegisterOptions` or `ForesightRegisterOptionsWithoutElement` if you want to omit the `element` ### Required Parameters[​](#required-parameters "Direct link to Required Parameters") #### `element`[​](#element "Direct link to element") * **Type:** `element | NodeListOf` * **Required:** Yes * **Description:** The DOM element (or a `NodeList` of elements) to monitor for user interactions. When a `NodeList` is passed, all elements are registered with the same callback and settings. ``` // Single element const button = document.querySelector("#my-button") ForesightManager.instance.register({ element: button, callback: () => { /* prefetch logic */ }, }) // Multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.register({ element: navLinks, callback: state => { /* state identifies which element triggered */ }, }) ``` *** #### `callback`[​](#callback "Direct link to callback") * **Type:** `(state: ForesightElementState) => void` * **Required:** Yes * **Description:** Function that executes when interaction is predicted or occurs. This is where your prefetching logic goes. The callback receives the [`ForesightElementState`](#registration-return-value) of the element that triggered it, which is especially useful when registering multiple elements with a shared callback. * **Note:** If you await your prefetch logic the `callbackCompleted` event will show you how long your prefetch took to run. ``` ForesightManager.instance.register({ element: myElement, callback: state => { console.log(`Triggered ${state.name}`) // prefetch logic }, }) ``` *** ### Optional Parameters[​](#optional-parameters "Direct link to Optional Parameters") #### `hitSlop`[​](#hitslop "Direct link to hitslop") * **Type:** `number | Rect` * **Required:** No * **Default:** Uses `defaultHitSlop` from global settings (which is 0 if not set) * **Description:** Fully invisible "slop" around the element that increases the hover hitbox. ``` // Uniform hit slop (20px on all sides) ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: 20, }) // Custom hit slop for each side ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: { top: 10, left: 50, right: 50, bottom: 100, }, }) ``` *** #### `name`[​](#name "Direct link to name") * **Type:** `string` * **Required:** No * **Default:** `element.id` or `"unnamed"` if no id * **Description:** A descriptive name for the element, useful for development tools and debugging. ``` ForesightManager.instance.register({ element: myElement, callback: () => { /* logic */ }, name: "Product Navigation Button", // Helpful for debugging }) ``` *** #### `reactivateAfter`[​](#reactivateafter "Direct link to reactivateafter") * **Type:** `number` * **Required:** No * **Default:** `Infinity` * **Description:** Time in milliseconds after which the callback can be fired again. Set to `Infinity` to prevent callback from firing again after first execution. * **Note:** Even though `ForesightJS` doesn't store any data, you can set this to the same time as your data would become `stale` normally. In most cases you can just leave this on the default value, as it will get reset on page refresh/rerouting anyways. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, reactivateAfter: 5 * 60 * 1000, // 5 minutes }) ``` *** #### `meta`[​](#meta "Direct link to meta") * **Type:** `ForesightMeta` (defaults to `Record`) * **Required:** No * **Default:** `{}` * **Description:** Stores additional information about the registered element. Visible in all element-related events and in the devtools. Use [module augmentation](/docs/getting-started/typescript.md#typed-metadata) to define its type once for your entire app, including framework bindings. ``` ForesightManager.instance.register({ element: productLink, callback: () => {}, meta: { route: "/about", section: "main", category: "content", }, }) ``` *** #### `enabled`[​](#enabled "Direct link to enabled") * **Type:** `boolean` * **Required:** No * **Default:** `true` * **Description:** When `false` the element stays registered but inactive: it is excluded from prediction and never fires its callback. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, enabled: userWantsPrefetching, // toggle prediction without unregistering }) ``` ## Registration Return Value[​](#registration-return-value "Direct link to Registration Return Value") The `register()` method returns the full `ForesightElementState` snapshot of the element at registration time, plus five functions: `unregister()`, `subscribe(listener)`, `getSnapshot()`, `subscribeToBounds(listener)`, and `getBounds()`. When a single element is passed, it returns a `ForesightRegisterResult`. When a `NodeList` is passed, it returns `ForesightRegisterResult[]`. **TypeScript Type:** `ForesightRegisterResult` or `ForesightRegisterResult[]` ``` // Single element returns ForesightRegisterResult const { unregister, subscribe, getSnapshot, isLimitedConnection } = ForesightManager.instance.register({ element: myElement, callback: () => {}, }) // NodeList returns ForesightRegisterResult[] const results = ForesightManager.instance.register({ element: document.querySelectorAll(".my-elements"), callback: () => {}, }) ``` *** ### Returned Functions[​](#returned-functions "Direct link to Returned Functions") #### `unregister()`[​](#unregister "Direct link to unregister") Removes the element from `ForesightManager`'s tracking. Call this when you permanently remove the element from the DOM, see [unregister](/docs/debugging/static-properties.md#foresightmanagerinstanceunregisterelement-reason). #### `subscribe(listener)`[​](#subscribelistener "Direct link to subscribelistener") Subscribes to logical state changes for this element. The listener is called every time the state snapshot is replaced - never for geometry-only changes during scroll/resize, which live on the separate bounds channel (`subscribeToBounds`). Returns an unsubscribe function. Together with `getSnapshot` this makes the state usable as an external store without any scroll-induced re-renders. #### `getSnapshot()`[​](#getsnapshot "Direct link to getsnapshot") Returns the current immutable `ForesightElementState` snapshot for this element. The snapshot reference is replaced (never mutated) on every logical change, so the properties spread on the return value itself are only current as of registration; use `getSnapshot()` (optionally with `subscribe`) for the up-to-date state. #### `subscribeToBounds(listener)`[​](#subscribetoboundslistener "Direct link to subscribetoboundslistener") Subscribes to geometry changes for this element. The listener is called every time the element's position or size changes, which is every scroll/resize tick while the element is visible. Returns an unsubscribe function. #### `getBounds()`[​](#getbounds "Direct link to getbounds") Returns the current immutable `ElementBounds` snapshot for this element: its measured `originalRect` and the hitSlop-expanded `expandedRect` used for interaction detection. *** ### State Fields[​](#state-fields "Direct link to State Fields") The `ForesightElementState` fields, also passed to your [`callback`](#callback): | Field | Type | Description | | ---------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | Unique identifier assigned during registration. | | `name` | `string` | Human-readable name for debugging, set via the [`name`](#name) option. | | `meta` | `ForesightMeta` | Arbitrary user-supplied metadata, set via the [`meta`](#meta) option. | | `hitSlop` | `Exclude` | The normalized hit slop applied to this element, set via the [`hitSlop`](#hitslop) option. The element's rects are not part of the state, read them via `getBounds()`. | | `isRegistered` | `boolean` | Whether the element is currently tracked by the manager. Stays `true` from registration until it is explicitly unregistered. Detaching the element from the DOM does **not** unregister it: it is parked (kept registered but inactive) and resumes when it reconnects. | | `isActive` | `boolean` | Whether the element is currently eligible to fire its callback. `false` when the element is disabled (`isEnabled: false`), on a limited connection, or parked (`isParked: true`). | | `isParked` | `boolean` | `true` while the element is detached from the DOM and parked: kept registered but inactive. Becomes `false` again when the element reconnects and resumes. | | `isEnabled` | `boolean` | Whether prediction is enabled for this element, set via the [`enabled`](#enabled) option. When `false` the element stays registered but inactive. | | `isPredicted` | `boolean` | `true` once the element's callback has been triggered by a prediction hit. Stays `true` until the element is reactivated or unregistered. | | `isCallbackRunning` | `boolean` | `true` while the callback is executing (between invocation and completion). The callback is awaited, so this stays `true` for async callbacks until they resolve or reject. | | `isLimitedConnection` | `boolean` | `true` when the user's connection is slower than the configured `minimumConnectionType` global setting (defaults to `"3g"`) or data saver is enabled. The element is still registered, but stays inactive so no data is consumed. | | `isIntersectingWithViewport` | `boolean` | Whether the element is currently intersecting the viewport. | | `hitCount` | `number` | Number of times the callback has fired for this element. | | `registerCount` | `number` | Number of times this element has been (re)registered. | | `durationMs` | `number \| undefined` | Duration in ms of the most recent callback run. | | `status` | `"success" \| "error" \| undefined` | Status of the most recently completed callback. | | `error` | `string \| null` | Error message from the most recently completed callback. | | `reactivateAfter` | `number` | Time in ms after which the callback can be fired again (`Infinity` = never), set via the [`reactivateAfter`](#reactivateafter) option. | note The `isLimitedConnection` detection relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS treats the connection as unlimited. --- # Development Tools The `ForesightJS` development tools are framework agnostic and work the same in Angular as anywhere else; initialize them once next to your `ForesightManager.initialize` call. [![npm version](https://img.shields.io/npm/v/js.foresight-devtools.svg)](https://www.npmjs.com/package/js.foresight-devtools) [![npm downloads](https://img.shields.io/npm/dt/js.foresight-devtools.svg)](https://www.npmjs.com/package/js.foresight-devtools) `ForesightJS` offers dedicated [Development Tools](https://github.com/spaansba/ForesightJS/tree/main/packages/js.foresight-devtools), written in [Lit](https://lit.dev/), to help you better understand and fine-tune how `ForesightJS` works within your application. You can see the development tools in action on the [playground page](https://foresightjs.com/#playground), which includes visual trajectory indicators, element boundaries, and a control panel in the bottom-right corner. They are built entirely on `ForesightJS`'s built-in events, so they double as a working example of what you can build on the same event system. ## Installation[​](#installation "Direct link to Installation") To install the `ForesightJS` Development Tools package, use your preferred package manager: ``` pnpm add -D js.foresight-devtools # or npm install -D js.foresight-devtools # or yarn add -D js.foresight-devtools ``` ## Enabling Development Tools[​](#enabling-development-tools "Direct link to Enabling Development Tools") ``` import { ForesightManager } from "js.foresight" import { ForesightDevtools } from "js.foresight-devtools" // Initialize ForesightJS ForesightManager.initialize({}) // Initialize the development tools (all options are optional) ForesightDevtools.initialize({ show: { controlPanel: true, // the floating control panel nameTags: true, // the element name above each registered element elementOverlays: true, // the hit-slop boundary around each registered element mouseTrajectory: true, // the predicted mouse trajectory line scrollTrajectory: true, // the predicted scroll trajectory line }, isControlPanelDefaultMinimized: false, // start with the control panel minimized sortElementList: "visibility", // "visibility" | "documentOrder" | "insertionOrder" logging: { logLocation: "controlPanel", // "controlPanel" | "console" | "both" | "none" callbackCompleted: true, callbackInvoked: true, elementRegistered: false, elementUnregistered: false, managerSettingsChanged: true, mouseTrajectoryUpdate: false, // too noisy to log scrollTrajectoryUpdate: false, // too noisy to log deviceStrategyChanged: true, }, }) ``` ## Development Tools Features[​](#development-tools-features "Direct link to Development Tools Features") Once enabled, the `ForesightJS` Development Tools add several visual layers to your application, including mouse and scroll trajectories and element hitboxes. A control panel also appears in the bottom-right corner of the screen. ### Control Panel[​](#control-panel "Direct link to Control Panel") The control panel has three tabs: Settings, Elements, and Log. #### Settings Tab[​](#settings-tab "Direct link to Settings Tab") The Settings tab provides controls for mouse, keyboard, scroll, touch, and connection settings. Configure `defaultHitSlop`, `setDataAttributes`, and `enableManagerLogging` through the manager API. Changes made through the panel update the `ForesightManager` configuration. #### Elements Tab[​](#elements-tab "Direct link to Elements Tab") The Elements tab shows every element currently registered with the `ForesightManager`, grouped into **Active** and **Inactive** sections. Each element entry shows its current status through a color-coded indicator: * 🟢 **Green** - Active visible elements in desktop mode * ⚫ **Grey** - Active invisible elements in desktop mode * 🟣 **Purple** - Active elements while in touch device mode (all elements, we dont track visibility in this mode) * 🟡 **Yellow** - Elements which callbacks are currently executing * 🔘 **Light Gray** - Inactive elements Inactive elements also carry one or more **reason badges** explaining *why* they are inactive (an element can have several at once, e.g. `disabled` while `detached`): * `disabled` - turned off via the `enabled` option * `parked` - removed from the DOM (`isParked: true`); the element stays registered but inactive and resumes when it reconnects * `limited` - on a limited connection (data saver / slow network) * `fired` - already ran its callback and is waiting on `reactivateAfter` (with the default `Infinity` it stays fired) Each element can also be expanded to reveal its [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) information including settings, callback status, and metadata. For `fired` elements with a finite `reactivateAfter`, a countdown timer appears for their reactivation cooldown period; clicking this timer will instantly reactivate the element. #### Log Tab[​](#log-tab "Direct link to Log Tab") The Log tab displays real-time events emitted by `ForesightJS`. You can see callback execution times, the full element's lifecycle and other system events. Events can be filtered through the devtools initialization configuration or in the control panel itself. You can also print out the complete [`ForesightManager.instance.getManagerData`](/docs/debugging/static-properties.md#foresightmanagerinstancegetmanagerdata) state without having to call it from your code. caution Avoid logging frequently emitted events to the browser console, as it can noticeably slow down your development environment. Use the control panel for this instead. note Element overlay visualization and visibility sorting in the control panel only work with desktop/mouse prediction strategies. When debugging `touchDeviceStrategy` configurations, these features are not available as touch strategies don't track the same positioning data. --- # fsForesight `[fsForesight]` is the simplest way to add predictive callbacks to an Angular app. Put it on an element, give it a callback or an options object, and it registers, updates, and unregisters the element for you. ## Import the directive[​](#import-the-directive "Direct link to Import the directive") `ForesightDirective` is standalone, so import it directly in the component that uses it: ``` import { Component } from "@angular/core" import { ForesightDirective } from "@foresightjs/angular" @Component({ standalone: true, imports: [ForesightDirective], template: ``, }) export class ExampleComponent { readonly prefetch = () => { void fetch("/api/example") } } ``` ## Callback shorthand[​](#callback-shorthand "Direct link to Callback shorthand") ``` ``` The callback receives the element's [`ForesightElementState`](/docs/angular/configuration/registration-options.md#state-fields). ## With options[​](#with-options "Direct link to With options") For more control, pass any [registration options](/docs/angular/configuration/registration-options.md): ``` ``` If the bound value changes, the directive updates the existing registration in place. Each options object describes the full configuration, so omitted fields reset to their defaults. ## Individual inputs[​](#individual-inputs "Direct link to Individual inputs") You can also pass common options through inputs: ``` Docs ``` Defined individual inputs override the matching fields in the options object. If you need a native `name` attribute, use `fsForesightName` for the Foresight debug name. ## Reading state[​](#reading-state "Direct link to Reading state") The directive exports itself as `foresight`: ``` ``` `state` is an Angular signal containing the latest immutable [`ForesightElementState`](/docs/angular/configuration/registration-options.md#state-fields). --- # Events ForesightManager emits events to provide insight into element registration, prediction activity, and callback execution. These events are primarily used by the [ForesightJS DevTools](/docs/angular/devtools.md), but can also power telemetry, analytics, and counters in your Angular app. ## Usage[​](#usage "Direct link to Usage") In Angular the easiest way to listen to events from a component, directive, or injectable context is [`injectForesightEvent`](/docs/angular/injectForesightEvent.md): ``` import { Component, signal } from "@angular/core" import { injectForesightEvent } from "@foresightjs/angular" @Component({ selector: "app-prefetch-counter", standalone: true, template: `

{{ hits() }} prefetches triggered

`, }) export class PrefetchCounterComponent { readonly hits = signal(0) constructor() { injectForesightEvent("callbackInvoked", () => { this.hits.update(count => count + 1) }) } } ``` The standard `ForesightManager.instance.addEventListener` / `removeEventListener` pattern is also available if you want to listen outside Angular. ## Available Events[​](#available-events "Direct link to Available Events") ### Interaction Events[​](#interaction-events "Direct link to Interaction Events") Events fired when user interactions trigger callbacks. *** #### `callbackInvoked`[​](#callbackinvoked "Direct link to callbackinvoked") Fired **before** an element's callback is executed ``` type CallbackInvokedEvent = { type: "callbackInvoked" timestamp: number element: ForesightElement state: ForesightElementState hitType: CallbackHitType } ``` **Related Types:** [`CallbackHitType`](/docs/getting-started/typescript.md#callbackhittype) • [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** #### `callbackCompleted`[​](#callbackcompleted "Direct link to callbackcompleted") Fired **after** an element's callback is executed ``` type CallbackCompletedEvent = { type: "callbackCompleted" timestamp: number element: ForesightElement state: ForesightElementState hitType: CallbackHitType elapsed: number // Time between callbackInvoked and callbackCompleted status: "success" | "error" | undefined errorMessage: string | null wasLastActiveElement: boolean } ``` **Related Types:** [`CallbackHitType`](/docs/getting-started/typescript.md#callbackhittype) • [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) ### Element Lifecycle Events[​](#element-lifecycle-events "Direct link to Element Lifecycle Events") Events fired during element registration, updates, and cleanup. *** #### `elementRegistered`[​](#elementregistered "Direct link to elementregistered") Fired when a new element is registered with `ForesightManager` using `ForesightManager.instance.register({ element, callback })`. Registering an already-tracked element updates its options without emitting another `elementRegistered` event. ``` type ElementRegisteredEvent = { type: "elementRegistered" timestamp: number element: ForesightElement state: ForesightElementState } ``` **Related Types:** [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** #### `elementUnregistered`[​](#elementunregistered "Direct link to elementunregistered") Fired when an element is removed from `ForesightManager`'s tracking via `ForesightManager.instance.unregister(element)`. Detaching an element from the DOM no longer unregisters it. It is parked (kept registered but inactive) and resumed on reattach, so no `elementUnregistered` event fires for that. The `"disconnected"` reason is therefore no longer emitted. ``` type ElementUnregisteredEvent = { type: "elementUnregistered" timestamp: number element: ForesightElement state: ForesightElementState unregisterReason: "disconnected" | "apiCall" | "devtools" | (string & {}) wasLastRegisteredElement: boolean } ``` **Related Types:** [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** ### Prediction Events[​](#prediction-events "Direct link to Prediction Events") Events fired during movement prediction calculations. *** #### `mouseTrajectoryUpdate`[​](#mousetrajectoryupdate "Direct link to mousetrajectoryupdate") Fired during mouse movement for trajectory calculations ``` type MouseTrajectoryUpdateEvent = { type: "mouseTrajectoryUpdate" trajectoryPositions: { positions: CircularBuffer // mouse position history currentPoint: { x: number; y: number } predictedPoint: { x: number; y: number } } predictionEnabled: boolean } ``` *** #### `scrollTrajectoryUpdate`[​](#scrolltrajectoryupdate "Direct link to scrolltrajectoryupdate") Fired during scroll events when scroll prediction is active ``` type ScrollTrajectoryUpdateEvent = { type: "scrollTrajectoryUpdate" currentPoint: Point // { x: number; y: number } predictedPoint: Point // { x: number; y: number } scrollDirection: ScrollDirection // "down" | "up" | "left" | "right" } ``` *** ### Configuration Events[​](#configuration-events "Direct link to Configuration Events") Events fired when ForesightManager configuration changes. *** #### `managerSettingsChanged`[​](#managersettingschanged "Direct link to managersettingschanged") Fired when global `ForesightManager` settings are updated via the devtools or via `ForesightManager.instance.alterGlobalSettings()`. ``` type ManagerSettingsChangedEvent = { type: "managerSettingsChanged" timestamp: number managerData: Readonly updatedSettings: UpdatedManagerSetting[] } ``` **Related Types:** [`ForesightManagerData`](/docs/getting-started/typescript.md#foresightmanagerdata) *** #### `deviceStrategyChanged`[​](#devicestrategychanged "Direct link to devicestrategychanged") Fired when user switches between input methods (mouse, touch, or pen). ``` type DeviceStrategyChangedEvent = { type: "deviceStrategyChanged" timestamp: number newStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" oldStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" } ``` *** --- # ForesightComponent `ForesightComponent` registers its own host element and projects your content inside it. It is useful when you want a component-shaped wrapper instead of adding a directive to an existing element. ## Usage[​](#usage "Direct link to Usage") ``` import { Component } from "@angular/core" import { ForesightComponent } from "@foresightjs/angular" @Component({ selector: "app-actions", standalone: true, imports: [ForesightComponent], template: ` `, }) export class ActionsComponent { readonly prefetchCheckout = () => { void fetch("/api/checkout") } } ``` The component accepts these inputs: * `callback` (required) * `foresightName` * `hitSlop` * `meta` * `reactivateAfter` * `enabled` `foresightName` maps to the Foresight `name` option so native `name` attributes can still be used inside projected content. ## When to use the directive instead[​](#when-to-use-the-directive-instead "Direct link to When to use the directive instead") Most Angular apps should use [`[fsForesight]`](/docs/angular/directive.md). It registers the exact DOM element you put it on and does not add a wrapper element. Use `ForesightComponent` when the wrapper itself is the target you want to track. --- # ForesightService `ForesightService` is the injectable Angular API underneath the directive and component. Use it when a directive is not a good fit, or when you want to register an element from lifecycle code. ## Register an element manually[​](#register-an-element-manually "Direct link to Register an element manually") ``` import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild, inject } from "@angular/core" import { ForesightService, type ForesightRegistration } from "@foresightjs/angular" @Component({ selector: "app-manual-prefetch", standalone: true, template: ``, }) export class ManualPrefetchComponent implements AfterViewInit, OnDestroy { @ViewChild("button", { static: true }) button!: ElementRef private readonly foresight = inject(ForesightService) private registration: ForesightRegistration | null = null ngAfterViewInit(): void { this.registration = this.foresight.register(this.button.nativeElement, { callback: () => void fetch("/api/checkout"), name: "checkout-button", hitSlop: 40, }) } ngOnDestroy(): void { this.registration?.unregister() } } ``` `register()` returns a `ForesightRegistration`: * `state`: an Angular signal with the latest [`ForesightElementState`](/docs/angular/configuration/registration-options.md#state-fields) * `update(options)`: updates the existing registration, resetting omitted fields to their defaults * `unregister()`: removes the element from the manager * `getSnapshot()`: reads the current state snapshot ## Update options[​](#update-options "Direct link to Update options") Pass the full configuration you want to keep. For example, this update removes the initial `hitSlop: 40` and restores the current global `defaultHitSlop`: ``` this.registration?.update({ callback: () => void fetch("/api/checkout"), name: "checkout-button", enabled: this.canPrefetch(), }) ``` ## Listen to manager events[​](#listen-to-manager-events "Direct link to Listen to manager events") Use `listen()` for app-level analytics or debugging outside a component injection context: ``` const stopListening = this.foresight.listen("callbackInvoked", event => { console.log("Prefetch started", event.state.name) }) stopListening() ``` Inside a component, [`injectForesightEvent`](/docs/angular/injectForesightEvent.md) usually gives cleaner cleanup. --- # Initialize the Manager Optional Step This step is **not needed** if you don't want to configure ForesightJS. The manager will initialize automatically with default settings the first time a directive, component, or service registration runs. If you don't want to configure anything, skip to [Quick Start](/docs/angular/quick-start.md). If you want to customize ForesightJS's global behavior, call `ForesightManager.initialize()` once at your application's entry point before Angular renders components that register elements: ``` import { ForesightManager } from "@foresightjs/angular" ForesightManager.initialize({ defaultHitSlop: 20, touchDeviceStrategy: "viewport", }) ``` ## Optional Settings[​](#optional-settings "Direct link to Optional Settings") You can configure any of these [global settings](/docs/angular/configuration/global-settings.md). ``` ForesightManager.initialize({ // Mouse prediction settings enableMousePrediction: true, // Enable/disable mouse trajectory prediction trajectoryPredictionTime: 120, // How far ahead to predict (10-200ms) positionHistorySize: 8, // Mouse positions to track (2-30) // Keyboard prediction settings enableTabPrediction: true, // Enable/disable keyboard navigation prediction tabOffset: 2, // Tab stops ahead to trigger (0-20) // Scroll prediction settings enableScrollPrediction: true, // Enable/disable scroll prediction scrollMargin: 150, // Pixel distance for scroll detection (30-300) // Touch device settings touchDeviceStrategy: "viewport", // "none", "viewport", or "onTouchStart" // Connection settings minimumConnectionType: "3g", // "slow-2g", "2g", "3g", "4g" // Default element settings defaultHitSlop: 0, // Default hit slop for all elements (number or {top, right, bottom, left}) }) ``` ## Good to know[​](#good-to-know "Direct link to Good to know") The `ForesightManager` is a singleton, meaning it can only be initialized once. If you call `ForesightManager.initialize()` multiple times, only the first call will take effect. Any subsequent calls will simply return the already initialized instance. The manager also provides several static methods you can use to inspect registered elements, callback counts, and [event](/docs/angular/events.md) listeners. You can find a list of these methods [here](/docs/angular/static-properties.md). --- # injectForesightEvent This is meant for high-level, app-wide use cases like analytics, debugging, or a global prediction counter. `injectForesightEvent` subscribes to [`ForesightManager` events](/docs/angular/events.md) for the lifetime of the current Angular injection context. ``` import { Component, signal } from "@angular/core" import { injectForesightEvent } from "@foresightjs/angular" @Component({ selector: "app-prefetch-counter", standalone: true, template: `

{{ hits() }} prefetches triggered

`, }) export class PrefetchCounterComponent { readonly hits = signal(0) constructor() { injectForesightEvent("callbackInvoked", () => { this.hits.update(count => count + 1) }) } } ``` You don't need to remove the listener yourself. Angular destroys the subscription with the component, directive, service, or other injection context where the helper was called. ## Dynamic event type[​](#dynamic-event-type "Direct link to Dynamic event type") Pass an Angular signal when the event type needs to change over time. The subscription re-binds automatically: ``` readonly eventType = signal<"callbackInvoked" | "callbackCompleted">("callbackInvoked") constructor() { injectForesightEvent(this.eventType, event => { console.log(event.type) }) } ``` For listeners outside an injection context, use [`ForesightService.listen`](/docs/angular/foresight-service.md#listen-to-manager-events). --- # Installation [![npm version](https://img.shields.io/npm/v/@foresightjs/angular.svg)](https://www.npmjs.com/package/@foresightjs/angular) [![npm downloads](https://img.shields.io/npm/dt/@foresightjs/angular.svg)](https://www.npmjs.com/package/@foresightjs/angular) `@foresightjs/angular` is the official Angular package for ForesightJS. It ships the standalone [`[fsForesight]`](/docs/angular/directive.md) directive for the common case, plus [`ForesightService`](/docs/angular/foresight-service.md), [`ForesightComponent`](/docs/angular/foresight-component.md), and [`injectForesightEvent`](/docs/angular/injectForesightEvent.md) for manual registration and event listeners. ``` npm install @foresightjs/angular js.foresight # or pnpm add @foresightjs/angular js.foresight # or yarn add @foresightjs/angular js.foresight ``` ``` import { ForesightDirective, ForesightManager, ForesightService } from "@foresightjs/angular" ``` ## Configuring the manager[​](#configuring-the-manager "Direct link to Configuring the manager") You don't have to configure anything. The manager auto-initializes with sensible defaults the first time you register an element. If you want to tweak global behavior, call `ForesightManager.initialize()` once in your `main.ts` before your app renders. See [Initialize the Manager](/docs/angular/initialize-the-manager.md) and [Global Settings](/docs/angular/configuration/global-settings.md). ## Devtools[​](#devtools "Direct link to Devtools") The visual [Development Tools](/docs/angular/devtools.md) live in their own package (`js.foresight-devtools`) and work the same regardless of framework. --- # Migrating to v4 In v3 the Angular docs handed you a directive to copy into your own project. v4 replaces that with the official [`@foresightjs/angular`](/docs/angular/installation.md) package. Install it, delete your copied directive, and import the standalone directive from the package. ``` npm install @foresightjs/angular js.foresight ``` ``` - import { ForesightDirective } from "./directives/foresight.directive" + import { ForesightDirective } from "@foresightjs/angular" ``` The directive selector is `[fsForesight]`: ``` Pricing ``` For reactive state, export the directive as `foresight` and read its signal: ``` ``` ## Core changes[​](#core-changes "Direct link to Core changes") The core `js.foresight` library also has a few breaking changes. The package handles most of these for you, but they matter if you call `register()` or listen to events directly. The `register()` call itself is unchanged, but element data is now an immutable state snapshot you can subscribe to - the foundation the wrapper packages are built on: * **`ForesightElementData` is now `ForesightElementState`.** Your callback receives the flat state snapshot: `callback: (state: ForesightElementState) => void`. See [TypeScript](/docs/getting-started/typescript.md). * **`register()` returns the state plus `unregister`, `subscribe` and `getSnapshot`.** `isTouchDevice` is gone from the result; touch devices are handled internally by the configured `touchDeviceStrategy` global setting. * **The `elementDataUpdated`, `elementOptionsUpdated` and `elementReactivated` events are removed.** Per-element changes are now observed through the `subscribe`/`getSnapshot` pair returned by `register()`. The remaining events carry the state in a `state` field instead of `elementData`. * **Detached elements are parked, not unregistered.** Removing an element from the DOM no longer emits `elementUnregistered` with reason `disconnected`; the element stays registered but inactive and resumes when it reconnects. * **New `enabled` element option.** Set it to `false` to keep an element registered but excluded from prediction. --- # Quick Start This guide walks through registering your first element with ForesightJS in Angular and reading its prediction state. ## Basic Usage Example[​](#basic-usage-example "Direct link to Basic Usage Example") The simplest way to register an element is the [`[fsForesight]`](/docs/angular/directive.md) directive. Import the standalone directive, put it on an element, and give it a callback: ``` import { Component } from "@angular/core" import { ForesightDirective } from "@foresightjs/angular" @Component({ selector: "app-nav", standalone: true, imports: [ForesightDirective], template: ` `, }) export class NavComponent { readonly prefetchPricing = () => { void fetch("/api/pricing") } } ``` That's it. Unregistration is automatic The directive registers the element when it appears and unregisters it when it is destroyed, so you never have to call `unregister()` yourself. The same applies to [`ForesightComponent`](/docs/angular/foresight-component.md). ## Provide registration options[​](#provide-registration-options "Direct link to Provide registration options") If you want more control, pass a full options object instead of just a callback: ``` ``` See [registration options](/docs/angular/configuration/registration-options.md) for the full list. ## Reading prediction state[​](#reading-prediction-state "Direct link to Reading prediction state") Export the directive as `foresight` to read its Angular signal state in the template: ``` {{ foresight.state().isPredicted ? "Pricing ready" : "Pricing" }} ``` ## Development Tools[​](#development-tools "Direct link to Development Tools") ForesightJS has dedicated [Development Tools](/docs/angular/devtools.md) that help you understand and tune how prediction is working in your application: ``` pnpm add js.foresight-devtools # or npm install js.foresight-devtools # or yarn add js.foresight-devtools ``` ``` import { ForesightDevtools } from "js.foresight-devtools" ForesightDevtools.initialize({ // optional props }) ``` --- # Static Properties The directive and component handle registering and unregistering for you; the properties below are escape hatches for debugging and inspecting the manager state. ``` import { ForesightManager } from "@foresightjs/angular" ``` The ForesightManager exposes static properties and instance APIs for accessing and controlling manager state. ***All properties are read-only*** ## ForesightManager.instance[​](#foresightmanagerinstance "Direct link to ForesightManager.instance") Gets the singleton instance of ForesightManager, initializing it if necessary. This is the primary way to access the manager throughout your application. **Returns:** `ForesightManager` **Example:** ``` const manager = ForesightManager.instance // Register a single element manager.register({ element: myButton, callback: () => console.log("Predicted interaction!"), }) // Register multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") manager.register({ element: navLinks, callback: state => console.log(`Predicted: ${state.name}`), }) ``` ## ForesightManager.instance.registeredElements[​](#foresightmanagerinstanceregisteredelements "Direct link to ForesightManager.instance.registeredElements") Gets a Map of all currently registered elements and their associated [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate). This is useful for debugging or inspecting the current state of registered elements. **Returns:** `ReadonlyMap` ## ForesightManager.isInitiated[​](#foresightmanagerisinitiated "Direct link to ForesightManager.isInitiated") Checks whether the ForesightManager has been initialized without initializing it. **Returns:** `Readonly` ## ForesightManager.instance.reactivate(element)[​](#foresightmanagerinstancereactivateelement "Direct link to ForesightManager.instance.reactivate(element)") Manually reactivates an element (or all elements in a `NodeList`), allowing its callback to be triggered again. This is useful when you want to re-enable prefetching for an element before its `reactivateAfter` timeout expires. **Parameters:** * `element` - The DOM element or `NodeList` to reactivate **Example:** ``` // Single element const myButton = document.getElementById("my-button") ForesightManager.instance.reactivate(myButton) // Multiple elements const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.reactivate(navLinks) ``` ## ForesightManager.instance.unregister(element, reason?)[​](#foresightmanagerinstanceunregisterelement-reason "Direct link to ForesightManager.instance.unregister(element, reason?)") Removes an element (or all elements in a `NodeList`) from ForesightManager's tracking. Frameworks handle this for you The official React, Vue, and Angular integrations call `unregister` automatically when a component unmounts, so you rarely call it yourself there. Detaching an element from the DOM does **not** unregister it. It is parked (kept registered but inactive) and resumes when it reattaches, so it survives detach/reattach and re-parenting. Unregistering only happens when you call this method explicitly. If you register elements yourself (vanilla JS) and permanently remove one from the DOM, call `unregister` to stop tracking it. Otherwise it stays parked and keeps a reference to the detached element, which prevents it from being garbage collected. **Parameters:** * `element` - The DOM element or `NodeList` to unregister * `reason` (optional) - A string describing why the element was unregistered (useful for debugging via events) **Example:** ``` // Single element const myButton = document.getElementById("my-button") ForesightManager.instance.unregister(myButton, "user-navigation") // Multiple elements const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.unregister(navLinks) ``` ## ForesightManager.instance.getManagerData[​](#foresightmanagerinstancegetmanagerdata "Direct link to ForesightManager.instance.getManagerData") Snapshot of the current `ForesightManager` state, including all global settings, registered elements, position observer data, and interaction statistics. This is primarily used for debugging, monitoring, and development purposes. **Properties:** * `registeredElements` - `Map` of all currently registered elements and their associated [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) * `eventListeners` - `Map` of all event listeners listening to ForesightManager events. * `globalSettings` - Current global configuration settings * `globalCallbackHits` - Total `callback` execution counts by interaction type (mouse/tab/scroll/viewport/touch) and by subtype (hover/trajectory for mouse, forwards/reverse for tab, direction for scroll) * `currentDeviceStrategy` - Which strategy is being used: `mouse`, `touch`, or `pen`. This changes dynamically as the input method changes * `activeElementCount` - Amount of elements currently active (not the same as registered) * `parkedElementCount` - Amount of elements currently parked: detached from the DOM (`isParked: true`) and waiting to reconnect * `loadedModules` - Shows which handlers and predictors have been [lazy loaded](/docs/Behind_the_Scenes.md#bundle-optimization) **Returns:** `Readonly` The return will look something like this: ``` { "registeredElements": { "size": 7, "entries": "" }, "activeElementCount": 5, "parkedElementCount": 0, "currentDeviceStrategy": "mouse", "eventListeners": { "0": { "elementRegistered": [] }, "1": { "elementUnregistered": [] }, "2": { "mouseTrajectoryUpdate": [] }, "3": { "scrollTrajectoryUpdate": [] }, "4": { "managerSettingsChanged": [] }, "5": { "callbackInvoked": [] }, "6": { "callbackCompleted": [] } }, "globalSettings": { "defaultHitSlop": { "bottom": 0, "left": 0, "right": 0, "top": 0 }, "enableMousePrediction": true, "enableScrollPrediction": true, "enableTabPrediction": true, "positionHistorySize": 8, "scrollMargin": 150, "tabOffset": 2, "trajectoryPredictionTime": 120, "touchDeviceStrategy": "onTouchStart", "minimumConnectionType": "3g", "enableManagerLogging": false, "setDataAttributes": true }, "globalCallbackHits": { "mouse": { "hover": 0, "trajectory": 3 }, "scroll": { "down": 2, "left": 0, "right": 0, "up": 0 }, "tab": { "forwards": 3, "reverse": 0 }, "touch": 0, "viewport": 0, "total": 8 }, "loadedModules": { "desktopHandler": true, "touchHandler": false, "predictors": { "mouse": true, "tab": true, "scroll": true, "viewport": false, "touchStart": false } } } ``` --- # TypeScript `@foresightjs/angular` exports its binding types and common core types such as `ForesightElementState`, `ForesightCallback`, and `ForesightRegisterOptionsWithoutElement`: ``` import type { ForesightOptions, ForesightRegistration } from "@foresightjs/angular" ``` Other core types come from `js.foresight` directly: ``` import type { ElementBounds, ForesightRegisterResult, ForesightManagerData } from "js.foresight" ``` Angular prediction state is exposed as signals: ``` import type { ForesightStateSignal } from "@foresightjs/angular" type Props = { state: ForesightStateSignal } ``` ## Typed metadata[​](#typed-metadata "Direct link to Typed metadata") Augment `ForesightRegister`, the global type registry, to define your metadata shape once in a `.d.ts` file included by your app's `tsconfig.json`: ``` import "js.foresight" declare module "js.foresight" { interface ForesightRegister { meta: { href: string } } } ``` Declare `meta` as a **required key** in the registry. Writing `meta?: AppMeta` silently falls back to `Record`. Put optionality on the fields inside it instead, for example `meta: { href?: string }`. Registration options, callback state, events, devtools, and all framework bindings use this shape automatically. No generics are needed. ``` import { ForesightManager } from "js.foresight" ForesightManager.instance.register({ element: document.querySelector("a")!, meta: { href: "/about" }, callback: state => { console.log(state.meta.href.toUpperCase()) }, }) ``` Without augmentation, `ForesightMeta` defaults to `Record`. Metadata remains optional and defaults to `{}`. Unregistered framework snapshots also contain `{}`, so use optional fields such as `href?: string` if you read them before registration or register elements without metadata. Augmentation only changes types, it does not validate metadata at runtime. ## Most Used Internal types[​](#most-used-internal-types "Direct link to Most Used Internal types") ### ForesightElementState[​](#foresightelementstate "Direct link to ForesightElementState") This is the main type used in ForesightJS: the flat, immutable state snapshot of a registered element. Your [callback](#foresightcallback) receives it, most [events](/docs/events.md) carry it as `state`, and `register()` returns it (plus a few functions, see [ForesightRegisterResult](#foresightregisterresult)). The reference is replaced on every logical change, never mutated. Geometry-only changes use [ElementBounds](#elementbounds), on their own subscription channel. Scrolling can still change logical state, such as viewport visibility or prediction status. ``` type ForesightElementState = { id: string // unique id assigned during registration name: string // name visible in the devtools meta: ForesightMeta hitSlop: Exclude // normalized hit slop applied to this element isLimitedConnection: boolean isIntersectingWithViewport: boolean isRegistered: boolean isActive: boolean isParked: boolean // detached from the DOM, kept registered but inactive until it reconnects isEnabled: boolean isPredicted: boolean isCallbackRunning: boolean hitCount: number registerCount: number // amount of times this element has been (re)registered durationMs: number | undefined // duration of the most recent callback run status: "error" | "success" | undefined // status of the most recent callback error: string | null reactivateAfter: number } ``` See the [registration return value](/docs/configuration/registration-options.md#registration-return-value) for what each field means. ### ElementBounds[​](#elementbounds "Direct link to ElementBounds") The element's geometry: its measured rect and the [hitSlop](/docs/configuration/global-settings.md)-expanded rect used for interaction detection. Visible elements' position and size changes are published on a separate channel (`getBounds`/`subscribeToBounds`) and kept out of `ForesightElementState`, so geometry-only updates do not notify state subscribers. ``` type ElementBounds = { originalRect: DOMRectReadOnly // as returned by getBoundingClientRect() expandedRect: Readonly // originalRect + hitSlop } ``` ### ForesightRegisterResult[​](#foresightregisterresult "Direct link to ForesightRegisterResult") What `ForesightManager.instance.register()` returns: the element's state at registration time plus control functions. ``` type ForesightRegisterResult = ForesightElementState & { unregister: () => void subscribe: (listener: () => void) => () => void // logical state changes; returns an unsubscribe function getSnapshot: () => ForesightElementState // current state snapshot, stable reference across scrolls subscribeToBounds: (listener: () => void) => () => void // geometry changes (every scroll/resize tick) getBounds: () => ElementBounds // current geometry snapshot } ``` ### CallbackHitType[​](#callbackhittype "Direct link to CallbackHitType") ``` type CallbackHitType = | { kind: "mouse"; subType: "hover" | "trajectory" } | { kind: "tab"; subType: "forwards" | "reverse" } | { kind: "scroll"; subType: "up" | "down" | "left" | "right" } | { kind: "touch"; subType?: string } | { kind: "viewport"; subType?: string } ``` ### CurrentDeviceStrategy[​](#currentdevicestrategy "Direct link to CurrentDeviceStrategy") Represents the current input method being used. ForesightJS automatically detects and switches between strategies. ``` type CurrentDeviceStrategy = "mouse" | "touch" | "pen" ``` ### ForesightManagerData[​](#foresightmanagerdata "Direct link to ForesightManagerData") Snapshot of the current ForesightManager state, including all [global settings](/docs/configuration/global-settings.md), registered elements, and interaction statistics. This is primarily used for debugging, monitoring, and development purposes. This data is returned in the [`managerSettingsChanged`](/docs/events.md) event or by calling [`ForesightManager.instance.getManagerData`](/docs/debugging/static-properties.md#foresightmanagerinstancegetmanagerdata) manually. ``` type ForesightManagerData = { registeredElements: ReadonlyMap // See above for ForesightElementState globalSettings: Readonly // See configuration for global settings globalCallbackHits: Readonly // See above for CallbackHitType, this is that but all of them combined eventListeners: ReadonlyMap> // All event listeners currently listening currentDeviceStrategy: CurrentDeviceStrategy activeElementCount: number parkedElementCount: number // elements detached from the DOM, waiting to reconnect loadedModules: ForesightModules // which handlers/predictors are lazily loaded } ``` ## Helper Types[​](#helper-types "Direct link to Helper Types") ### ForesightCallback[​](#foresightcallback "Direct link to ForesightCallback") The callback function type used when registering elements. Receives the [`ForesightElementState`](#foresightelementstate) of the element that triggered the callback. ``` type ForesightCallback = (state: ForesightElementState) => void ``` This is useful when registering multiple elements with a shared callback, as you can identify which element triggered it: ``` const myCallback: ForesightCallback = state => { console.log(`Triggered by: ${state.name}`) } ``` ### ForesightRegisterOptionsWithoutElement[​](#foresightregisteroptionswithoutelement "Direct link to ForesightRegisterOptionsWithoutElement") Useful for if you want to create a custom button component in a modern framework. And you want to have the `ForesightRegisterOptions` used in `ForesightManager.instance.register({})` without the element as the element will be the ref of the component. This type is the options object you pass to the [`useForesight`](/docs/react/useForesight.md) hook in React, the [`useForesight`](/docs/vue/useForesight.md) composable in Vue, and Angular's [`[fsForesight]`](/docs/angular/directive.md) / [`ForesightService`](/docs/angular/foresight-service.md) APIs. ``` type ForesightButtonProps = { registerOptions: ForesightRegisterOptionsWithoutElement } ``` --- # What is ForesightJS [![npm version](https://img.shields.io/npm/v/js.foresight.svg)](https://www.npmjs.com/package/js.foresight) [![npm downloads](https://img.shields.io/npm/dt/js.foresight.svg)](https://www.npmjs.com/package/js.foresight) [![Bundle Size](https://img.shields.io/bundlejs/size/js.foresight)](https://bundlejs.com/?q=js.foresight) [![GitHub stars](https://img.shields.io/github/stars/spaansba/ForesightJS.svg?style=social\&label=Star)](https://github.com/spaansba/ForesightJS) [![Best of JS](https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=spaansba%2FForesightJS%26since=daily)](https://bestofjs.org/projects/foresightjs) [![Demo](https://img.shields.io/badge/demo-live-blue)](https://foresightjs.com#playground) ForesightJS is a lightweight JavaScript library that predicts user intent to prefetch content before it's needed. It works completely out of the box without configuration, supporting both desktop and mobile devices with different prediction strategies. ## Understanding ForesightJS's Role[​](#understanding-foresightjss-role "Direct link to Understanding ForesightJS's Role") When you over simplify prefetching it exists of three parts: * **What** resource or data to load * **How** the loading method and caching strategy is * **When** the optimal moment to start fetching is ForesightJS takes care of the **When** by predicting user intent with mouse trajectory and tab navigation. You supply the **What** and **How** inside your `callback` when you register an element. In Angular you use ForesightJS through the official `@foresightjs/angular` package: the `[fsForesight]` directive registers your elements, `ForesightService` gives you a manual registration API, and Angular signals expose prediction state. ## Installation[​](#installation "Direct link to Installation") ``` npm install @foresightjs/angular js.foresight ``` Continue with [Installation](/docs/angular/installation.md) and the [Quick Start](/docs/angular/quick-start.md). ## Prediction Strategies[​](#prediction-strategies "Direct link to Prediction Strategies") ForesightJS uses different prediction strategies depending on the device type. For limited connections (slower than the minimum connection type - default: 3G - or [data-saver mode](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData)), we respect the user's preference to minimize data usage: the element is still registered but stays inactive, so it is never predicted and never prefetches. Foresight automatically detects mouse, pen, or touch input, and updates its strategy if the input method changes. ### Keyboard/Mouse Users[​](#keyboardmouse-users "Direct link to Keyboard/Mouse Users") Pick and choose multiple prediction strategies. All are enabled by default, but you can disable any of them when initializing the `ForesightManager`. * **Mouse Trajectory** default - Analyzes cursor movement patterns to predict which links users are heading towards and prefetches content before they arrive * **Keyboard Navigation** default - Tracks tab key usage to prefetch when the user is N tab stops away from your registered element * **Scroll** default - Prefetches content when users scroll towards registered elements, predicting which elements will be reached based on scroll direction ### Touch Devices[​](#touch-devices "Direct link to Touch Devices") While desktop systems use multiple concurrent prediction strategies, touch devices operate with a single active strategy that can be configured while initializing the `ForesightManager`. * **onTouchStart** default - Captures the initial touch event to begin prefetching when users start interacting with registered elements * **Viewport Enter** - Detects when registered elements enter the viewport and prefetches their content based on scroll behavior and visibility * **None** - Disables ForesightJS on touch devices (previous behavior) ## Why ForesightJS?[​](#why-foresightjs "Direct link to Why ForesightJS?") ForesightJS is a specialized performance tool, not something to drop into every project. ### Which problems does ForesightJS solve?[​](#which-problems-does-foresightjs-solve "Direct link to Which problems does ForesightJS solve?") #### Problem 1: On-Hover Prefetching Still Has Latency[​](#problem-1-on-hover-prefetching-still-has-latency "Direct link to Problem 1: On-Hover Prefetching Still Has Latency") Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires-time that could be used for prefetching. #### Problem 2: Viewport-Based Prefetching is Wasteful[​](#problem-2-viewport-based-prefetching-is-wasteful "Direct link to Problem 2: Viewport-Based Prefetching is Wasteful") Many modern frameworks (like Next.js) automatically prefetch resources for all links that enter the viewport. While well-intentioned, this creates significant overhead since users typically interact with only a small fraction of visible elements. Simply scrolling up and down the Next.js homepage can trigger ***1.59MB*** of unnecessary prefetch requests. #### Problem 3: Hover-Based Prefetching Excludes Keyboard Users[​](#problem-3-hover-based-prefetching-excludes-keyboard-users "Direct link to Problem 3: Hover-Based Prefetching Excludes Keyboard Users") Many routers rely on hover-based prefetching, but this approach completely excludes keyboard users since keyboard navigation never triggers hover events. This means keyboard users miss out on the performance benefits that mouse users get from hover-based prefetching. ## Lightweight[​](#lightweight "Direct link to Lightweight") The full library is **\~32 KB** minified, and code splitting means you only ship the parts you actually use. See [Behind the Scenes](/docs/Behind_the_Scenes.md#bundle-optimization) for the per-chunk breakdown. --- # AI Context ForesightJS provides an `llms.txt` file following the [llms.txt specification](https://llmstxt.org/) to help LLMs understand and work with the library effectively. ## What is llms.txt?[​](#what-is-llmstxt "Direct link to What is llms.txt?") The `llms.txt` file is a standardized way to provide LLM-friendly information about a project. It contains: * A concise overview of what ForesightJS does * Links to key documentation sections in markdown format * Essential information for understanding the API and usage patterns ## Using ForesightJS's llms.txt[​](#using-foresightjss-llmstxt "Direct link to Using ForesightJS's llms.txt") ### Quick Access[​](#quick-access "Direct link to Quick Access") * **Main file:** [foresightjs.com/llms.txt](https://foresightjs.com/llms.txt) * **Full context:** [foresightjs.com/llms-full.txt](https://foresightjs.com/llms-full.txt) ### Example Prompt[​](#example-prompt "Direct link to Example Prompt") ``` Read the ForesightJS documentation from: https://foresightjs.com/llms.txt I need to implement ForesightJS predictive prefetching in my Next.js application. I want to: 1. Use the useForesight React hook for my custom link components 2. Integrate with Next.js router prefetching using ForesightLink 3. Use the default configuration, except that I want mobile prefetching to be on touch start ``` ## Individual Page Markdown[​](#individual-page-markdown "Direct link to Individual Page Markdown") All documentation pages are available as markdown by adding `.md` to any URL: * `https://foresightjs.com/docs/getting-started/what-is-foresightjs.md` * `https://foresightjs.com/docs/getting-started/initialize-the-manager.md` * `https://foresightjs.com/docs/getting-started/quick-start.md` * `https://foresightjs.com/docs/getting-started/typescript.md` * `https://foresightjs.com/docs/configuration/global-settings.md` * `https://foresightjs.com/docs/configuration/registration-options.md` * `https://foresightjs.com/docs/events.md` * `https://foresightjs.com/docs/debugging/devtools.md` * `https://foresightjs.com/docs/debugging/static-properties.md` * `https://foresightjs.com/docs/Behind_the_Scenes.md` * `https://foresightjs.com/docs/other-frameworks.md` * `https://foresightjs.com/docs/migrating-to-v4.md` * `https://foresightjs.com/docs/react/what-is-foresightjs.md` * `https://foresightjs.com/docs/react/installation.md` * `https://foresightjs.com/docs/react/initialize-the-manager.md` * `https://foresightjs.com/docs/react/quick-start.md` * `https://foresightjs.com/docs/react/typescript.md` * `https://foresightjs.com/docs/react/useForesight.md` * `https://foresightjs.com/docs/react/foresight-component.md` * `https://foresightjs.com/docs/react/useForesightEvent.md` * `https://foresightjs.com/docs/react/nextjs.md` * `https://foresightjs.com/docs/react/react-router.md` * `https://foresightjs.com/docs/react/configuration/global-settings.md` * `https://foresightjs.com/docs/react/configuration/registration-options.md` * `https://foresightjs.com/docs/react/events.md` * `https://foresightjs.com/docs/react/devtools.md` * `https://foresightjs.com/docs/react/static-properties.md` * `https://foresightjs.com/docs/react/migrating-to-v4.md` * `https://foresightjs.com/docs/react/behind-the-scenes.md` * `https://foresightjs.com/docs/react/ai-context.md` * `https://foresightjs.com/docs/vue/what-is-foresightjs.md` * `https://foresightjs.com/docs/vue/installation.md` * `https://foresightjs.com/docs/vue/initialize-the-manager.md` * `https://foresightjs.com/docs/vue/quick-start.md` * `https://foresightjs.com/docs/vue/typescript.md` * `https://foresightjs.com/docs/vue/directive.md` * `https://foresightjs.com/docs/vue/useForesight.md` * `https://foresightjs.com/docs/vue/foresight-component.md` * `https://foresightjs.com/docs/vue/useForesightEvent.md` * `https://foresightjs.com/docs/vue/configuration/global-settings.md` * `https://foresightjs.com/docs/vue/configuration/registration-options.md` * `https://foresightjs.com/docs/vue/events.md` * `https://foresightjs.com/docs/vue/devtools.md` * `https://foresightjs.com/docs/vue/static-properties.md` * `https://foresightjs.com/docs/vue/migrating-to-v4.md` * `https://foresightjs.com/docs/vue/behind-the-scenes.md` * `https://foresightjs.com/docs/vue/ai-context.md` * `https://foresightjs.com/docs/angular/what-is-foresightjs.md` * `https://foresightjs.com/docs/angular/installation.md` * `https://foresightjs.com/docs/angular/initialize-the-manager.md` * `https://foresightjs.com/docs/angular/quick-start.md` * `https://foresightjs.com/docs/angular/typescript.md` * `https://foresightjs.com/docs/angular/directive.md` * `https://foresightjs.com/docs/angular/foresight-service.md` * `https://foresightjs.com/docs/angular/foresight-component.md` * `https://foresightjs.com/docs/angular/injectForesightEvent.md` * `https://foresightjs.com/docs/angular/configuration/global-settings.md` * `https://foresightjs.com/docs/angular/configuration/registration-options.md` * `https://foresightjs.com/docs/angular/events.md` * `https://foresightjs.com/docs/angular/devtools.md` * `https://foresightjs.com/docs/angular/static-properties.md` * `https://foresightjs.com/docs/angular/migrating-to-v4.md` * `https://foresightjs.com/docs/angular/behind-the-scenes.md` * `https://foresightjs.com/docs/angular/ai-context.md` --- # 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. Registering the same element again patches its options and increments `registerCount`, preserving its identity, state, and subscriptions. Omitted options keep their current values. 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**: `pointermove` events record `clientX` and `clientY` coordinates for mouse and pen input. 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 `trajectoryPredictionTime` 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 `pointerdown` events on registered HTML elements. Callbacks fire when users begin interacting with 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. --- # Global Settings Global settings apply to all registered elements. In Astro you pass them through the [`manager`](/docs/astro/integration.md#manager) option of the integration instead of calling `ForesightManager.initialize()` yourself: ``` // astro.config.mjs foresight({ manager: { trajectoryPredictionTime: 100, defaultHitSlop: 20, }, }) ``` The `manager` object accepts everything `ForesightManager.initialize()` does: tip If you want the default global options you don't need to initialize the ForesightManager explicitly. ## Basic Configuration[​](#basic-configuration "Direct link to Basic Configuration") ``` // Initialize the manager once at the top of your app if you want custom global settings // ALL SETTINGS ARE OPTIONAL ForesightManager.initialize({ enableMousePrediction: true, positionHistorySize: 8, trajectoryPredictionTime: 80, defaultHitSlop: 10, enableTabPrediction: true, tabOffset: 3, enableScrollPrediction: true, scrollMargin: 150, touchDeviceStrategy: "viewport", enableManagerLogging: false, minimumConnectionType: "3g", setDataAttributes: true, }) ``` ## Available Settings[​](#available-settings "Direct link to Available Settings") **TypeScript Type:** `ForesightManagerSettings` note All numeric settings are clamped to their specified Min/Max values to prevent invalid configurations. ### Mouse Prediction Settings[​](#mouse-prediction-settings "Direct link to Mouse Prediction Settings") #### `enableMousePrediction`[​](#enablemouseprediction "Direct link to enablemouseprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether trajectory prediction is active. If `false`, only direct hovers will trigger the callback for mouse users. * **Note:** When this is turned off `ForesightJS` will still trigger `callbacks` while hovering over the element's extended range (`rect` + `hitSlop`) ``` ForesightManager.initialize({ enableMousePrediction: false, // Disable trajectory prediction }) ``` *** #### `positionHistorySize`[​](#positionhistorysize "Direct link to positionhistorysize") * **Type:** `number` * **Default:** `8` * **Clamped between:** `2-30` * **Description:** Number of mouse positions to keep in history for velocity calculations. Higher values provide smoother predictions but use more memory. ``` ForesightManager.initialize({ positionHistorySize: 12, // Keep more position history for smoother predictions }) ``` *** #### `trajectoryPredictionTime`[​](#trajectorypredictiontime "Direct link to trajectorypredictiontime") * **Type:** `number` * **Default:** `120` * **Clamped between:** `10-200` milliseconds * **Description:** How far ahead (in milliseconds) to predict the mouse trajectory. Larger values trigger callbacks sooner but may reduce accuracy. ``` ForesightManager.initialize({ trajectoryPredictionTime: 100, // Predict 100ms into the future }) ``` *** ### Keyboard Prediction Settings[​](#keyboard-prediction-settings "Direct link to Keyboard Prediction Settings") #### `enableTabPrediction`[​](#enabletabprediction "Direct link to enabletabprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether keyboard prediction is active for tab navigation. ``` ForesightManager.initialize({ enableTabPrediction: false, // Disable keyboard prediction }) ``` *** #### `tabOffset`[​](#taboffset "Direct link to taboffset") * **Type:** `number` * **Default:** `2` * **Clamped between:** `0-20` * **Description:** Number of tab stops away from an element to trigger its callback. Only works when `enableTabPrediction` is `true`. ``` ForesightManager.initialize({ tabOffset: 1, // Trigger callback when 1 tab stop away }) ``` *** ### Scroll Prediction Settings[​](#scroll-prediction-settings "Direct link to Scroll Prediction Settings") #### `enableScrollPrediction`[​](#enablescrollprediction "Direct link to enablescrollprediction") * **Type:** `boolean` * **Default:** `true` * **Description:** Toggles whether scroll prediction is active. ``` ForesightManager.initialize({ enableScrollPrediction: false, // Disable scroll prediction }) ``` *** #### `scrollMargin`[​](#scrollmargin "Direct link to scrollmargin") * **Type:** `number` * **Default:** `150` * **Clamped between:** `30-300` pixels * **Description:** Sets the pixel distance to check from the mouse position in the scroll direction for triggering callbacks. ``` ForesightManager.initialize({ scrollMargin: 200, // Check 200px ahead in scroll direction }) ``` *** ### Hit Slop Settings[​](#hit-slop-settings "Direct link to Hit Slop Settings") #### `defaultHitSlop`[​](#defaulthitslop "Direct link to defaulthitslop") * **Type:** `number | Rect` * **Default:** `{top: 0, left: 0, right: 0, bottom: 0}` * **Clamped between:** `0-2000` pixels (per side) * **Description:** Default fully invisible "slop" around elements for all registered elements. Basically increases the hover hitbox. * **Note:** This can be overridden per element by setting its `hitSlop`. Changing `defaultHitSlop` at runtime does not replace existing elements' hit slop. Use `updateElementOptions` to change it, or `replaceElementOptions` with `hitSlop` omitted to restore the current default. ``` // Uniform hit slop ForesightManager.initialize({ defaultHitSlop: 20, // 20px on all sides }) // Custom hit slop for each side ForesightManager.initialize({ defaultHitSlop: { top: 10, left: 20, right: 20, bottom: 15, }, }) ``` *** ### Touch Device Settings[​](#touch-device-settings "Direct link to Touch Device Settings") #### `touchDeviceStrategy`[​](#touchdevicestrategy "Direct link to touchdevicestrategy") * **Type:** `TouchDeviceStrategy` * **Default:** `"onTouchStart"` * **Options:** `"none"`, `"viewport"`, `"onTouchStart"` * **Description:** Strategy to use for touch devices. Pen input uses the desktop handler with mouse, keyboard, and scroll prediction. ``` ForesightManager.initialize({ touchDeviceStrategy: "viewport", }) ``` *** ### Minimum Connection Settings[​](#minimum-connection-settings "Direct link to Minimum Connection Settings") #### `minimumConnectionType`[​](#minimumconnectiontype "Direct link to minimumconnectiontype") * **Type:** `MinimumConnectionType` * **Default:** `"3g"` * **Options:** `"slow-2g"`, `"2g"`, `"3g"`, `"4g"` * **Description:** The minimum connection speed required for prefetching. When the user's connection is slower than this threshold (or data saver is enabled), elements are still registered but stay inactive: they are excluded from prediction and never fire their callback. * **Note:** This feature relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS treats the connection as unlimited. * **Runtime behavior:** Connection limitations are checked when an element is first registered. Later connection changes or updates to `minimumConnectionType` do not recalculate existing registrations. Unregister and register again to recheck. ``` ForesightManager.initialize({ minimumConnectionType: "3g", }) ``` *** ### Other Settings[​](#other-settings "Direct link to Other Settings") #### `enableManagerLogging`[​](#enablemanagerlogging "Direct link to enablemanagerlogging") * **Type:** `boolean` * **Default:** `false` * **Description:** Logs basic information about the `ForesightManager` and its handlers that is not available through events. Mostly used by the maintainers of `ForesightJS` to debug the manager, but might be useful for implementers aswell. * **Note:** Examples of logs are: Initializing the manager, switching from device strategy (e.g. Mouse to pen), aborting controllers and invalidating cache. ``` ForesightManager.initialize({ enableManagerLogging: true, }) ``` *** ### Data Attribute Settings[​](#data-attribute-settings "Direct link to Data Attribute Settings") #### `setDataAttributes`[​](#setdataattributes "Direct link to setdataattributes") * **Type:** `boolean` * **Default:** `true` * **Description:** When `true`, the manager mirrors every registered element's prediction state onto `data-*` attributes via direct DOM mutation, so plain CSS can style predictions without any framework re-render. The attributes are kept in sync on every state change and removed when an element unregisters. Toggling this at runtime applies or removes the attributes on all currently-registered elements. * `data-predicted` — present while `isPredicted` is `true` * `data-active` — present while `isActive` is `true` (eligible to fire) * `data-callback-running` — present while `isCallbackRunning` is `true` * `data-status` — set to `"success"` or `"error"` once a callback completes ``` ForesightManager.initialize({ setDataAttributes: false, // Opt out of data-* mirroring }) ``` ``` [data-predicted] { outline: 2px solid orange; } ``` ## Runtime Configuration Changes[​](#runtime-configuration-changes "Direct link to Runtime Configuration Changes") You can update global settings at runtime using `alterGlobalSettings()`. This is mainly for developers building tools on top of `Foresight`. For regular use, set your global settings once during initialization with `ForesightManager.initialize()`. ``` // Change settings after initialization ForesightManager.instance.alterGlobalSettings({ trajectoryPredictionTime: 100, enableScrollPrediction: false, }) ``` ## Development Tools Integration[​](#development-tools-integration "Direct link to Development Tools Integration") Development Tools Visual development tools are available as a separate package, `js.foresight-devtools`. See the development tools page in the debugging section for details on installing and configuring it. The development tools provide a real-time interface for adjusting these global settings and seeing their immediate effects on prediction behavior. --- # Registration Options In Astro the integration registers links for you, so you usually don't touch these options directly. The `element` is the anchor itself and the `callback` is Astro's `prefetch()` for the link's `href`. The remaining options are set per link through [data attributes](/docs/astro/data-attributes.md), [`linkDefaults`, or `rules`](/docs/astro/integration.md#options): * `hitSlop` via `data-foresight-hit-slop` * `name` via `data-foresight-name` * `reactivateAfter` via `data-foresight-reactivate-after` * `enabled` via `data-foresight-enabled` For a custom `callback` or `meta`, register manually with [`registerForesight`](/docs/astro/register-foresight.md), which takes the full options below (minus `element`). ## Registration Parameters[​](#registration-parameters "Direct link to Registration Parameters") **TypeScript Type:** `ForesightRegisterOptions` or `ForesightRegisterOptionsWithoutElement` if you want to omit the `element` ### Required Parameters[​](#required-parameters "Direct link to Required Parameters") #### `element`[​](#element "Direct link to element") * **Type:** `element | NodeListOf` * **Required:** Yes * **Description:** The DOM element (or a `NodeList` of elements) to monitor for user interactions. When a `NodeList` is passed, all elements are registered with the same callback and settings. ``` // Single element const button = document.querySelector("#my-button") ForesightManager.instance.register({ element: button, callback: () => { /* prefetch logic */ }, }) // Multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.register({ element: navLinks, callback: state => { /* state identifies which element triggered */ }, }) ``` *** #### `callback`[​](#callback "Direct link to callback") * **Type:** `(state: ForesightElementState) => void` * **Required:** Yes * **Description:** Function that executes when interaction is predicted or occurs. This is where your prefetching logic goes. The callback receives the [`ForesightElementState`](#registration-return-value) of the element that triggered it, which is especially useful when registering multiple elements with a shared callback. * **Note:** If you await your prefetch logic the `callbackCompleted` event will show you how long your prefetch took to run. ``` ForesightManager.instance.register({ element: myElement, callback: state => { console.log(`Triggered ${state.name}`) // prefetch logic }, }) ``` *** ### Optional Parameters[​](#optional-parameters "Direct link to Optional Parameters") #### `hitSlop`[​](#hitslop "Direct link to hitslop") * **Type:** `number | Rect` * **Required:** No * **Default:** Uses `defaultHitSlop` from global settings (which is 0 if not set) * **Description:** Fully invisible "slop" around the element that increases the hover hitbox. ``` // Uniform hit slop (20px on all sides) ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: 20, }) // Custom hit slop for each side ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: { top: 10, left: 50, right: 50, bottom: 100, }, }) ``` *** #### `name`[​](#name "Direct link to name") * **Type:** `string` * **Required:** No * **Default:** `element.id` or `"unnamed"` if no id * **Description:** A descriptive name for the element, useful for development tools and debugging. ``` ForesightManager.instance.register({ element: myElement, callback: () => { /* logic */ }, name: "Product Navigation Button", // Helpful for debugging }) ``` *** #### `reactivateAfter`[​](#reactivateafter "Direct link to reactivateafter") * **Type:** `number` * **Required:** No * **Default:** `Infinity` * **Description:** Time in milliseconds after which the callback can be fired again. Set to `Infinity` to prevent callback from firing again after first execution. * **Note:** Even though `ForesightJS` doesn't store any data, you can set this to the same time as your data would become `stale` normally. In most cases you can just leave this on the default value, as it will get reset on page refresh/rerouting anyways. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, reactivateAfter: 5 * 60 * 1000, // 5 minutes }) ``` *** #### `meta`[​](#meta "Direct link to meta") * **Type:** `ForesightMeta` (defaults to `Record`) * **Required:** No * **Default:** `{}` * **Description:** Stores additional information about the registered element. Visible in all element-related events and in the devtools. Use [module augmentation](/docs/getting-started/typescript.md#typed-metadata) to define its type once for your entire app, including framework bindings. ``` ForesightManager.instance.register({ element: productLink, callback: () => {}, meta: { route: "/about", section: "main", category: "content", }, }) ``` *** #### `enabled`[​](#enabled "Direct link to enabled") * **Type:** `boolean` * **Required:** No * **Default:** `true` * **Description:** When `false` the element stays registered but inactive: it is excluded from prediction and never fires its callback. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, enabled: userWantsPrefetching, // toggle prediction without unregistering }) ``` ## Registration Return Value[​](#registration-return-value "Direct link to Registration Return Value") The `register()` method returns the full `ForesightElementState` snapshot of the element at registration time, plus five functions: `unregister()`, `subscribe(listener)`, `getSnapshot()`, `subscribeToBounds(listener)`, and `getBounds()`. When a single element is passed, it returns a `ForesightRegisterResult`. When a `NodeList` is passed, it returns `ForesightRegisterResult[]`. **TypeScript Type:** `ForesightRegisterResult` or `ForesightRegisterResult[]` ``` // Single element returns ForesightRegisterResult const { unregister, subscribe, getSnapshot, isLimitedConnection } = ForesightManager.instance.register({ element: myElement, callback: () => {}, }) // NodeList returns ForesightRegisterResult[] const results = ForesightManager.instance.register({ element: document.querySelectorAll(".my-elements"), callback: () => {}, }) ``` *** ### Returned Functions[​](#returned-functions "Direct link to Returned Functions") #### `unregister()`[​](#unregister "Direct link to unregister") Removes the element from `ForesightManager`'s tracking. Call this when you permanently remove the element from the DOM, see [unregister](/docs/debugging/static-properties.md#foresightmanagerinstanceunregisterelement-reason). #### `subscribe(listener)`[​](#subscribelistener "Direct link to subscribelistener") Subscribes to logical state changes for this element. The listener is called every time the state snapshot is replaced - never for geometry-only changes during scroll/resize, which live on the separate bounds channel (`subscribeToBounds`). Returns an unsubscribe function. Together with `getSnapshot` this makes the state usable as an external store without any scroll-induced re-renders. #### `getSnapshot()`[​](#getsnapshot "Direct link to getsnapshot") Returns the current immutable `ForesightElementState` snapshot for this element. The snapshot reference is replaced (never mutated) on every logical change, so the properties spread on the return value itself are only current as of registration; use `getSnapshot()` (optionally with `subscribe`) for the up-to-date state. #### `subscribeToBounds(listener)`[​](#subscribetoboundslistener "Direct link to subscribetoboundslistener") Subscribes to geometry changes for this element. The listener is called every time the element's position or size changes, which is every scroll/resize tick while the element is visible. Returns an unsubscribe function. #### `getBounds()`[​](#getbounds "Direct link to getbounds") Returns the current immutable `ElementBounds` snapshot for this element: its measured `originalRect` and the hitSlop-expanded `expandedRect` used for interaction detection. *** ### State Fields[​](#state-fields "Direct link to State Fields") The `ForesightElementState` fields, also passed to your [`callback`](#callback): | Field | Type | Description | | ---------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | Unique identifier assigned during registration. | | `name` | `string` | Human-readable name for debugging, set via the [`name`](#name) option. | | `meta` | `ForesightMeta` | Arbitrary user-supplied metadata, set via the [`meta`](#meta) option. | | `hitSlop` | `Exclude` | The normalized hit slop applied to this element, set via the [`hitSlop`](#hitslop) option. The element's rects are not part of the state, read them via `getBounds()`. | | `isRegistered` | `boolean` | Whether the element is currently tracked by the manager. Stays `true` from registration until it is explicitly unregistered. Detaching the element from the DOM does **not** unregister it: it is parked (kept registered but inactive) and resumes when it reconnects. | | `isActive` | `boolean` | Whether the element is currently eligible to fire its callback. `false` when the element is disabled (`isEnabled: false`), on a limited connection, or parked (`isParked: true`). | | `isParked` | `boolean` | `true` while the element is detached from the DOM and parked: kept registered but inactive. Becomes `false` again when the element reconnects and resumes. | | `isEnabled` | `boolean` | Whether prediction is enabled for this element, set via the [`enabled`](#enabled) option. When `false` the element stays registered but inactive. | | `isPredicted` | `boolean` | `true` once the element's callback has been triggered by a prediction hit. Stays `true` until the element is reactivated or unregistered. | | `isCallbackRunning` | `boolean` | `true` while the callback is executing (between invocation and completion). The callback is awaited, so this stays `true` for async callbacks until they resolve or reject. | | `isLimitedConnection` | `boolean` | `true` when the user's connection is slower than the configured `minimumConnectionType` global setting (defaults to `"3g"`) or data saver is enabled. The element is still registered, but stays inactive so no data is consumed. | | `isIntersectingWithViewport` | `boolean` | Whether the element is currently intersecting the viewport. | | `hitCount` | `number` | Number of times the callback has fired for this element. | | `registerCount` | `number` | Number of times this element has been (re)registered. | | `durationMs` | `number \| undefined` | Duration in ms of the most recent callback run. | | `status` | `"success" \| "error" \| undefined` | Status of the most recently completed callback. | | `error` | `string \| null` | Error message from the most recently completed callback. | | `reactivateAfter` | `number` | Time in ms after which the callback can be fired again (`Infinity` = never), set via the [`reactivateAfter`](#reactivateafter) option. | note The `isLimitedConnection` detection relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS treats the connection as unlimited. --- # Data Attributes Individual links are configured with `data-foresight-*` attributes, the same way Astro's own prefetch uses `data-astro-prefetch`. Data attributes win over [`rules`](/docs/astro/integration.md#rules), which win over [`linkDefaults`](/docs/astro/integration.md#linkdefaults). ``` Pricing ``` If you prefer typed props over attributes, use the [``](/docs/astro/foresight-link.md) component, which renders exactly this. ## Available options[​](#available-options "Direct link to Available options") These map to the core [registration options](/docs/astro/configuration/registration-options.md) of the same name: | Attribute | Type | Description | | --------------------------------- | ---------------- | ---------------------------------------------------------------------- | | `data-foresight-hit-slop` | margin shorthand | Invisible margin around the link that expands its hit area | | `data-foresight-name` | string | Debug name shown in the [devtools](/docs/astro/devtools.md) and events | | `data-foresight-reactivate-after` | number (ms) | Time after which the link can trigger again | | `data-foresight-enabled` | boolean | Set to `"false"` to keep the link registered but inactive | `data-foresight-hit-slop` uses CSS margin shorthand with 1 to 4 values (top right bottom left): ``` … … … ``` The `callback` is always Astro's `prefetch()` for the link's `href`. For a custom callback or `meta` (options that can't be serialized into attributes), use [`registerForesight`](/docs/astro/register-foresight.md). --- # Development Tools In Astro the easiest way to enable the devtools is the integration's [`devtools`](/docs/astro/integration.md#devtools) option, which loads them automatically during `astro dev` and keeps them out of production builds: ``` // astro.config.mjs foresight({ devtools: true, }) ``` You still need to install the `js.foresight-devtools` package yourself (see below). If you want to customize the devtools options, skip the integration option and initialize them from a ` ``` The integration initializes the manager before your page scripts run, so `ForesightManager.instance` is safe to use directly. ## Available Events[​](#available-events "Direct link to Available Events") ### Interaction Events[​](#interaction-events "Direct link to Interaction Events") Events fired when user interactions trigger callbacks. *** #### `callbackInvoked`[​](#callbackinvoked "Direct link to callbackinvoked") Fired **before** an element's callback is executed ``` type CallbackInvokedEvent = { type: "callbackInvoked" timestamp: number element: ForesightElement state: ForesightElementState hitType: CallbackHitType } ``` **Related Types:** [`CallbackHitType`](/docs/getting-started/typescript.md#callbackhittype) • [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** #### `callbackCompleted`[​](#callbackcompleted "Direct link to callbackcompleted") Fired **after** an element's callback is executed ``` type CallbackCompletedEvent = { type: "callbackCompleted" timestamp: number element: ForesightElement state: ForesightElementState hitType: CallbackHitType elapsed: number // Time between callbackInvoked and callbackCompleted status: "success" | "error" | undefined errorMessage: string | null wasLastActiveElement: boolean } ``` **Related Types:** [`CallbackHitType`](/docs/getting-started/typescript.md#callbackhittype) • [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) ### Element Lifecycle Events[​](#element-lifecycle-events "Direct link to Element Lifecycle Events") Events fired during element registration, updates, and cleanup. *** #### `elementRegistered`[​](#elementregistered "Direct link to elementregistered") Fired when a new element is registered with `ForesightManager` using `ForesightManager.instance.register({ element, callback })`. Registering an already-tracked element updates its options without emitting another `elementRegistered` event. ``` type ElementRegisteredEvent = { type: "elementRegistered" timestamp: number element: ForesightElement state: ForesightElementState } ``` **Related Types:** [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** #### `elementUnregistered`[​](#elementunregistered "Direct link to elementunregistered") Fired when an element is removed from `ForesightManager`'s tracking via `ForesightManager.instance.unregister(element)`. Detaching an element from the DOM no longer unregisters it. It is parked (kept registered but inactive) and resumed on reattach, so no `elementUnregistered` event fires for that. The `"disconnected"` reason is therefore no longer emitted. ``` type ElementUnregisteredEvent = { type: "elementUnregistered" timestamp: number element: ForesightElement state: ForesightElementState unregisterReason: "disconnected" | "apiCall" | "devtools" | (string & {}) wasLastRegisteredElement: boolean } ``` **Related Types:** [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** ### Prediction Events[​](#prediction-events "Direct link to Prediction Events") Events fired during movement prediction calculations. *** #### `mouseTrajectoryUpdate`[​](#mousetrajectoryupdate "Direct link to mousetrajectoryupdate") Fired during mouse movement for trajectory calculations ``` type MouseTrajectoryUpdateEvent = { type: "mouseTrajectoryUpdate" trajectoryPositions: { positions: CircularBuffer // mouse position history currentPoint: { x: number; y: number } predictedPoint: { x: number; y: number } } predictionEnabled: boolean } ``` *** #### `scrollTrajectoryUpdate`[​](#scrolltrajectoryupdate "Direct link to scrolltrajectoryupdate") Fired during scroll events when scroll prediction is active ``` type ScrollTrajectoryUpdateEvent = { type: "scrollTrajectoryUpdate" currentPoint: Point // { x: number; y: number } predictedPoint: Point // { x: number; y: number } scrollDirection: ScrollDirection // "down" | "up" | "left" | "right" } ``` *** ### Configuration Events[​](#configuration-events "Direct link to Configuration Events") Events fired when ForesightManager configuration changes. *** #### `managerSettingsChanged`[​](#managersettingschanged "Direct link to managersettingschanged") Fired when global `ForesightManager` settings are updated via the devtools or via `ForesightManager.instance.alterGlobalSettings()`. ``` type ManagerSettingsChangedEvent = { type: "managerSettingsChanged" timestamp: number managerData: Readonly updatedSettings: UpdatedManagerSetting[] } ``` **Related Types:** [`ForesightManagerData`](/docs/getting-started/typescript.md#foresightmanagerdata) *** #### `deviceStrategyChanged`[​](#devicestrategychanged "Direct link to devicestrategychanged") Fired when user switches between input methods (mouse, touch, or pen). ``` type DeviceStrategyChangedEvent = { type: "deviceStrategyChanged" timestamp: number newStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" oldStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" } ``` *** --- # ForesightLink `` is typed sugar over the [data attributes](/docs/astro/data-attributes.md). It renders a plain `` with `data-astro-prefetch="foresight"` and the `data-foresight-*` attributes filled in from its props: ``` --- import { ForesightLink } from "@foresightjs/astro/components" --- Pricing ``` ## Props[​](#props "Direct link to Props") Besides all regular `` attributes it accepts the options from [`ForesightLinkOptions`](/docs/astro/typescript.md): | Prop | Type | Description | | ----------------- | ---------------- | ----------------------------------------------------------- | | `hitSlop` | `number \| Rect` | Invisible margin around the link that expands its hit area | | `name` | `string` | Debug name shown in the [devtools](/docs/astro/devtools.md) | | `reactivateAfter` | `number` | Time in ms after which the link can trigger again | | `enabled` | `boolean` | Set to `false` to keep the link registered but inactive | ``` Checkout ``` Since it renders a regular anchor there is no client-side JavaScript involved, the injected foresight script picks the link up like any other. --- # Initialize the Manager Optional Step This step is **not needed** if you don't want to configure ForesightJS. The integration initializes the manager automatically with default settings on page load. If you don't want to configure anything, skip to [Quick Start](/docs/astro/quick-start.md). In Astro you don't call `ForesightManager.initialize()` yourself. The integration does it in the script it injects into every page. To customize global behavior, pass the same settings object through the `manager` option: ``` // astro.config.mjs import { defineConfig } from "astro/config" import foresight from "@foresightjs/astro" export default defineConfig({ integrations: [ foresight({ manager: { defaultHitSlop: 20, touchDeviceStrategy: "viewport", }, }), ], }) ``` ## Optional Settings[​](#optional-settings "Direct link to Optional Settings") `manager` accepts any of these [global settings](/docs/astro/configuration/global-settings.md). ``` ForesightManager.initialize({ // Mouse prediction settings enableMousePrediction: true, // Enable/disable mouse trajectory prediction trajectoryPredictionTime: 120, // How far ahead to predict (10-200ms) positionHistorySize: 8, // Mouse positions to track (2-30) // Keyboard prediction settings enableTabPrediction: true, // Enable/disable keyboard navigation prediction tabOffset: 2, // Tab stops ahead to trigger (0-20) // Scroll prediction settings enableScrollPrediction: true, // Enable/disable scroll prediction scrollMargin: 150, // Pixel distance for scroll detection (30-300) // Touch device settings touchDeviceStrategy: "viewport", // "none", "viewport", or "onTouchStart" // Connection settings minimumConnectionType: "3g", // "slow-2g", "2g", "3g", "4g" // Default element settings defaultHitSlop: 0, // Default hit slop for all elements (number or {top, right, bottom, left}) }) ``` ## Good to know[​](#good-to-know "Direct link to Good to know") The `ForesightManager` is a singleton, meaning it can only be initialized once. If you call `ForesightManager.initialize()` multiple times, only the first call will take effect. Any subsequent calls will simply return the already initialized instance. The manager also provides several static methods you can use to inspect registered elements, callback counts, and [event](/docs/astro/events.md) listeners. You can find a list of these methods [here](/docs/astro/static-properties.md). --- # Installation [![npm version](https://img.shields.io/npm/v/@foresightjs/astro.svg)](https://www.npmjs.com/package/@foresightjs/astro) [![npm downloads](https://img.shields.io/npm/dt/@foresightjs/astro.svg)](https://www.npmjs.com/package/@foresightjs/astro) `@foresightjs/astro` is the official Astro integration for ForesightJS. Unlike the React, Vue, and Angular packages it is not a set of component bindings: you add [`foresight()`](/docs/astro/integration.md) to your Astro config once and it registers your links automatically, prefetching them through Astro's own prefetch pipeline when intent is predicted. ``` npm install @foresightjs/astro js.foresight # or pnpm add @foresightjs/astro js.foresight # or yarn add @foresightjs/astro js.foresight ``` ``` // astro.config.mjs import { defineConfig } from "astro/config" import foresight from "@foresightjs/astro" export default defineConfig({ integrations: [foresight()], }) ``` ## Configuring the manager[​](#configuring-the-manager "Direct link to Configuring the manager") You don't have to configure anything. The manager auto-initializes with sensible defaults when the first page loads. If you want to tweak global behavior, pass a `manager` object to the integration. See [Initialize the Manager](/docs/astro/initialize-the-manager.md) and [Global Settings](/docs/astro/configuration/global-settings.md). ## Devtools[​](#devtools "Direct link to Devtools") The visual [Development Tools](/docs/astro/devtools.md) live in their own package (`js.foresight-devtools`). Install it and set `devtools: true` on the integration to load them automatically during `astro dev`. --- # foresight() `foresight()` is the integration you add to `astro.config.mjs`. It is configured exactly like Astro's [built-in prefetch](https://docs.astro.build/en/guides/prefetch/) and composes with it: the four native strategies keep working, foresight becomes the fifth. ## Options[​](#options "Direct link to Options") ``` foresight({ // mirror Astro's prefetch config: native values are forwarded to Astro, // "foresight" hands unattributed links to foresight instead prefetchAll: true, defaultStrategy: "foresight", // forwarded to ForesightManager.initialize manager: { trajectoryPredictionTime: 120, defaultHitSlop: 0, touchDeviceStrategy: "viewport", }, // base options for every registered link linkDefaults: { hitSlop: 10 }, // selector-based overrides, later rules win rules: [ { selector: "nav a", hitSlop: 30 }, { selector: "footer a", enabled: false }, ], // load js.foresight-devtools during `astro dev` (install it separately) devtools: true, }) ``` ### `prefetchAll`[​](#prefetchall "Direct link to prefetchall") * **Type:** `boolean` Mirrors Astro's `prefetch.prefetchAll`: also prefetch links without a `data-astro-prefetch` attribute, using `defaultStrategy`. Without it, unattributed links are not prefetched at all, exactly like Astro's own config. ### `defaultStrategy`[​](#defaultstrategy "Direct link to defaultstrategy") * **Type:** `"tap" | "hover" | "viewport" | "load" | "foresight"` Strategy for links covered by `prefetchAll`. Native values (`tap`, `hover`, `viewport`, `load`) are forwarded to Astro's `prefetch.defaultStrategy`. `"foresight"` makes foresight register those links itself. Links with a native or bare `data-astro-prefetch` attribute always stay with Astro's own script. ### `manager`[​](#manager "Direct link to manager") * **Type:** `Partial` [Global settings](/docs/astro/configuration/global-settings.md) forwarded to `ForesightManager.initialize`. See [Initialize the Manager](/docs/astro/initialize-the-manager.md). ### `linkDefaults`[​](#linkdefaults "Direct link to linkdefaults") * **Type:** `ForesightLinkOptions` Base link options applied to every link the integration registers: ``` type ForesightLinkOptions = { hitSlop?: HitSlop name?: string reactivateAfter?: number enabled?: boolean } ``` These are the same options the [data attributes](/docs/astro/data-attributes.md#available-options) set per link. ### `rules`[​](#rules "Direct link to rules") * **Type:** `ForesightRule[]` Selector-based overrides on top of `linkDefaults`. A rule is `ForesightLinkOptions` plus the CSS `selector` it applies to: ``` type ForesightRule = ForesightLinkOptions & { selector: string } ``` Rules are applied in declaration order, later rules win, and [`data-foresight-*` attributes](/docs/astro/data-attributes.md) win over all rules. ### `devtools`[​](#devtools "Direct link to devtools") * **Type:** `boolean` Loads the [Development Tools](/docs/astro/devtools.md) during `astro dev`. The `js.foresight-devtools` package is an optional peer dependency, install it separately. ## Which links get registered[​](#which-links-get-registered "Direct link to Which links get registered") A link is handled by foresight when either: * it has an explicit `data-astro-prefetch="foresight"` attribute, or * it has no `data-astro-prefetch` attribute at all and you configured `prefetchAll: true` with `defaultStrategy: "foresight"`. Only same-origin links are registered. When a registered link's callback fires, the page is fetched through Astro's own `prefetch()` from `astro:prefetch`, so slow-connection detection, deduplication, and the Speculation Rules upgrade (`experimental.clientPrerender`) all apply. ## How it composes with Astro's prefetch[​](#how-it-composes-with-astros-prefetch "Direct link to How it composes with Astro's prefetch") * The four native strategies and the `astro:prefetch` module keep working. Native `prefetchAll` and `defaultStrategy` values are forwarded to Astro's own prefetch config. * On connections Astro considers slow, or in data-saver mode, links with an explicit `data-astro-prefetch="foresight"` attribute fall back to Astro's `tap` strategy. Unattributed links handled by `prefetchAll: true` and `defaultStrategy: "foresight"` stay inactive without a tap fallback. * Links are re-scanned after view transitions and when anchors are injected or removed (server islands), so dynamically added links register automatically. --- # Quick Start This guide walks through prefetching your first links with ForesightJS in Astro. ## Basic Usage Example[​](#basic-usage-example "Direct link to Basic Usage Example") Add the integration to your Astro config. With `prefetchAll: true` and `defaultStrategy: "foresight"`, every internal link is prefetched when intent is predicted, no changes to your markup needed: ``` // astro.config.mjs import { defineConfig } from "astro/config" import foresight from "@foresightjs/astro" export default defineConfig({ integrations: [ foresight({ prefetchAll: true, defaultStrategy: "foresight", }), ], }) ``` Individual links can still use Astro's native strategies or opt out entirely: ``` About Docs Big page ``` ## Opt-in per link[​](#opt-in-per-link "Direct link to Opt-in per link") Without `defaultStrategy: "foresight"`, only links that opt in explicitly are handled by foresight: ``` Pricing ``` Astro's four native strategies (`tap`, `hover`, `viewport`, `load`) keep working exactly as before. See the [`foresight()` options](/docs/astro/integration.md) for how the two configs compose. ## Tuning individual links[​](#tuning-individual-links "Direct link to Tuning individual links") Registration options are set per link with [data attributes](/docs/astro/data-attributes.md), or with the typed [``](/docs/astro/foresight-link.md) component: ``` Pricing ``` For custom callbacks instead of page prefetching, use [`registerForesight`](/docs/astro/register-foresight.md). ## Development Tools[​](#development-tools "Direct link to Development Tools") ForesightJS has dedicated [Development Tools](/docs/astro/devtools.md) that help you understand and tune how prediction is working in your application. Install the package and enable the integration's `devtools` option to load them during `astro dev`: ``` pnpm add -D js.foresight-devtools # or npm install -D js.foresight-devtools # or yarn add -D js.foresight-devtools ``` ``` foresight({ devtools: true, }) ``` --- # registerForesight The integration only handles link prefetching. For everything else (custom callbacks, non-link elements, `meta`) use `registerForesight` from the client entry point inside a ` ``` It is a thin wrapper over `ForesightManager.instance.register` that accepts a CSS selector or an element: ``` registerForesight(target: string | Element, options: ForesightRegisterOptionsWithoutElement): ForesightRegisterResult[] ``` A selector registers every matching element with the same options. The options are the regular [registration options](/docs/astro/configuration/registration-options.md), `callback` is the only required field. Each returned [`ForesightRegisterResult`](/docs/astro/configuration/registration-options.md#registration-return-value) contains the element's state snapshot plus `unregister`, `subscribe`, and `getSnapshot`. note Elements registered this way are not managed by the integration, so unlike links they are not automatically re-registered after view transitions. Re-run your script on `astro:page-load` if the element is part of a page that gets swapped. --- # Static Properties The integration handles registering and unregistering links for you, the properties below are escape hatches for debugging and inspecting the manager state from a ` ``` The directive takes the same options object: ``` ``` When options change, both APIs update the existing registration in place, resetting omitted fields to their defaults. The directive requires a new binding reference, while the composable also tracks in-place mutations of reactive options. ## Registration Parameters[​](#registration-parameters "Direct link to Registration Parameters") **TypeScript Type:** `ForesightRegisterOptions` or `ForesightRegisterOptionsWithoutElement` if you want to omit the `element` ### Required Parameters[​](#required-parameters "Direct link to Required Parameters") #### `element`[​](#element "Direct link to element") * **Type:** `element | NodeListOf` * **Required:** Yes * **Description:** The DOM element (or a `NodeList` of elements) to monitor for user interactions. When a `NodeList` is passed, all elements are registered with the same callback and settings. ``` // Single element const button = document.querySelector("#my-button") ForesightManager.instance.register({ element: button, callback: () => { /* prefetch logic */ }, }) // Multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.register({ element: navLinks, callback: state => { /* state identifies which element triggered */ }, }) ``` *** #### `callback`[​](#callback "Direct link to callback") * **Type:** `(state: ForesightElementState) => void` * **Required:** Yes * **Description:** Function that executes when interaction is predicted or occurs. This is where your prefetching logic goes. The callback receives the [`ForesightElementState`](#registration-return-value) of the element that triggered it, which is especially useful when registering multiple elements with a shared callback. * **Note:** If you await your prefetch logic the `callbackCompleted` event will show you how long your prefetch took to run. ``` ForesightManager.instance.register({ element: myElement, callback: state => { console.log(`Triggered ${state.name}`) // prefetch logic }, }) ``` *** ### Optional Parameters[​](#optional-parameters "Direct link to Optional Parameters") #### `hitSlop`[​](#hitslop "Direct link to hitslop") * **Type:** `number | Rect` * **Required:** No * **Default:** Uses `defaultHitSlop` from global settings (which is 0 if not set) * **Description:** Fully invisible "slop" around the element that increases the hover hitbox. ``` // Uniform hit slop (20px on all sides) ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: 20, }) // Custom hit slop for each side ForesightManager.instance.register({ element: myElement, callback: () => {}, hitSlop: { top: 10, left: 50, right: 50, bottom: 100, }, }) ``` *** #### `name`[​](#name "Direct link to name") * **Type:** `string` * **Required:** No * **Default:** `element.id` or `"unnamed"` if no id * **Description:** A descriptive name for the element, useful for development tools and debugging. ``` ForesightManager.instance.register({ element: myElement, callback: () => { /* logic */ }, name: "Product Navigation Button", // Helpful for debugging }) ``` *** #### `reactivateAfter`[​](#reactivateafter "Direct link to reactivateafter") * **Type:** `number` * **Required:** No * **Default:** `Infinity` * **Description:** Time in milliseconds after which the callback can be fired again. Set to `Infinity` to prevent callback from firing again after first execution. * **Note:** Even though `ForesightJS` doesn't store any data, you can set this to the same time as your data would become `stale` normally. In most cases you can just leave this on the default value, as it will get reset on page refresh/rerouting anyways. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, reactivateAfter: 5 * 60 * 1000, // 5 minutes }) ``` *** #### `meta`[​](#meta "Direct link to meta") * **Type:** `ForesightMeta` (defaults to `Record`) * **Required:** No * **Default:** `{}` * **Description:** Stores additional information about the registered element. Visible in all element-related events and in the devtools. Use [module augmentation](/docs/getting-started/typescript.md#typed-metadata) to define its type once for your entire app, including framework bindings. ``` ForesightManager.instance.register({ element: productLink, callback: () => {}, meta: { route: "/about", section: "main", category: "content", }, }) ``` *** #### `enabled`[​](#enabled "Direct link to enabled") * **Type:** `boolean` * **Required:** No * **Default:** `true` * **Description:** When `false` the element stays registered but inactive: it is excluded from prediction and never fires its callback. ``` ForesightManager.instance.register({ element: myElement, callback: () => {}, enabled: userWantsPrefetching, // toggle prediction without unregistering }) ``` ## Registration Return Value[​](#registration-return-value "Direct link to Registration Return Value") The `register()` method returns the full `ForesightElementState` snapshot of the element at registration time, plus five functions: `unregister()`, `subscribe(listener)`, `getSnapshot()`, `subscribeToBounds(listener)`, and `getBounds()`. When a single element is passed, it returns a `ForesightRegisterResult`. When a `NodeList` is passed, it returns `ForesightRegisterResult[]`. **TypeScript Type:** `ForesightRegisterResult` or `ForesightRegisterResult[]` ``` // Single element returns ForesightRegisterResult const { unregister, subscribe, getSnapshot, isLimitedConnection } = ForesightManager.instance.register({ element: myElement, callback: () => {}, }) // NodeList returns ForesightRegisterResult[] const results = ForesightManager.instance.register({ element: document.querySelectorAll(".my-elements"), callback: () => {}, }) ``` *** ### Returned Functions[​](#returned-functions "Direct link to Returned Functions") #### `unregister()`[​](#unregister "Direct link to unregister") Removes the element from `ForesightManager`'s tracking. Call this when you permanently remove the element from the DOM, see [unregister](/docs/debugging/static-properties.md#foresightmanagerinstanceunregisterelement-reason). #### `subscribe(listener)`[​](#subscribelistener "Direct link to subscribelistener") Subscribes to logical state changes for this element. The listener is called every time the state snapshot is replaced - never for geometry-only changes during scroll/resize, which live on the separate bounds channel (`subscribeToBounds`). Returns an unsubscribe function. Together with `getSnapshot` this makes the state usable as an external store without any scroll-induced re-renders. #### `getSnapshot()`[​](#getsnapshot "Direct link to getsnapshot") Returns the current immutable `ForesightElementState` snapshot for this element. The snapshot reference is replaced (never mutated) on every logical change, so the properties spread on the return value itself are only current as of registration; use `getSnapshot()` (optionally with `subscribe`) for the up-to-date state. #### `subscribeToBounds(listener)`[​](#subscribetoboundslistener "Direct link to subscribetoboundslistener") Subscribes to geometry changes for this element. The listener is called every time the element's position or size changes, which is every scroll/resize tick while the element is visible. Returns an unsubscribe function. #### `getBounds()`[​](#getbounds "Direct link to getbounds") Returns the current immutable `ElementBounds` snapshot for this element: its measured `originalRect` and the hitSlop-expanded `expandedRect` used for interaction detection. *** ### State Fields[​](#state-fields "Direct link to State Fields") The `ForesightElementState` fields, also passed to your [`callback`](#callback): | Field | Type | Description | | ---------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | Unique identifier assigned during registration. | | `name` | `string` | Human-readable name for debugging, set via the [`name`](#name) option. | | `meta` | `ForesightMeta` | Arbitrary user-supplied metadata, set via the [`meta`](#meta) option. | | `hitSlop` | `Exclude` | The normalized hit slop applied to this element, set via the [`hitSlop`](#hitslop) option. The element's rects are not part of the state, read them via `getBounds()`. | | `isRegistered` | `boolean` | Whether the element is currently tracked by the manager. Stays `true` from registration until it is explicitly unregistered. Detaching the element from the DOM does **not** unregister it: it is parked (kept registered but inactive) and resumes when it reconnects. | | `isActive` | `boolean` | Whether the element is currently eligible to fire its callback. `false` when the element is disabled (`isEnabled: false`), on a limited connection, or parked (`isParked: true`). | | `isParked` | `boolean` | `true` while the element is detached from the DOM and parked: kept registered but inactive. Becomes `false` again when the element reconnects and resumes. | | `isEnabled` | `boolean` | Whether prediction is enabled for this element, set via the [`enabled`](#enabled) option. When `false` the element stays registered but inactive. | | `isPredicted` | `boolean` | `true` once the element's callback has been triggered by a prediction hit. Stays `true` until the element is reactivated or unregistered. | | `isCallbackRunning` | `boolean` | `true` while the callback is executing (between invocation and completion). The callback is awaited, so this stays `true` for async callbacks until they resolve or reject. | | `isLimitedConnection` | `boolean` | `true` when the user's connection is slower than the configured `minimumConnectionType` global setting (defaults to `"3g"`) or data saver is enabled. The element is still registered, but stays inactive so no data is consumed. | | `isIntersectingWithViewport` | `boolean` | Whether the element is currently intersecting the viewport. | | `hitCount` | `number` | Number of times the callback has fired for this element. | | `registerCount` | `number` | Number of times this element has been (re)registered. | | `durationMs` | `number \| undefined` | Duration in ms of the most recent callback run. | | `status` | `"success" \| "error" \| undefined` | Status of the most recently completed callback. | | `error` | `string \| null` | Error message from the most recently completed callback. | | `reactivateAfter` | `number` | Time in ms after which the callback can be fired again (`Infinity` = never), set via the [`reactivateAfter`](#reactivateafter) option. | note The `isLimitedConnection` detection relies on the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation) which is not supported in all major browsers. When the API is not available, ForesightJS treats the connection as unlimited. --- # Development Tools The `ForesightJS` development tools are framework agnostic and work the same in Vue as anywhere else; initialize them once next to your `ForesightManager.initialize` call, typically in `main.ts`. [![npm version](https://img.shields.io/npm/v/js.foresight-devtools.svg)](https://www.npmjs.com/package/js.foresight-devtools) [![npm downloads](https://img.shields.io/npm/dt/js.foresight-devtools.svg)](https://www.npmjs.com/package/js.foresight-devtools) `ForesightJS` offers dedicated [Development Tools](https://github.com/spaansba/ForesightJS/tree/main/packages/js.foresight-devtools), written in [Lit](https://lit.dev/), to help you better understand and fine-tune how `ForesightJS` works within your application. You can see the development tools in action on the [playground page](https://foresightjs.com/#playground), which includes visual trajectory indicators, element boundaries, and a control panel in the bottom-right corner. They are built entirely on `ForesightJS`'s built-in events, so they double as a working example of what you can build on the same event system. ## Installation[​](#installation "Direct link to Installation") To install the `ForesightJS` Development Tools package, use your preferred package manager: ``` pnpm add -D js.foresight-devtools # or npm install -D js.foresight-devtools # or yarn add -D js.foresight-devtools ``` ## Enabling Development Tools[​](#enabling-development-tools "Direct link to Enabling Development Tools") ``` import { ForesightManager } from "js.foresight" import { ForesightDevtools } from "js.foresight-devtools" // Initialize ForesightJS ForesightManager.initialize({}) // Initialize the development tools (all options are optional) ForesightDevtools.initialize({ show: { controlPanel: true, // the floating control panel nameTags: true, // the element name above each registered element elementOverlays: true, // the hit-slop boundary around each registered element mouseTrajectory: true, // the predicted mouse trajectory line scrollTrajectory: true, // the predicted scroll trajectory line }, isControlPanelDefaultMinimized: false, // start with the control panel minimized sortElementList: "visibility", // "visibility" | "documentOrder" | "insertionOrder" logging: { logLocation: "controlPanel", // "controlPanel" | "console" | "both" | "none" callbackCompleted: true, callbackInvoked: true, elementRegistered: false, elementUnregistered: false, managerSettingsChanged: true, mouseTrajectoryUpdate: false, // too noisy to log scrollTrajectoryUpdate: false, // too noisy to log deviceStrategyChanged: true, }, }) ``` ## Development Tools Features[​](#development-tools-features "Direct link to Development Tools Features") Once enabled, the `ForesightJS` Development Tools add several visual layers to your application, including mouse and scroll trajectories and element hitboxes. A control panel also appears in the bottom-right corner of the screen. ### Control Panel[​](#control-panel "Direct link to Control Panel") The control panel has three tabs: Settings, Elements, and Log. #### Settings Tab[​](#settings-tab "Direct link to Settings Tab") The Settings tab provides controls for mouse, keyboard, scroll, touch, and connection settings. Configure `defaultHitSlop`, `setDataAttributes`, and `enableManagerLogging` through the manager API. Changes made through the panel update the `ForesightManager` configuration. #### Elements Tab[​](#elements-tab "Direct link to Elements Tab") The Elements tab shows every element currently registered with the `ForesightManager`, grouped into **Active** and **Inactive** sections. Each element entry shows its current status through a color-coded indicator: * 🟢 **Green** - Active visible elements in desktop mode * ⚫ **Grey** - Active invisible elements in desktop mode * 🟣 **Purple** - Active elements while in touch device mode (all elements, we dont track visibility in this mode) * 🟡 **Yellow** - Elements which callbacks are currently executing * 🔘 **Light Gray** - Inactive elements Inactive elements also carry one or more **reason badges** explaining *why* they are inactive (an element can have several at once, e.g. `disabled` while `detached`): * `disabled` - turned off via the `enabled` option * `parked` - removed from the DOM (`isParked: true`); the element stays registered but inactive and resumes when it reconnects * `limited` - on a limited connection (data saver / slow network) * `fired` - already ran its callback and is waiting on `reactivateAfter` (with the default `Infinity` it stays fired) Each element can also be expanded to reveal its [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) information including settings, callback status, and metadata. For `fired` elements with a finite `reactivateAfter`, a countdown timer appears for their reactivation cooldown period; clicking this timer will instantly reactivate the element. #### Log Tab[​](#log-tab "Direct link to Log Tab") The Log tab displays real-time events emitted by `ForesightJS`. You can see callback execution times, the full element's lifecycle and other system events. Events can be filtered through the devtools initialization configuration or in the control panel itself. You can also print out the complete [`ForesightManager.instance.getManagerData`](/docs/debugging/static-properties.md#foresightmanagerinstancegetmanagerdata) state without having to call it from your code. caution Avoid logging frequently emitted events to the browser console, as it can noticeably slow down your development environment. Use the control panel for this instead. note Element overlay visualization and visibility sorting in the control panel only work with desktop/mouse prediction strategies. When debugging `touchDeviceStrategy` configurations, these features are not available as touch strategies don't track the same positioning data. --- # v-foresight `v-foresight` is the simplest way to add prefetching to a Vue app. Put it on an element, give it a callback (or an options object), and it registers and unregisters the element for you. Reach for the [`useForesight`](/docs/vue/useForesight.md) composable instead when you need the element's prediction state. ## Register the directive[​](#register-the-directive "Direct link to Register the directive") Register it globally once in your `main.ts`: ``` import { createApp } from "vue" import { vForesight } from "@foresightjs/vue" import App from "./App.vue" createApp(App).directive("foresight", vForesight).mount("#app") ``` Or import it into a single component and use it locally: ``` ``` ## Usage[​](#usage "Direct link to Usage") The directive takes either a callback function or a full options object. ### Callback shorthand[​](#callback-shorthand "Direct link to Callback shorthand") ``` ``` The callback receives the element's [`ForesightElementState`](/docs/vue/configuration/registration-options.md#state-fields). ### With options[​](#with-options "Direct link to With options") For more control, pass any [registration options](/docs/vue/configuration/registration-options.md): ``` ``` When the bound value changes reference, the directive updates the existing registration in place. Each options object describes the full configuration, so omitted fields reset to their defaults. Replace the options object when updating it, as in-place mutations of the same object are skipped. --- # Events ForesightManager emits events for element registration, prediction, and callback execution. The [ForesightJS DevTools](/docs/vue/devtools.md) use them for visual debugging, and you can listen to them yourself for telemetry, analytics, or counters. ## Usage[​](#usage "Direct link to Usage") In Vue the easiest way to listen to events is the [`useForesightEvent`](/docs/vue/useForesightEvent.md) composable, which adds and removes the listener for you: ``` ``` The standard `ForesightManager.instance.addEventListener` / `removeEventListener` pattern is also available if you want to listen outside of a component. ## Available Events[​](#available-events "Direct link to Available Events") ### Interaction Events[​](#interaction-events "Direct link to Interaction Events") Events fired when user interactions trigger callbacks. *** #### `callbackInvoked`[​](#callbackinvoked "Direct link to callbackinvoked") Fired **before** an element's callback is executed ``` type CallbackInvokedEvent = { type: "callbackInvoked" timestamp: number element: ForesightElement state: ForesightElementState hitType: CallbackHitType } ``` **Related Types:** [`CallbackHitType`](/docs/getting-started/typescript.md#callbackhittype) • [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** #### `callbackCompleted`[​](#callbackcompleted "Direct link to callbackcompleted") Fired **after** an element's callback is executed ``` type CallbackCompletedEvent = { type: "callbackCompleted" timestamp: number element: ForesightElement state: ForesightElementState hitType: CallbackHitType elapsed: number // Time between callbackInvoked and callbackCompleted status: "success" | "error" | undefined errorMessage: string | null wasLastActiveElement: boolean } ``` **Related Types:** [`CallbackHitType`](/docs/getting-started/typescript.md#callbackhittype) • [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) ### Element Lifecycle Events[​](#element-lifecycle-events "Direct link to Element Lifecycle Events") Events fired during element registration, updates, and cleanup. *** #### `elementRegistered`[​](#elementregistered "Direct link to elementregistered") Fired when a new element is registered with `ForesightManager` using `ForesightManager.instance.register({ element, callback })`. Registering an already-tracked element updates its options without emitting another `elementRegistered` event. ``` type ElementRegisteredEvent = { type: "elementRegistered" timestamp: number element: ForesightElement state: ForesightElementState } ``` **Related Types:** [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** #### `elementUnregistered`[​](#elementunregistered "Direct link to elementunregistered") Fired when an element is removed from `ForesightManager`'s tracking via `ForesightManager.instance.unregister(element)`. Detaching an element from the DOM no longer unregisters it. It is parked (kept registered but inactive) and resumed on reattach, so no `elementUnregistered` event fires for that. The `"disconnected"` reason is therefore no longer emitted. ``` type ElementUnregisteredEvent = { type: "elementUnregistered" timestamp: number element: ForesightElement state: ForesightElementState unregisterReason: "disconnected" | "apiCall" | "devtools" | (string & {}) wasLastRegisteredElement: boolean } ``` **Related Types:** [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) *** ### Prediction Events[​](#prediction-events "Direct link to Prediction Events") Events fired during movement prediction calculations. *** #### `mouseTrajectoryUpdate`[​](#mousetrajectoryupdate "Direct link to mousetrajectoryupdate") Fired during mouse movement for trajectory calculations ``` type MouseTrajectoryUpdateEvent = { type: "mouseTrajectoryUpdate" trajectoryPositions: { positions: CircularBuffer // mouse position history currentPoint: { x: number; y: number } predictedPoint: { x: number; y: number } } predictionEnabled: boolean } ``` *** #### `scrollTrajectoryUpdate`[​](#scrolltrajectoryupdate "Direct link to scrolltrajectoryupdate") Fired during scroll events when scroll prediction is active ``` type ScrollTrajectoryUpdateEvent = { type: "scrollTrajectoryUpdate" currentPoint: Point // { x: number; y: number } predictedPoint: Point // { x: number; y: number } scrollDirection: ScrollDirection // "down" | "up" | "left" | "right" } ``` *** ### Configuration Events[​](#configuration-events "Direct link to Configuration Events") Events fired when ForesightManager configuration changes. *** #### `managerSettingsChanged`[​](#managersettingschanged "Direct link to managersettingschanged") Fired when global `ForesightManager` settings are updated via the devtools or via `ForesightManager.instance.alterGlobalSettings()`. ``` type ManagerSettingsChangedEvent = { type: "managerSettingsChanged" timestamp: number managerData: Readonly updatedSettings: UpdatedManagerSetting[] } ``` **Related Types:** [`ForesightManagerData`](/docs/getting-started/typescript.md#foresightmanagerdata) *** #### `deviceStrategyChanged`[​](#devicestrategychanged "Direct link to devicestrategychanged") Fired when user switches between input methods (mouse, touch, or pen). ``` type DeviceStrategyChangedEvent = { type: "deviceStrategyChanged" timestamp: number newStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" oldStrategy: CurrentDeviceStrategy // "mouse" | "touch" | "pen" } ``` *** --- # Foresight component `Foresight` is the component form of [`useForesight`](/docs/vue/useForesight.md): one instance, one registration. It renders in one of two ways. ## Rendering an element with `as`[​](#rendering-an-element-with-as "Direct link to rendering-an-element-with-as") With `as`, `Foresight` renders that element itself and registers it. ``` ``` `as` accepts any element tag (`"button"`, `"a"`, `"div"`, ...) or a component whose root is a DOM element. note In the `as` form there is no way to pass your own `ref` to the rendered element, `Foresight` uses the ref slot for its registration. If you need the DOM node, use the render-prop form below and attach both refs yourself. ### Reading state in `as` form[​](#reading-state-in-as-form "Direct link to reading-state-in-as-form") To use the [reactive state](/docs/vue/useForesight.md#reactive-state) in this form, read it from the scoped default slot: ``` {{ isCallbackRunning ? "Prefetching…" : "Checkout" }} ``` ## Render-prop form[​](#render-prop-form "Direct link to Render-prop form") Without `as`, `Foresight` renders only its default slot. The scoped slot receives the [reactive state](/docs/vue/useForesight.md#reactive-state) plus the `elementRef` to attach, which gives full control over the markup: ``` ``` You must bind `elementRef` to an element with `:ref="elementRef"`, without it nothing is registered with `Foresight`. ## Options[​](#options "Direct link to Options") `Foresight` takes the same options as [`useForesight`](/docs/vue/useForesight.md), passed as props. The only renamed option is `name`, on the component it is `foresightName`, so the HTML `name` attribute (on `input`, `button`, `select`, ...) falls through to the element like any other attribute. ## Dynamic lists[​](#dynamic-lists "Direct link to Dynamic lists") `Foresight` can be rendered in a `v-for`, registering one element per item: ``` ``` --- # Initialize the Manager Optional Step This step is **not needed** if you don't want to configure ForesightJS. The manager will initialize automatically with default settings the first time the directive or a composable registers an element. If you don't want to configure anything, skip to [Quick Start](/docs/vue/quick-start.md). If you want to customize ForesightJS's global behavior, call `ForesightManager.initialize()` once in your `main.ts`, before mounting the app: ``` import { ForesightManager } from "@foresightjs/vue" ``` ## Optional Settings[​](#optional-settings "Direct link to Optional Settings") You can configure any of these [global settings](/docs/vue/configuration/global-settings.md). ``` ForesightManager.initialize({ // Mouse prediction settings enableMousePrediction: true, // Enable/disable mouse trajectory prediction trajectoryPredictionTime: 120, // How far ahead to predict (10-200ms) positionHistorySize: 8, // Mouse positions to track (2-30) // Keyboard prediction settings enableTabPrediction: true, // Enable/disable keyboard navigation prediction tabOffset: 2, // Tab stops ahead to trigger (0-20) // Scroll prediction settings enableScrollPrediction: true, // Enable/disable scroll prediction scrollMargin: 150, // Pixel distance for scroll detection (30-300) // Touch device settings touchDeviceStrategy: "viewport", // "none", "viewport", or "onTouchStart" // Connection settings minimumConnectionType: "3g", // "slow-2g", "2g", "3g", "4g" // Default element settings defaultHitSlop: 0, // Default hit slop for all elements (number or {top, right, bottom, left}) }) ``` ## Good to know[​](#good-to-know "Direct link to Good to know") The `ForesightManager` is a singleton, meaning it can only be initialized once. If you call `ForesightManager.initialize()` multiple times, only the first call will take effect. Any subsequent calls will simply return the already initialized instance. The manager also provides several static methods for inspecting which elements are registered, how many callbacks have run, and the registered [event](/docs/vue/events.md) listeners. You can find a list of these methods [here](/docs/vue/static-properties.md). --- # Installation [![npm version](https://img.shields.io/npm/v/@foresightjs/vue.svg)](https://www.npmjs.com/package/@foresightjs/vue) [![npm downloads](https://img.shields.io/npm/dt/@foresightjs/vue.svg)](https://www.npmjs.com/package/@foresightjs/vue) `@foresightjs/vue` is the official Vue 3 package for ForesightJS. It ships the [`v-foresight`](/docs/vue/directive.md) directive for the common case, plus the [`useForesight`](/docs/vue/useForesight.md) and [`useForesightEvent`](/docs/vue/useForesightEvent.md) composables and the [`Foresight`](/docs/vue/foresight-component.md) component when you want the element's prediction state. ``` npm install @foresightjs/vue js.foresight # or pnpm add @foresightjs/vue js.foresight # or yarn add @foresightjs/vue js.foresight ``` ``` import { useForesight, vForesight, ForesightManager } from "@foresightjs/vue" ``` ## Configuring the manager[​](#configuring-the-manager "Direct link to Configuring the manager") You don't have to configure anything. The manager auto-initializes with sensible defaults the first time you register an element. If you want to tweak global behaviour, call `ForesightManager.initialize()` once in your `main.ts`. See [Initialize the Manager](/docs/vue/initialize-the-manager.md) and [Global Settings](/docs/vue/configuration/global-settings.md). ## Devtools[​](#devtools "Direct link to Devtools") The visual [Development Tools](/docs/vue/devtools.md) live in their own package (`js.foresight-devtools`) and work the same regardless of framework. --- # Migrating to v4 In v3 there was no Vue package - the docs handed you a directive and composable to copy into your own project. v4 replaces those with the official [`@foresightjs/vue`](/docs/vue/installation.md) package. Install it, delete your copied files, and update a couple of names. Install the package and delete your hand-rolled `vForesight.ts` / `useForesight.ts`: ``` npm install @foresightjs/vue js.foresight ``` ## Directive[​](#directive "Direct link to Directive") ``` - import { vForesight } from "@/directives/vForesight" + import { vForesight } from "@foresightjs/vue" app.directive("foresight", vForesight) ``` The usage in your templates is unchanged. ## Composable[​](#composable "Direct link to Composable") The composable lost the `templateRefKey` string. Instead it returns an `elementRef` function you bind directly: ``` - import { useForesight } from "./composables/useForesight" + import { useForesight } from "@foresightjs/vue" - const { templateRef } = useForesight({ templateRefKey: "myButton", callback }) + const { elementRef } = useForesight({ callback }) ``` ``` - + ``` You also get the [reactive state](/docs/vue/useForesight.md#reactive-state) back as refs instead of a `registerResults` object. ## Core changes[​](#core-changes "Direct link to Core changes") The core `js.foresight` library also has a few breaking changes. The package handles most of these for you, but they matter if you call `register()` or listen to events directly. The `register()` call itself is unchanged, but element data is now an immutable state snapshot you can subscribe to - the foundation the wrapper packages are built on: * **`ForesightElementData` is now `ForesightElementState`.** Your callback receives the flat state snapshot: `callback: (state: ForesightElementState) => void`. See [TypeScript](/docs/getting-started/typescript.md). * **`register()` returns the state plus `unregister`, `subscribe` and `getSnapshot`.** `isTouchDevice` is gone from the result; touch devices are handled internally by the configured `touchDeviceStrategy` global setting. * **The `elementDataUpdated`, `elementOptionsUpdated` and `elementReactivated` events are removed.** Per-element changes are now observed through the `subscribe`/`getSnapshot` pair returned by `register()`. The remaining events carry the state in a `state` field instead of `elementData`. * **Detached elements are parked, not unregistered.** Removing an element from the DOM no longer emits `elementUnregistered` with reason `disconnected`; the element stays registered but inactive and resumes when it reconnects. * **New `enabled` element option.** Set it to `false` to keep an element registered but excluded from prediction. --- # Quick Start Register your first element with ForesightJS in Vue and see how prediction works. ## Basic Usage Example[​](#basic-usage-example "Direct link to Basic Usage Example") The simplest way to register an element is the [`v-foresight`](/docs/vue/directive.md) directive. Put it on an element and give it a callback (this is where your prefetching logic goes): ``` ``` That's it! Unregistration is automatic The directive registers the element when it mounts and unregisters it when it unmounts, so you never have to call `unregister()` yourself. The same goes for the `useForesight` composable below. ## Provide registration options[​](#provide-registration-options "Direct link to Provide registration options") To configure the element, pass an options object instead of a bare callback: ``` ``` See [registration options](/docs/vue/configuration/registration-options.md) for the full list. ## Reading prediction state[​](#reading-prediction-state "Direct link to Reading prediction state") When you want to read the element's prediction state, you have two options: the [`useForesight`](/docs/vue/useForesight.md) composable or the [`Foresight`](/docs/vue/foresight-component.md) component. The composable returns the state as reactive refs plus an `elementRef` function to bind to the element: ``` ``` ## Development Tools[​](#development-tools "Direct link to Development Tools") ForesightJS has dedicated [Development Tools](/docs/vue/devtools.md) that help you understand and tune how prediction is working in your application: ``` pnpm add js.foresight-devtools # or npm install js.foresight-devtools # or yarn add js.foresight-devtools ``` ``` import { ForesightDevtools } from "js.foresight-devtools" // Initialize development tools ForesightDevtools.initialize({ // optional props }) ``` --- # Static Properties The [directive](/docs/vue/directive.md) and [composables](/docs/vue/useForesight.md) handle registering and unregistering for you; the properties below are escape hatches for debugging and inspecting the manager state. ``` import { ForesightManager } from "@foresightjs/vue" ``` The ForesightManager exposes static properties and instance APIs for accessing and controlling manager state. ***All properties are read-only*** ## ForesightManager.instance[​](#foresightmanagerinstance "Direct link to ForesightManager.instance") Gets the singleton instance of ForesightManager, initializing it if necessary. This is the primary way to access the manager throughout your application. **Returns:** `ForesightManager` **Example:** ``` const manager = ForesightManager.instance // Register a single element manager.register({ element: myButton, callback: () => console.log("Predicted interaction!"), }) // Register multiple elements via NodeList const navLinks = document.querySelectorAll("nav a") manager.register({ element: navLinks, callback: state => console.log(`Predicted: ${state.name}`), }) ``` ## ForesightManager.instance.registeredElements[​](#foresightmanagerinstanceregisteredelements "Direct link to ForesightManager.instance.registeredElements") Gets a Map of all currently registered elements and their associated [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate). This is useful for debugging or inspecting the current state of registered elements. **Returns:** `ReadonlyMap` ## ForesightManager.isInitiated[​](#foresightmanagerisinitiated "Direct link to ForesightManager.isInitiated") Checks whether the ForesightManager has been initialized without initializing it. **Returns:** `Readonly` ## ForesightManager.instance.reactivate(element)[​](#foresightmanagerinstancereactivateelement "Direct link to ForesightManager.instance.reactivate(element)") Manually reactivates an element (or all elements in a `NodeList`), allowing its callback to be triggered again. This is useful when you want to re-enable prefetching for an element before its `reactivateAfter` timeout expires. **Parameters:** * `element` - The DOM element or `NodeList` to reactivate **Example:** ``` // Single element const myButton = document.getElementById("my-button") ForesightManager.instance.reactivate(myButton) // Multiple elements const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.reactivate(navLinks) ``` ## ForesightManager.instance.unregister(element, reason?)[​](#foresightmanagerinstanceunregisterelement-reason "Direct link to ForesightManager.instance.unregister(element, reason?)") Removes an element (or all elements in a `NodeList`) from ForesightManager's tracking. Frameworks handle this for you The official React, Vue, and Angular integrations call `unregister` automatically when a component unmounts, so you rarely call it yourself there. Detaching an element from the DOM does **not** unregister it. It is parked (kept registered but inactive) and resumes when it reattaches, so it survives detach/reattach and re-parenting. Unregistering only happens when you call this method explicitly. If you register elements yourself (vanilla JS) and permanently remove one from the DOM, call `unregister` to stop tracking it. Otherwise it stays parked and keeps a reference to the detached element, which prevents it from being garbage collected. **Parameters:** * `element` - The DOM element or `NodeList` to unregister * `reason` (optional) - A string describing why the element was unregistered (useful for debugging via events) **Example:** ``` // Single element const myButton = document.getElementById("my-button") ForesightManager.instance.unregister(myButton, "user-navigation") // Multiple elements const navLinks = document.querySelectorAll("nav a") ForesightManager.instance.unregister(navLinks) ``` ## ForesightManager.instance.getManagerData[​](#foresightmanagerinstancegetmanagerdata "Direct link to ForesightManager.instance.getManagerData") Snapshot of the current `ForesightManager` state, including all global settings, registered elements, position observer data, and interaction statistics. This is primarily used for debugging, monitoring, and development purposes. **Properties:** * `registeredElements` - `Map` of all currently registered elements and their associated [`ForesightElementState`](/docs/getting-started/typescript.md#foresightelementstate) * `eventListeners` - `Map` of all event listeners listening to ForesightManager events. * `globalSettings` - Current global configuration settings * `globalCallbackHits` - Total `callback` execution counts by interaction type (mouse/tab/scroll/viewport/touch) and by subtype (hover/trajectory for mouse, forwards/reverse for tab, direction for scroll) * `currentDeviceStrategy` - Which strategy is being used: `mouse`, `touch`, or `pen`. This changes dynamically as the input method changes * `activeElementCount` - Amount of elements currently active (not the same as registered) * `parkedElementCount` - Amount of elements currently parked: detached from the DOM (`isParked: true`) and waiting to reconnect * `loadedModules` - Shows which handlers and predictors have been [lazy loaded](/docs/Behind_the_Scenes.md#bundle-optimization) **Returns:** `Readonly` The return will look something like this: ``` { "registeredElements": { "size": 7, "entries": "" }, "activeElementCount": 5, "parkedElementCount": 0, "currentDeviceStrategy": "mouse", "eventListeners": { "0": { "elementRegistered": [] }, "1": { "elementUnregistered": [] }, "2": { "mouseTrajectoryUpdate": [] }, "3": { "scrollTrajectoryUpdate": [] }, "4": { "managerSettingsChanged": [] }, "5": { "callbackInvoked": [] }, "6": { "callbackCompleted": [] } }, "globalSettings": { "defaultHitSlop": { "bottom": 0, "left": 0, "right": 0, "top": 0 }, "enableMousePrediction": true, "enableScrollPrediction": true, "enableTabPrediction": true, "positionHistorySize": 8, "scrollMargin": 150, "tabOffset": 2, "trajectoryPredictionTime": 120, "touchDeviceStrategy": "onTouchStart", "minimumConnectionType": "3g", "enableManagerLogging": false, "setDataAttributes": true }, "globalCallbackHits": { "mouse": { "hover": 0, "trajectory": 3 }, "scroll": { "down": 2, "left": 0, "right": 0, "up": 0 }, "tab": { "forwards": 3, "reverse": 0 }, "touch": 0, "viewport": 0, "total": 8 }, "loadedModules": { "desktopHandler": true, "touchHandler": false, "predictors": { "mouse": true, "tab": true, "scroll": true, "viewport": false, "touchStart": false } } } ``` --- # TypeScript `@foresightjs/vue` exports its binding types and common core types such as `ForesightElementState`, `ForesightCallback`, and `ForesightRegisterOptionsWithoutElement`: ``` import type { ForesightRegisterOptionsWithoutElement } from "@foresightjs/vue" ``` Other core types come from `js.foresight` directly: ``` import type { ElementBounds, ForesightRegisterResult, ForesightManagerData } from "js.foresight" ``` ## Typed metadata[​](#typed-metadata "Direct link to Typed metadata") Augment `ForesightRegister`, the global type registry, to define your metadata shape once in a `.d.ts` file included by your app's `tsconfig.json`: ``` import "js.foresight" declare module "js.foresight" { interface ForesightRegister { meta: { href: string } } } ``` Declare `meta` as a **required key** in the registry. Writing `meta?: AppMeta` silently falls back to `Record`. Put optionality on the fields inside it instead, for example `meta: { href?: string }`. Registration options, callback state, events, devtools, and all framework bindings use this shape automatically. No generics are needed. ``` import { ForesightManager } from "js.foresight" ForesightManager.instance.register({ element: document.querySelector("a")!, meta: { href: "/about" }, callback: state => { console.log(state.meta.href.toUpperCase()) }, }) ``` Without augmentation, `ForesightMeta` defaults to `Record`. Metadata remains optional and defaults to `{}`. Unregistered framework snapshots also contain `{}`, so use optional fields such as `href?: string` if you read them before registration or register elements without metadata. Augmentation only changes types, it does not validate metadata at runtime. ## Most Used Internal types[​](#most-used-internal-types "Direct link to Most Used Internal types") ### ForesightElementState[​](#foresightelementstate "Direct link to ForesightElementState") This is the main type used in ForesightJS: the flat, immutable state snapshot of a registered element. Your [callback](#foresightcallback) receives it, most [events](/docs/events.md) carry it as `state`, and `register()` returns it (plus a few functions, see [ForesightRegisterResult](#foresightregisterresult)). The reference is replaced on every logical change, never mutated. Geometry-only changes use [ElementBounds](#elementbounds), on their own subscription channel. Scrolling can still change logical state, such as viewport visibility or prediction status. ``` type ForesightElementState = { id: string // unique id assigned during registration name: string // name visible in the devtools meta: ForesightMeta hitSlop: Exclude // normalized hit slop applied to this element isLimitedConnection: boolean isIntersectingWithViewport: boolean isRegistered: boolean isActive: boolean isParked: boolean // detached from the DOM, kept registered but inactive until it reconnects isEnabled: boolean isPredicted: boolean isCallbackRunning: boolean hitCount: number registerCount: number // amount of times this element has been (re)registered durationMs: number | undefined // duration of the most recent callback run status: "error" | "success" | undefined // status of the most recent callback error: string | null reactivateAfter: number } ``` See the [registration return value](/docs/configuration/registration-options.md#registration-return-value) for what each field means. ### ElementBounds[​](#elementbounds "Direct link to ElementBounds") The element's geometry: its measured rect and the [hitSlop](/docs/configuration/global-settings.md)-expanded rect used for interaction detection. Visible elements' position and size changes are published on a separate channel (`getBounds`/`subscribeToBounds`) and kept out of `ForesightElementState`, so geometry-only updates do not notify state subscribers. ``` type ElementBounds = { originalRect: DOMRectReadOnly // as returned by getBoundingClientRect() expandedRect: Readonly // originalRect + hitSlop } ``` ### ForesightRegisterResult[​](#foresightregisterresult "Direct link to ForesightRegisterResult") What `ForesightManager.instance.register()` returns: the element's state at registration time plus control functions. ``` type ForesightRegisterResult = ForesightElementState & { unregister: () => void subscribe: (listener: () => void) => () => void // logical state changes; returns an unsubscribe function getSnapshot: () => ForesightElementState // current state snapshot, stable reference across scrolls subscribeToBounds: (listener: () => void) => () => void // geometry changes (every scroll/resize tick) getBounds: () => ElementBounds // current geometry snapshot } ``` ### CallbackHitType[​](#callbackhittype "Direct link to CallbackHitType") ``` type CallbackHitType = | { kind: "mouse"; subType: "hover" | "trajectory" } | { kind: "tab"; subType: "forwards" | "reverse" } | { kind: "scroll"; subType: "up" | "down" | "left" | "right" } | { kind: "touch"; subType?: string } | { kind: "viewport"; subType?: string } ``` ### CurrentDeviceStrategy[​](#currentdevicestrategy "Direct link to CurrentDeviceStrategy") Represents the current input method being used. ForesightJS automatically detects and switches between strategies. ``` type CurrentDeviceStrategy = "mouse" | "touch" | "pen" ``` ### ForesightManagerData[​](#foresightmanagerdata "Direct link to ForesightManagerData") Snapshot of the current ForesightManager state, including all [global settings](/docs/configuration/global-settings.md), registered elements, and interaction statistics. This is primarily used for debugging, monitoring, and development purposes. This data is returned in the [`managerSettingsChanged`](/docs/events.md) event or by calling [`ForesightManager.instance.getManagerData`](/docs/debugging/static-properties.md#foresightmanagerinstancegetmanagerdata) manually. ``` type ForesightManagerData = { registeredElements: ReadonlyMap // See above for ForesightElementState globalSettings: Readonly // See configuration for global settings globalCallbackHits: Readonly // See above for CallbackHitType, this is that but all of them combined eventListeners: ReadonlyMap> // All event listeners currently listening currentDeviceStrategy: CurrentDeviceStrategy activeElementCount: number parkedElementCount: number // elements detached from the DOM, waiting to reconnect loadedModules: ForesightModules // which handlers/predictors are lazily loaded } ``` ## Helper Types[​](#helper-types "Direct link to Helper Types") ### ForesightCallback[​](#foresightcallback "Direct link to ForesightCallback") The callback function type used when registering elements. Receives the [`ForesightElementState`](#foresightelementstate) of the element that triggered the callback. ``` type ForesightCallback = (state: ForesightElementState) => void ``` This is useful when registering multiple elements with a shared callback, as you can identify which element triggered it: ``` const myCallback: ForesightCallback = state => { console.log(`Triggered by: ${state.name}`) } ``` ### ForesightRegisterOptionsWithoutElement[​](#foresightregisteroptionswithoutelement "Direct link to ForesightRegisterOptionsWithoutElement") Useful for if you want to create a custom button component in a modern framework. And you want to have the `ForesightRegisterOptions` used in `ForesightManager.instance.register({})` without the element as the element will be the ref of the component. This type is the options object you pass to the [`useForesight`](/docs/react/useForesight.md) hook in React, the [`useForesight`](/docs/vue/useForesight.md) composable in Vue, and Angular's [`[fsForesight]`](/docs/angular/directive.md) / [`ForesightService`](/docs/angular/foresight-service.md) APIs. ``` type ForesightButtonProps = { registerOptions: ForesightRegisterOptionsWithoutElement } ``` --- # useForesight `useForesight` registers a single element and gives you back the element's reactive prediction state as refs, plus an `elementRef` function to bind to the element. Use it over the [`v-foresight`](/docs/vue/directive.md) directive when you want to read state like `isPredicted` in your template. ``` ``` Bind `elementRef` with `:ref="elementRef"` and the composable handles the rest: it registers the element when it mounts, unregisters it when the component is disposed, and re-registers if the element swaps. `elementRef` works on plain elements and on component instances (it pulls out the underlying element for you). ## Options[​](#options "Direct link to Options") The options are the [registration options](/docs/vue/configuration/registration-options.md), minus the `element` (`elementRef` handles that). `callback` is the only required field. To make options reactive, pass a ref, reactive object, or getter. The composable updates the registration in place when options change. Each options object describes the full configuration, so omitted fields reset to their defaults: ``` ``` ## Reactive state[​](#reactive-state "Direct link to Reactive state") Every [state](/docs/vue/configuration/registration-options.md#state-fields) field comes back as a reactive ref, so reading it in your template re-renders when it changes. The ones you'll reach for most: * `isPredicted` → the callback has fired for this element * `isActive` → eligible to fire (not disabled, not on a limited connection, not parked) * `isParked` → detached from the DOM and parked; resumes when it reconnects * `isCallbackRunning` → your (awaited) callback is mid-flight * `hitCount`, `status`, `error` → how the last run went ``` ``` --- # useForesightEvent This is meant for high-level, app-wide use cases (analytics, debugging, a global prediction counter), not per-element logic. `useForesightEvent` subscribes a component to the [`ForesightManager` events](/docs/vue/events.md). Pass the event name and a listener, and the composable adds and removes the listener for you. ``` ``` You don't need to remove the listener yourself, the composable does it for you when the component is disposed. See [Events](/docs/vue/events.md) for the full list of event names and their payloads. --- # What is ForesightJS [![npm version](https://img.shields.io/npm/v/js.foresight.svg)](https://www.npmjs.com/package/js.foresight) [![npm downloads](https://img.shields.io/npm/dt/js.foresight.svg)](https://www.npmjs.com/package/js.foresight) [![Bundle Size](https://img.shields.io/bundlejs/size/js.foresight)](https://bundlejs.com/?q=js.foresight) [![GitHub stars](https://img.shields.io/github/stars/spaansba/ForesightJS.svg?style=social\&label=Star)](https://github.com/spaansba/ForesightJS) [![Best of JS](https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=spaansba%2FForesightJS%26since=daily)](https://bestofjs.org/projects/foresightjs) [![Demo](https://img.shields.io/badge/demo-live-blue)](https://foresightjs.com#playground) ForesightJS is a lightweight JavaScript library that predicts user intent to prefetch content before it's needed. It works completely out of the box without configuration, supporting both desktop and mobile devices with different prediction strategies. ## Understanding ForesightJS's Role[​](#understanding-foresightjss-role "Direct link to Understanding ForesightJS's Role") When you over simplify prefetching it exists of three parts: * **What** resource or data to load * **How** the loading method and caching strategy is * **When** the optimal moment to start fetching is ForesightJS takes care of the **When** by predicting user intent with mouse trajectory and tab navigation. You supply the **What** and **How** inside your `callback` when you register an element. In Vue you use ForesightJS through the official `@foresightjs/vue` package: the `v-foresight` directive registers your elements, and composables expose their reactive prediction state. ## Installation[​](#installation "Direct link to Installation") ``` npm install @foresightjs/vue js.foresight ``` Continue with [Installation](/docs/vue/installation.md) and the [Quick Start](/docs/vue/quick-start.md). ## Prediction Strategies[​](#prediction-strategies "Direct link to Prediction Strategies") ForesightJS uses different prediction strategies depending on the device type. For limited connections (slower than the minimum connection type - default: 3G - or [data-saver mode](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData)), we respect the user's preference to minimize data usage: the element is still registered but stays inactive, so it is never predicted and never prefetches. Foresight automatically detects mouse, pen, or touch input, and updates its strategy if the input method changes. ### Keyboard/Mouse Users[​](#keyboardmouse-users "Direct link to Keyboard/Mouse Users") Pick and choose multiple prediction strategies. All are enabled by default, but you can disable any of them when initializing the `ForesightManager`. * **Mouse Trajectory** default - Analyzes cursor movement patterns to predict which links users are heading towards and prefetches content before they arrive * **Keyboard Navigation** default - Tracks tab key usage to prefetch when the user is N tab stops away from your registered element * **Scroll** default - Prefetches content when users scroll towards registered elements, predicting which elements will be reached based on scroll direction ### Touch Devices[​](#touch-devices "Direct link to Touch Devices") While desktop systems use multiple concurrent prediction strategies, touch devices operate with a single active strategy that can be configured while initializing the `ForesightManager`. * **onTouchStart** default - Captures the initial touch event to begin prefetching when users start interacting with registered elements * **Viewport Enter** - Detects when registered elements enter the viewport and prefetches their content based on scroll behavior and visibility * **None** - Disables ForesightJS on touch devices (previous behavior) ## Why ForesightJS?[​](#why-foresightjs "Direct link to Why ForesightJS?") ForesightJS is a specialized performance tool, not something to drop into every project. ### Which problems does ForesightJS solve?[​](#which-problems-does-foresightjs-solve "Direct link to Which problems does ForesightJS solve?") #### Problem 1: On-Hover Prefetching Still Has Latency[​](#problem-1-on-hover-prefetching-still-has-latency "Direct link to Problem 1: On-Hover Prefetching Still Has Latency") Traditional hover-based prefetching only triggers after the user's cursor reaches an element. This approach wastes the critical 100-200ms window between when a user begins moving toward a target and when the hover event actually fires-time that could be used for prefetching. #### Problem 2: Viewport-Based Prefetching is Wasteful[​](#problem-2-viewport-based-prefetching-is-wasteful "Direct link to Problem 2: Viewport-Based Prefetching is Wasteful") Many modern frameworks (like Next.js) automatically prefetch resources for all links that enter the viewport. While well-intentioned, this creates significant overhead since users typically interact with only a small fraction of visible elements. Simply scrolling up and down the Next.js homepage can trigger ***1.59MB*** of unnecessary prefetch requests. #### Problem 3: Hover-Based Prefetching Excludes Keyboard Users[​](#problem-3-hover-based-prefetching-excludes-keyboard-users "Direct link to Problem 3: Hover-Based Prefetching Excludes Keyboard Users") Many routers rely on hover-based prefetching, but this approach completely excludes keyboard users since keyboard navigation never triggers hover events. This means keyboard users miss out on the performance benefits that mouse users get from hover-based prefetching. ## Lightweight[​](#lightweight "Direct link to Lightweight") The full library is **\~32 KB** minified, and code splitting means you only ship the parts you actually use. See [Behind the Scenes](/docs/Behind_the_Scenes.md#bundle-optimization) for the per-chunk breakdown. ---