diff --git a/.changeset/great-turtles-clap.md b/.changeset/great-turtles-clap.md
new file mode 100644
index 0000000000..2634ec5dc0
--- /dev/null
+++ b/.changeset/great-turtles-clap.md
@@ -0,0 +1,10 @@
+---
+"astro": patch
+---
+
+The prefetch feature is updated to better support different browsers and different cache headers setup, including:
+
+1. All prefetch strategies will now always try to use `` if supported, or will fall back to `fetch()`.
+2. The `prefetch()` programmatic API's `with` option is deprecated in favour of an automatic approach that will also try to use ` u.includes(url)).length;
+ expect(
+ fetchCount,
+ `${url} should be prefetched ${count} time(s), but is prefetch with link ${linkCount} time(s) and with fetch ${fetchCount} time(s)`
+ ).toEqual(count);
+ }
+ }
+}
+
+/**
+ * Check if url is not prefetched via `link[rel="prefetch"]` and `fetch()` (from `reqUrls`)
+ * @param {string} url
+ * @param {import('@playwright/test').Page} page
+ */
+async function expectUrlNotPrefetched(url, page) {
+ await expect(page.locator(`link[rel="prefetch"][href$="${url}"]`)).not.toBeAttached();
+ expect(reqUrls).not.toContainEqual(url);
+}
+
test.describe('Prefetch (default)', () => {
let devServer;
- /** @type {string[]} */
- const reqUrls = [];
test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer();
});
- test.beforeEach(async ({ page }) => {
- page.on('request', (req) => {
- const urlObj = new URL(req.url());
- reqUrls.push(urlObj.pathname + urlObj.search);
- });
- });
-
- test.afterEach(() => {
- reqUrls.length = 0;
- });
-
test.afterAll(async () => {
await devServer.stop();
});
test('Link without data-astro-prefetch should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-default');
+ await expectUrlNotPrefetched('/prefetch-default', page);
});
test('data-astro-prefetch="false" should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-false');
+ await expectUrlNotPrefetched('/prefetch-false', page);
});
test('Link with search param should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/?search-param=true');
+ await expectUrlNotPrefetched('/?search-param=true', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-search-param').hover(),
]);
- expect(reqUrls).toContainEqual('/?search-param=true');
+ await expectUrlPrefetched('/?search-param=true', page);
});
test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
@@ -61,52 +99,47 @@ test.describe('Prefetch (default)', () => {
test('data-astro-prefetch="hover" should prefetch on hover', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-hover');
+ await expectUrlNotPrefetched('/prefetch-hover', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-hover').hover(),
]);
- expect(reqUrls).toContainEqual('/prefetch-hover');
+ await expectUrlPrefetched('/prefetch-hover', page);
});
test('data-astro-prefetch="viewport" should prefetch on viewport', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-viewport');
+ await expectUrlNotPrefetched('/prefetch-viewport', page);
// Scroll down to show the element
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-viewport').scrollIntoViewIfNeeded(),
]);
- expect(reqUrls).toContainEqual('/prefetch-viewport');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-viewport"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-viewport', page);
});
test('manual prefetch() works once', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-manual');
+ await expectUrlNotPrefetched('/prefetch-manual', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-manual').click(),
]);
- expect(reqUrls).toContainEqual('/prefetch-manual');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-manual"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-manual', page);
// prefetch again should have no effect
await page.locator('#prefetch-manual').click();
- expect(reqUrls.filter((u) => u.includes('/prefetch-manual')).length).toEqual(1);
+ await expectUrlPrefetched('/prefetch-manual', page, 1);
});
test('data-astro-prefetch="load" should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).toContainEqual('/prefetch-load');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-load"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-load', page);
});
});
test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'tap')", () => {
let devServer;
- /** @type {string[]} */
- const reqUrls = [];
test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer({
@@ -117,89 +150,74 @@ test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'tap')", () => {
});
});
- test.beforeEach(async ({ page }) => {
- page.on('request', (req) => {
- const urlObj = new URL(req.url());
- reqUrls.push(urlObj.pathname + urlObj.search);
- });
- });
-
- test.afterEach(() => {
- reqUrls.length = 0;
- });
-
test.afterAll(async () => {
await devServer.stop();
});
test('Link without data-astro-prefetch should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-default');
+ await expectUrlNotPrefetched('/prefetch-default', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-default').click(),
]);
- expect(reqUrls).toContainEqual('/prefetch-default');
+ await expectUrlPrefetched('/prefetch-default', page);
});
test('data-astro-prefetch="false" should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-false');
+ await expectUrlNotPrefetched('/prefetch-false', page);
});
test('Link with search param should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/?search-param=true');
+ await expectUrlNotPrefetched('/?search-param=true', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-search-param').hover(),
]);
- expect(reqUrls).toContainEqual('/?search-param=true');
+await expectUrlPrefetched('/?search-param=true', page);
});
test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-tap');
+ await expectUrlNotPrefetched('/prefetch-tap', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-tap').click(),
]);
- expect(reqUrls).toContainEqual('/prefetch-tap');
+await expectUrlPrefetched('/prefetch-tap', page);
});
test('data-astro-prefetch="hover" should prefetch on hover', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-hover');
+ await expectUrlNotPrefetched('/prefetch-hover', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-hover').hover(),
]);
- expect(reqUrls).toContainEqual('/prefetch-hover');
+ await expectUrlPrefetched('/prefetch-hover', page);
});
test('data-astro-prefetch="viewport" should prefetch on viewport', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-viewport');
+ await expectUrlNotPrefetched('/prefetch-viewport', page);
// Scroll down to show the element
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-viewport').scrollIntoViewIfNeeded(),
]);
- expect(reqUrls).toContainEqual('/prefetch-viewport');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-viewport"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-viewport', page);
});
test('data-astro-prefetch="load" should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).toContainEqual('/prefetch-load');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-load"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-load', page);
});
});
test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'load')", () => {
let devServer;
- /** @type {string[]} */
- const reqUrls = [];
test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer({
@@ -210,78 +228,64 @@ test.describe("Prefetch (prefetchAll: true, defaultStrategy: 'load')", () => {
});
});
- test.beforeEach(async ({ page }) => {
- page.on('request', (req) => {
- const urlObj = new URL(req.url());
- reqUrls.push(urlObj.pathname + urlObj.search);
- });
- });
-
- test.afterEach(() => {
- reqUrls.length = 0;
- });
-
test.afterAll(async () => {
await devServer.stop();
});
test('Link without data-astro-prefetch should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).toContainEqual('/prefetch-default');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-default"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-default', page);
});
test('data-astro-prefetch="false" should not prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-false');
+ await expectUrlNotPrefetched('/prefetch-false', page);
});
test('Link with search param should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/?search-param=true');
+ await expectUrlNotPrefetched('/?search-param=true', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-search-param').hover(),
]);
- expect(reqUrls).toContainEqual('/?search-param=true');
+ await expectUrlPrefetched('/?search-param=true', page);
});
test('data-astro-prefetch="tap" should prefetch on tap', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-tap');
+ await expectUrlNotPrefetched('/prefetch-tap', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-tap').click(),
]);
- expect(reqUrls).toContainEqual('/prefetch-tap');
+ await expectUrlPrefetched('/prefetch-tap', page);
});
test('data-astro-prefetch="hover" should prefetch on hover', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-hover');
+ await expectUrlNotPrefetched('/prefetch-hover', page);
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-hover').hover(),
]);
- expect(reqUrls).toContainEqual('/prefetch-hover');
+ await expectUrlPrefetched('/prefetch-hover', page);
});
test('data-astro-prefetch="viewport" should prefetch on viewport', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).not.toContainEqual('/prefetch-viewport');
+ await expectUrlNotPrefetched('/prefetch-viewport', page);
// Scroll down to show the element
await Promise.all([
page.waitForEvent('request'), // wait prefetch request
page.locator('#prefetch-viewport').scrollIntoViewIfNeeded(),
]);
- expect(reqUrls).toContainEqual('/prefetch-viewport');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-viewport"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-viewport', page);
});
test('data-astro-prefetch="load" should prefetch', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/'));
- expect(reqUrls).toContainEqual('/prefetch-load');
- expect(page.locator('link[rel="prefetch"][href$="/prefetch-load"]')).toBeDefined();
+ await expectUrlPrefetched('/prefetch-load', page);
});
});
@@ -311,7 +315,9 @@ test.describe('Prefetch (default), Experimental ({ clientPrerender: true })', ()
let devServer;
- test.beforeAll(async ({ astro }) => {
+ test.beforeAll(async ({ astro, browserName }) => {
+ test.skip(browserName !== 'chromium', 'Only Chromium supports clientPrerender')
+
devServer = await astro.startDevServer({
experimental: {
clientPrerender: true,
@@ -320,7 +326,7 @@ test.describe('Prefetch (default), Experimental ({ clientPrerender: true })', ()
});
test.afterAll(async () => {
- await devServer.stop();
+ await devServer?.stop();
});
test('Link without data-astro-prefetch should not prefetch', async ({ page, astro }) => {
diff --git a/packages/astro/playwright.firefox.config.js b/packages/astro/playwright.firefox.config.js
index 1934a68ed1..d8ec35031d 100644
--- a/packages/astro/playwright.firefox.config.js
+++ b/packages/astro/playwright.firefox.config.js
@@ -5,7 +5,7 @@ process.stdout.isTTY = false;
const config = {
// TODO: add more tests like view transitions and audits, and fix them. Some of them are failing.
- testMatch: ['e2e/css.test.js'],
+ testMatch: ['e2e/css.test.js', 'e2e/prefetch.test.js'],
/* Maximum time one test can run for. */
timeout: 40 * 1000,
expect: {
diff --git a/packages/astro/src/prefetch/index.ts b/packages/astro/src/prefetch/index.ts
index 6d7cb294bb..bdf8676e7e 100644
--- a/packages/astro/src/prefetch/index.ts
+++ b/packages/astro/src/prefetch/index.ts
@@ -59,7 +59,7 @@ function initTapStrategy() {
event,
(e) => {
if (elMatchesStrategy(e.target, 'tap')) {
- prefetch(e.target.href, { with: 'fetch', ignoreSlowConnection: true });
+ prefetch(e.target.href, { ignoreSlowConnection: true });
}
},
{ passive: true }
@@ -107,7 +107,7 @@ function initHoverStrategy() {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
- prefetch(href, { with: 'fetch' });
+ prefetch(href);
}, 80) as unknown as number;
}
@@ -158,7 +158,7 @@ function createViewportIntersectionObserver() {
setTimeout(() => {
observer.unobserve(anchor);
timeouts.delete(anchor);
- prefetch(anchor.href, { with: 'link' });
+ prefetch(anchor.href);
}, 300) as unknown as number
);
} else {
@@ -180,7 +180,7 @@ function initLoadStrategy() {
for (const anchor of document.getElementsByTagName('a')) {
if (elMatchesStrategy(anchor, 'load')) {
// Prefetch every link in this page
- prefetch(anchor.href, { with: 'link' });
+ prefetch(anchor.href);
}
}
});
@@ -189,8 +189,12 @@ function initLoadStrategy() {
export interface PrefetchOptions {
/**
* How the prefetch should prioritize the URL. (default `'link'`)
- * - `'link'`: use ``, has lower loading priority.
- * - `'fetch'`: use `fetch()`, has higher loading priority.
+ * - `'link'`: use ``.
+ * - `'fetch'`: use `fetch()`.
+ *
+ * @deprecated It is recommended to not use this option, and let prefetch use `'link'` whenever it's supported,
+ * or otherwise fall back to `'fetch'`. `'link'` works better if the URL doesn't set an appropriate cache header,
+ * as the browser will continue to cache it as long as it's used subsequently.
*/
with?: 'link' | 'fetch';
/**
@@ -215,28 +219,27 @@ export function prefetch(url: string, opts?: PrefetchOptions) {
if (!canPrefetchUrl(url, ignoreSlowConnection)) return;
prefetchedUrls.add(url);
- const priority = opts?.with ?? 'link';
- debug?.(`[astro] Prefetching ${url} with ${priority}`);
-
- if (
- clientPrerender &&
- HTMLScriptElement.supports &&
- HTMLScriptElement.supports('speculationrules')
- ) {
- // this code is tree-shaken if unused
+ // Prefetch with speculationrules if `clientPrerender` is enabled and supported
+ // NOTE: This condition is tree-shaken if `clientPrerender` is false as its a static value
+ if (clientPrerender && HTMLScriptElement.supports?.('speculationrules')) {
+ debug?.(`[astro] Prefetching ${url} with