Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/guide/pages-routing/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ voidReact({ prefetch: { defaultStrategy: 'hover' } });

Prefer `'hover'` or `'click'` as the default. With `'mount'` or `'visible'`, every `Link` loads its page as soon as it renders or scrolls into view, including links in long lists.

`Link` doesn't prefetch while the browser reports that data saver is on or the connection is 2G, which only Chromium-based browsers report.

Prefetching runs the destination's loader, so keep loaders free of side effects such as counting views.

## Scroll Restoration
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type LinkPrefetchStrategy = 'hover' | 'click' | 'mount' | 'visible';
| `'visible'` | The first time the link scrolls into view |
| `false` | Never |

With an array such as `['visible', 'hover']`, the first strategy to happen loads the page, and later ones reuse it. `prefetch` works only on GET links, and links to other sites are never prefetched.
With an array such as `['visible', 'hover']`, the first strategy to happen loads the page, and later ones reuse it. `prefetch` works only on GET links, and links to other sites are never prefetched. No strategy prefetches while the browser reports data saver or a 2G connection, which only Chromium-based browsers report; calling `router.prefetch()` yourself still loads the page.

`cacheFor` sets how long a prefetched page is reused, in milliseconds or as a duration such as `'5m'`. A pair such as `['30s', '5m']` treats the page as fresh for 30 seconds, then still shows it for up to 5 minutes while loading a fresh copy in the background.

Expand Down
16 changes: 15 additions & 1 deletion packages/void/src/pages/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,26 @@ function resolveSameOrigin(href: string, base: URL): URL | null {
return target;
}

/** The Network Information API fields Link reads. Only Chromium implements them. */
type NetworkConnection = { saveData?: boolean; effectiveType?: string };

/** Data saver is on, or the connection is 2G-class. Read per call: both change while the page is open. */
function isConstrainedConnection(): boolean {
const connection = (navigator as Navigator & { connection?: NetworkConnection }).connection;
return (
connection?.saveData === true ||
connection?.effectiveType === '2g' ||
connection?.effectiveType === 'slow-2g'
);
}

/** Link's automatic prefetch. `router.prefetch()` called directly is not limited by the connection. */
export function prefetchLink(
router: VoidRouter | null | undefined,
href: Href,
cacheFor: CacheForInput | undefined,
): void {
if (!router?.prefetch) {
if (!router?.prefetch || isConstrainedConnection()) {
return;
}

Expand Down
39 changes: 39 additions & 0 deletions packages/void/test/unit/pages-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,45 @@ describe('prefetchLink', () => {

expect(router.prefetch.mock.calls).toEqual([['https://app.test/users/42', {}]]);
});

it('skips while data saver is on or the connection is 2G-class', () => {
const router = createRouter();
for (const connection of [
{ saveData: true, effectiveType: '4g' },
{ effectiveType: '2g' },
{ effectiveType: 'slow-2g' },
]) {
vi.stubGlobal('navigator', { connection });
prefetchLink(router, '/users/42', undefined);
}

expect(router.prefetch).not.toHaveBeenCalled();
});

it('prefetches on faster connections and when the browser reports none', () => {
const router = createRouter();
for (const stub of [
{ connection: { saveData: false, effectiveType: '3g' } },
{ connection: { effectiveType: '4g' } },
{},
]) {
vi.stubGlobal('navigator', stub);
prefetchLink(router, '/users/42', undefined);
}

expect(router.prefetch).toHaveBeenCalledTimes(3);
});

it('reads the connection on each prefetch', () => {
const router = createRouter();
const connection = { saveData: true };
vi.stubGlobal('navigator', { connection });
prefetchLink(router, '/users/42', undefined);
connection.saveData = false;
prefetchLink(router, '/users/43', undefined);

expect(router.prefetch.mock.calls).toEqual([['/users/43', {}]]);
});
});

describe('handleLinkClick', () => {
Expand Down
Loading