diff --git a/.changeset/disabled-link-preload.md b/.changeset/disabled-link-preload.md
new file mode 100644
index 00000000000..5578bd3d821
--- /dev/null
+++ b/.changeset/disabled-link-preload.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": patch
+---
+
+Add `` timeout counter and disabling logic in case preloading is disabled by the user in Firefox. This prevents us from hanging on client-side navigations when we try to preload stylesheets and never receive a `load`/`error` event on the `link` tag.
diff --git a/packages/remix-react/links.ts b/packages/remix-react/links.ts
index ec4962fb113..8ff5025c2fc 100644
--- a/packages/remix-react/links.ts
+++ b/packages/remix-react/links.ts
@@ -221,12 +221,42 @@ export function getLinksForMatches(
return dedupe(descriptors, preloads);
}
+let stylesheetPreloadTimeouts = 0;
+let isPreloadDisabled = false;
+
export async function prefetchStyleLinks(
routeModule: RouteModule
): Promise {
if (!routeModule.links) return;
let descriptors = routeModule.links();
if (!descriptors) return;
+ if (isPreloadDisabled) return;
+
+ // If we've hit our timeout 3 times, we may be in firefox with the
+ // `network.preload` config disabled and we'll _never_ get onload/onerror
+ // callbacks. Let's try to confirm this with a totally invalid link preload
+ // which should immediately throw the onerror
+ if (stylesheetPreloadTimeouts >= 3) {
+ let linkLoadedOrErrored = await prefetchStyleLink({
+ rel: "preload",
+ as: "style",
+ href: "__remix-preload-detection-404.css",
+ });
+ if (linkLoadedOrErrored) {
+ // If this processed correctly, then our previous timeouts were probably
+ // legit, reset the counter.
+ stylesheetPreloadTimeouts = 0;
+ } else {
+ // If this bogus preload also times out without an onerror then it's safe
+ // to assume preloading is disabled and let's just stop trying. This
+ // _will_ cause FOUC on destination pages but there's nothing we can
+ // really do there if preloading is disabled since client-side injected
+ // scripts aren't render blocking. Maybe eventually React's client side
+ // async component stuff will provide an easier solution here
+ console.warn("Disabling preload due to lack of browser support");
+ isPreloadDisabled = true;
+ }
+ }
let styleLinks: HtmlLinkDescriptor[] = [];
for (let descriptor of descriptors) {
@@ -246,13 +276,12 @@ export async function prefetchStyleLinks(
(!link.media || window.matchMedia(link.media).matches) &&
!document.querySelector(`link[rel="stylesheet"][href="${link.href}"]`)
);
-
await Promise.all(matchingLinks.map(prefetchStyleLink));
}
async function prefetchStyleLink(
descriptor: HtmlLinkDescriptor
-): Promise {
+): Promise {
return new Promise((resolve) => {
let link = document.createElement("link");
Object.assign(link, descriptor);
@@ -266,16 +295,20 @@ async function prefetchStyleLink(
}
}
- link.onload = () => {
+ // Allow 3s for the link preload to timeout
+ let timeoutId = setTimeout(() => {
+ stylesheetPreloadTimeouts++;
removeLink();
- resolve();
- };
+ resolve(false);
+ }, 3_000);
- link.onerror = () => {
+ let done = () => {
+ clearTimeout(timeoutId);
removeLink();
- resolve();
+ resolve(true);
};
-
+ link.onload = done;
+ link.onerror = done;
document.head.appendChild(link);
});
}