From a6a0dc2649f2a8b06ff5a0ed5b9d53eb1919a0af Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 8 Aug 2023 16:05:38 -0400 Subject: [PATCH] Handle disabled preload config in firefox (#7106) --- .changeset/disabled-link-preload.md | 5 +++ packages/remix-react/links.ts | 49 ++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 .changeset/disabled-link-preload.md 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); }); }