#Prefetch is over fetching

513 messages · Page 1 of 1 (latest)

sleek nimbus
#

I'm test qwik 1.8.0, with default prefetchStrategy (Speculative prefetch)
In my q-manifest.json I've got 417 bundles.
When I navigate for the first time to a new page in SPA, I've got a total of 779 fetch requests and 110 scripts that took 1min to finalize (img 1).

Upon inspection some files are called multiple times (img 2)
When analysing the timing, we see that most of the time needed come from the service worker response (img 3)
@barren tiger I think you might be interested by this.

**Addendum **:
1min was with "Disabled cache" checked in network tab.
When enabling cache time is reduce to 10-20sec, which still is a lot for a navigation.

Alternative:
I did an experience by create one <link rel="modulepreload" href={bundle} /> for each bundle in the q-manifest.json and it took 500ms to download.

#

I forgot img 3 sorry

ashen shell
sleek nimbus
#

Ok, good know is that using linkInsert with linkRel: 'modulepreload' doesn't overfetch. This is a good alternative.

ashen shell
sleek nimbus
#

Yes, which is better in the end because you don't have to worry about the SW boot time and all. The only problem is that support is 91.57% while Cache API is 95.49%

barren tiger
sleek nimbus
#

Not from my experience. modulepreload cache the file and compile it, so it's a little faster to execute than only caching it. 400 files (1Mo) took 500ms to prefetch, I think it's ok.

ashen shell
#

yup @barren tiger I'm thinking Qwik could use modulePreload as first-class prefetch mechanism for the current page and fallback to SW (new or old) with NavigationPreloadManager for improved startup/fetching of resources

sleek nimbus
#

Beside browser support, the only issue I find is for offline app. But even there qwik would have to prefetch all the bundles with the server worker, not only the next one.

I think, with the current situation, making modulepreload the default prefetchStrategy would be a cheap solution while trying to improve SW for older browsers.

ashen shell
#

Well theoretically it sounds good, but it seems something's off in your preview. About ~400 files with a 404.
And it seems to be wanting to prefetch all those files on each page navigation, also seems to be happening on the main thread 🤔

So on slow 3G here it takes like ~20s for interactions to work (second screenshot). Open a new incognito tab, slow 3G, and click on hamburger menu/modal on smaller screen sizes.

sleek nimbus
ashen shell
#

Interesting!

It actually works pretty well on first page load! Only caveat I see is that it doesn't cache anything, so subsequent requests need to preload everything again. But if it's possible to put the modules in the cache once they're loaded, then modulesPreload could be a robust API to use.

Thanks for sharing 🫶

sleek nimbus
#

"subsequent requests need to preload everything again" you mean after reloading the page ?
I see a 304 status in the network when I reload, but I assume it's returned by Vercel. I'm not sure.

ashen shell
#

May I ask you to create an MRE of using modulepreload in Qwik?

#

Would help us in playing out with it

#

in case it changed since a year ago

sleek nimbus
barren tiger
ashen shell
barren tiger
ashen shell
# barren tiger Do we? As long as the sw puts it in the browser cache in the background I don't ...

Imo it's modulepreload or SW, not and.

  • modulepreload will prefetch the modules and parse, but won't cache them
  • current Service Workers will prefetch and cache

So if you use them together, both will prefetch which is not desirable.

What we could do is

  1. modulepreload and have a script (or SW 🤷‍♂️ ) to cache the modules once loaded
  2. if modulepreload non-supported, current SW strategy + NavigationPreloadManager
#

but I'm not sure 1 is even possible

sleek nimbus
#

I think you should :

  • listen to fetch event in the SW and cache the result (as any other basic SW)
  • let modulepreload prefetch what it needs

I would assume (didn't tested it) that the SW will catch the fetch request of the module preload except for the one that happen before SW is activated.

If module preload is not supported and cache API is (3% of browser use) you can fallback to speculative prefetch with cache API in SW

#

The <link rel="modulepreload"/> tags could have a qprefetch attribute to cache into the bucket. Like that you've got a single source of truth even if modulepreload is not supported

It might even simplify the current speculative prefetch code

sleek nimbus
#

There is a qwikPrefetchServiceWorker option in the renderToStream function.
It's doc says "Specifies if the Qwik Prefetch Service Worker script is added to the document or not".
It feels like it's something that should be removed with the new prefetch from 1.8.0 no ?

barren tiger
#

Qwik City uses its own

#

currently

sleek nimbus
#

ah right, it makes sense thanks.

sleek nimbus
#

I forgot how annoying working with service worker is ^^.

I've got some good news and bad news :
The good: I was able to start a SW, get all href of the <link rel="modulepreload" /> currently in the DOM and cache them. Then update the cache when there is more <link rel="modulepreload" /> added in the DOM.

The bad: It seems that prefetchStrategy doen't add more <link /> on navigation. But I think we could leverage the qprefetch event to add a <link />

#

Here is the code :
useModulePreloadCache.tsx

export const useModulePreloadCache = () => {
  useOnWindow('DOMContentLoaded', $(async () => {
    if (!("serviceWorker" in navigator)) throw 'Buy a new computer!';
    await navigator.serviceWorker.register('service-worker.js');
    await navigator.serviceWorker.ready;
    const modules = document.querySelectorAll('link[rel="modulepreload"]');
    const hrefs = Array.from(modules).map(link => link.href);
    const controller = navigator.serviceWorker.controller;
    // Initialize cache
    controller?.postMessage({ type: 'cache', value: hrefs });
    // Listen on changes: could listen on qprefetch event instead
    new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type === 'childList') {
          const newHrefs = document.querySelectorAll('link[rel="modulepreload"]');
          if (newHrefs.length !== hrefs.length) {
            controller?.postMessage({ type: 'cache', value: hrefs });
          }
        }
      }
    });
  }))
}

service-worker.js

const main = async () => {
  let cache;
  self.addEventListener('activate', async () => {
    cache = await caches.open('QwikModulePreload');
  });
  self.addEventListener('message', (message) => {
    if (message.data.type === 'cache') {
      const hrefs = message.data.value;
      hrefs.forEach(async href => {
        const res = await fetch(href);
        cache.put(href, res.clone());
      })
    }
  });
  // TODO react to a fetch event
}
main();
sleek nimbus
#

Alright, I managed to listen on qprefetch to do speculation prefetch.

  1. server set the inital <link /> with linkInsert
  2. browser prefetch the initial state
  3. activate service worker & start listening on qprefetch
  4. upon activation, main thread send the list on initial modulepreload to cache.
  5. on qprefetch event, add a modulepreaload
  6. modulepreload emit a fetch event that the service worker will cache if no

There is still some improvement to preload the dependancies along with the bundles. But it's a good start I think.

#

Here is the code :

useModulePreloadCache.tsx

export const useModulePreloadCache = () => {
 useOnWindow('DOMContentLoaded', $(async () => {
   if (isDev) return;
   if (!("serviceWorker" in navigator)) return;
   const base = document.documentElement.getAttribute('q:base') ?? '/';
   await navigator.serviceWorker.register('service-worker.js');
   await navigator.serviceWorker.ready;
   const modules = document.querySelectorAll('link[rel="modulepreload"]');
   const controller = navigator.serviceWorker.controller;
   
   // Initialize cache
   const hrefs = Array.from(modules).map(link => (link as HTMLLinkElement).href);
   controller?.postMessage({ type: 'cache', value: hrefs });
   
   // Listen on prefetch event
   document.addEventListener('qprefetch', (event) => {
     const { bundles } = (event as CustomEvent).detail;
     if (!Array.isArray(bundles)) return;
     for (const bundle of bundles) {
       const link = document.createElement('link');
       link.rel = 'modulepreload';
       link.href = join(base, bundle);
       document.body.appendChild(link);
     }
   })
 }))
}

service-worker.js

const main = async () => {
  let cache;
  self.addEventListener('activate', async () => {
    cache ||= await caches.open('QwikModulePreload');
  });
  self.addEventListener('message', async (message) => {
    cache ||= await caches.open('QwikModulePreload');
    if (message.data.type === 'cache') {
      const bundles = Array.from(new Set(message.data.value));
      cache.addAll(bundles);
    }
  });
  self.addEventListener('fetch', async (event) => {
    const req = event.request;
    const match = await cache.match(req);
    if (match) return event.respondWith(match);
    if (req.url.includes('q-')) cache.add(req);
  });
}
main();
addEventListener('install', () => self.skipWaiting());
addEventListener('activate', () => self.clients.claim());
barren tiger
#

what you're doing with a QRL is currently async

#

it also wakes up qwik core

#

which we don't want

#

sync$ -> similar to script tag

#

but with optimizations

#

also the isDev check isn't needed

#

rather than create a new sw, I think it would be beneficial to use the existing one

#

as this logic is duplicate

sleek nimbus
#

sync$ is fine for me. There were some weird behavior with the bundle path in dev, this is why I added it. I had to remove the PrefetchServiceWorker to disable current SW prefetch. There where no service worker instance running after that, so I created one.

Happy to improve the code tomorrow. At least we know it's working. And with this flow we won't experience any concurrency call between modulepreload & cache api.

barren tiger
#

Forgot about the prefetch logic

#

If you'd like I can show you around the new sw tomorrow

#

I think the implementation is not too complicated

#

All we need to do is tell the new SW when to prefetch

#

and also give it the hrefs you collected

#

I'll play with your code example 🙂

sleek nimbus
#

Sure, I would be happy to discuss about it tomorrow. It's just a PoC with a bunch of flows and improvement. But it prefetch with modulepreload based on qprefetch events and caches the result, it's a start.

barren tiger
barren tiger
#

@sleek nimbus @ashen shell btw this is also huge for dev mode

barren tiger
#

/build//src/components/counter/counter_component_button_onclick_sefr0m6h0sk.js

modulepreload gets the wrong path in dev mode, I think because we're doing something with the symbol mapper, but I think this can be fixed, and would really speed up Qwik in dev (while also staying true to its promise of disabled caching in dev)

cc @gaunt furnace

#

correct code is located at:

http://localhost:5173/src/components/counter/counter_component_button_onclick_sefr0m6h0sk.js
#

remove the base /build/ and the proper path is found

barren tiger
#

I've confirmed that we get all the bundles on the current page with the <link /> tags even on unsupported browsers 🙂

#

tested on firefox 110

#

so in this case, we check if it's supported, if not then we grab the info in the html from the link tags and send it to the sw for prefetching

barren tiger
#

@sleek nimbus made a bit of changes to the impl. btw why do we add more link module preload tags?

  // sync$ can't reference outside sources
  useOnWindow(
    "DOMContentLoaded",
    sync$(async () => {
      if (document.querySelector('[q\\:render="ssr-dev"]')) return;
      if (!("serviceWorker" in navigator)) return;
      const base = document.documentElement.getAttribute("q:base") ?? "/";
      await navigator.serviceWorker.register("sw.js");
      await navigator.serviceWorker.ready;
      const modules = document.querySelectorAll('link[rel="modulepreload"]');
      const controller = navigator.serviceWorker.controller;

      // Initialize cache
      const hrefs = Array.from(modules).map(
        (link) => (link as HTMLLinkElement).href
      );
      controller?.postMessage({ type: "cache", value: hrefs });

      // Listen on prefetch event
      document.addEventListener("qprefetch", (event) => {
        const { bundles } = (event as CustomEvent).detail;
        console.log(bundles);
        if (!Array.isArray(bundles)) return;
        for (const bundle of bundles) {
          const link = document.createElement("link");
          link.rel = "modulepreload";
          link.href = `${base}${bundle}`.replace(/\/\./g, "");
          document.body.appendChild(link);
        }
      });
    })
  );

in the public folder sw.js

same as what you had

const main = async () => {
  let cache;
  self.addEventListener("activate", async () => {
    cache ||= await caches.open("QwikModulePreload");
  });
  self.addEventListener("message", async (message) => {
    cache ||= await caches.open("QwikModulePreload");
    if (message.data.type === "cache") {
      const bundles = Array.from(new Set(message.data.value));
      cache.addAll(bundles);
    }
  });
  self.addEventListener("fetch", async (event) => {
    const req = event.request;
    const match = await cache.match(req);
    if (match) return event.respondWith(match);
    if (req.url.includes("q-")) cache.add(req);
  });
};
main();
addEventListener("install", () => self.skipWaiting());
addEventListener("activate", () => self.clients.claim());
#

these bundles we grab from the event detail don't seem to have the same hash as the modules that have been added to the html.

From the looks of it, it's qwik city, the new prefetch service worker file in core, and then it imports Qwik core, but I don't see it cache the actual code with the button click. I am testing with a regular counter component

barren tiger
sleek nimbus
sleek nimbus
#

@barren tiger I opened a PR on your repo :
https://github.com/thejackshelton/sw-playground/pull/1
Now <link rel="modulepreload" /> are added in devmode, we are just not caching in SW.
If modulepreload is not supported we use a regular fetch in the main thread that will be picked by the SW and cached.

GitHub

Devmode: Support modulepreload but do not start SW
Support: try to load an empty file to see if modulepreload is supported
Fallback: If modulepreload is not supported, do a fetch instead of addin...

barren tiger
sleek nimbus
#

No it doesn't

barren tiger
#

It seems that even in the case where something is cached, there is still a slight delay because the javascript engine needs to parse and compile it

#

Meaning the non-cached state <-- faster than the cached state 😅

barren tiger
# sleek nimbus No it doesn't

It seems the SW itself can't do the full range of module preloading, but if we can grab it from the browser somehow after 🤔

sleek nimbus
#

The cache API can store any file so the browser don't know what to do with it. It's just a file storage.
If the modulepreload requests a file that is in the cache and the SW is already activated, the SW will return the file, then modulepreload will parse and compile it. So when it's consumed this step won't be needed

#

Basically modulepreload does :

  1. Fetch
  2. Parse
  3. Compile

If the the SW catch the fetch et return something from the cache you "just" save a round trip to the network. But 2 and 3 should happen anyway.

barren tiger
#

I see, so even in the case where we are caching, we're saving the fetch step

#

?

sleek nimbus
#

It's very difficult to validate my hypothesis because there are no devtools for that. But this is what I understand.

sleek nimbus
#

But for initial load the SW is not yet activated, so the cache is not used

barren tiger
#

Because subsequent visits it would be in the cache and the sw would be active (ideally)

sleek nimbus
#

From my tests this is what happen :

**A. Initial visit **:

  1. module preload fetch initial modules
  2. SW is activated, main thread sends it the list of initial modules to cache
  3. on qprefetch a new module preload is added
  4. SW get the fetch event and cache the response for next time.

B. Next visit:

  1. module preload fetch initial modules, SW is not yet activated, we don't use the cache
  2. SW is activated, the cache is already filled from A.1
  3. on qprefetch a new module preload is added
  4. SW get the fetch event and returns the cached value from A.4.
#

A next step would be to leverage navigationPreload to create a concurrent call in B.1. between cache & modulepreload and get the fastest.

barren tiger
sleek nimbus
#

yes

#

For me the final picture for initial load would be :

  1. Early Hint preload header from the server
  2. modulepreload in the client
  3. NavigationPreloadManager to have concurrent calls
  4. SW cache check for preloadResponse

But this has to be tested in real life, maybe this whole process is worst than a simpler one.

barren tiger
#

Another thing we need to check is how we can know for sure when certain JS is executing

#

because the parse and compile step now fills the network tab

sleek nimbus
#

I guess this is the heart of qwik speculative prefetch. When is the best time to trigger qprefetch event

#

I wouldn't worry too much on the parse/compile operation though. Files are very small and devtools don't give us this information, so it's going to be hard to measure

barren tiger
#

have you confirmed this still works?

#

that is why I had the querySelector with ssr-dev

sleek nimbus
barren tiger
#

based on what I've seen with sync$

#

just want to confirm before merging since I want to show the proof of concept at the next core team meeting

sleek nimbus
#

I need to update the onload anyway 🙂

#

Btw, I just tested the preload manager, it works great.

  1. initial load: prefetch with modulepreload
  2. reload with disabled cache: use the cache
  3. reload without disable cache: use memory

So the browser will use the fastest solution on initial load

#

I'll add it to the PR

barren tiger
#

yeah I remember finding the preload manager and immediately thinking this would be useful for fetching the critical bundles while the SW boots up. Great find!!

barren tiger
ashen shell
#

Really cool exploration @sleek nimbus 🔥💪

barren tiger
#

if we could confirm from the SW cache as well

#

oh wait I didn't see it

#

service worker cache used in the middle

#

interesting that it uses SW cache even when caching is disabled

sleek nimbus
sleek nimbus
barren tiger
#

ah so it only disables memory cache?

sleek nimbus
# barren tiger ah so it only disables memory cache?

yes, there are so many layers of cache :

  1. Server / DB cache (managed by code usually)
  2. CDN cache (managed by headers usually)
  3. Memory cache from the browser (black box managed by HTML tags and browser behavior)
  4. Cache API (managed by JS)
#

This is one of the reason devs usually hate SW, it just add a new layer of annoying debugging when you want to change the img on the frontpage for christmas and the app gets stock on Halloween for some users XD

#

I pushed the changes into the PR:

  • use onLoad$ with sync$: it works on preview, I'm not sure for devmode
  • use document.documentElement.getAttribute('q:render') to check if is dev
  • add preload navigation manager to SW

There is still two remaining issues for me :

  1. I think qprefetch only sends the top level bundle. So currently we don't prefetch the imports from this bundle. We should verify that, and explicitly prefetch the import also if this is the case.
  2. linkInsert in devmode doesn't work because it prefix the bundles with /build

I think none of the issues above are critical for the Proof of Concept

sleek nimbus
barren tiger
sleek nimbus
barren tiger
#

let me take a look 🙂

sleek nimbus
#

If you've got an old browser installed on your computer, can you try to see if the file is still prefetch and stored in the cache ?

barren tiger
#

that's how I confirmed we still get the link tags in unsupported browsers

#

I'll check that here soon 👍

sleek nimbus
#

I appreciate. I really have bad experiences with bowsertack and would rather not use it again... Ever XD

barren tiger
#

Ok here's the two tests in firefox 110 locally

#

one is private browsing (left)

#

the other one is not but cache cleared (right)

#

on the right I see the caching coming from the SW

#

on the left it doesn't tell me where, but says it's cached

sleek nimbus
#

I think you need to disable the cache in the network tab, since native cache is faster than SW cache.

barren tiger
#

Ah yeah so it appears in private browsing mode (incognito) it does not use the SW

#

but it does outside of it

#

private browsing mode will only use the memory cache

#

so for most firefox users

sleek nimbus
#

Ok, that's good enough for me. It's not like we could do something about it ^^"

barren tiger
#

I think it will be using the SW

#

and this seems specific to firefox tbh

#

and firefox already supports it in later versions

#

so yeah haha

#

I think it does the job!

#

sw caching on unsupported browsers

sleek nimbus
#

Feel free to merge when you want. I won't be much available tonight. I would be happy to write an article about this if you decide to include it in qwik at some point.

barren tiger
#

Ah should already be merged

barren tiger
#

based off the testing we've done this looks like a clear improvement

ashen shell
#

Guys, I just noticed that using modulepreload only, the browser will serve chunks from a previous page with the memory cache 👀

#

and when tab is closed the re-opened, it serves from the disk cache

sleek nimbus
ashen shell
#

even after quitting the browser and re-opening afresh it uses the disk cache still 🤯

sleek nimbus
ashen shell
sleek nimbus
#

Btw, using the cache API we can do more than just caching the modules. We can cache font, img and also HTML files and even part of an HTML file !
It's out of the scope of this thread, but we can also leverage the service worker to store static HTML content. For example if you've got a static nav, you can do something like that :

addEventListener('fetch', (event) => {
  if (isPage(event.request.url)) {
    event.respondWith(async () => {
      // Merge sidenav with the content of the page
      const {done, response} = await mergeResponses([
        caches.match('/sidenav'),
        fetch(event.request.url)
      ]);
      // Wait until stream is complete
      event.waitUntil(done);
      return response;
    });
  }
})
ashen shell
#

would be useful for offline mode I guess?

sleek nimbus
#

Could be, but it reduces the amount of HTML the server has to compile and send to the client. If qwik is able to identify static chunk of HTML, it can store it in another file. Like that it only have to build the dynamic part, and the static part is send once, and reused from the service worker after.

ashen shell
#

Interesing! although a bit out of scope yeah and harder to implement I guess ^^

#

Do you see any hurdles we could run into using modulepreload for offline mode?

sleek nimbus
ashen shell
#

Tried modulepreload on Qwik UI with <Link> component, it seems the prefetching q-data.json still happens, but Qwik still performs an MPA navigation, so kinda broken

#

we'll need to fix that

#

cause SPA is also about retaining state between route navs

#

unless we find a way to do that with MPA

sleek nimbus
sleek nimbus
sleek nimbus
ashen shell
sleek nimbus
#

Don't cache the response of the fetch request for "q-data.json" for example.

#

I'm not in front of my computer so I cannot do a PR. But it would be something like if (!blacklist.includes(req.url) && req.url.includes('q-'))...

ashen shell
#

you think that would prevent the MPA navigation and force the SPA? 🤔

sleek nimbus
#

No, it'll just not cache the q-data.json so you won't reuse the same all the time

sleek nimbus
#

I'm sorry if I am not clear. Basically the problem you see is that the q-data.json response is cached by the SW because the URL has q- inside. This is a lazy technic I used to only cache qwik chunks (and not all img, font,...). Obviously this is not a good check because now the q-data.json is cached. Since it's cached every future request will return the cached version. Which is bad.
A short term solution would be to add a blacklist of file not to cache to prevent this issue.

Now I realize that you're talking about qwik UI and not the PoC repo, so maybe everything above is unrelated ^^". In qwik UI are you using the code I shared yesterday or just the linkInsert strategy?

ashen shell
modest narwhal
sleek nimbus
# ashen shell unless we find a way to do that with MPA

I'm not sure to understand the problem you're facing.
If you want to keep the state between navigation you can store a serialized version of the current state into the session storage, and fetch it back after navigation. You might have some conflict with the state returned by the server though.

barren tiger
ashen shell
#

it's probably a bug ^^

barren tiger
#

btw it does seem like FCP and LCP are consistently larger with modulepreload

#

affecting pagespeed scores

ashen shell
#

yup

#

I've been investigating the question for the past 2 hours 😅

barren tiger
#

still an improvement over delays on interactivity, but if we can improve that as well would be nice

ashen shell
#

also problem is that it looks scary on lightouse :/

#

like it's double than on pagespeed insights

barren tiger
ashen shell
#

chatgpt 4o:

Q:

I'm experimenting using Qwik (new framework like react) and modulepreload instead of the Service Worker to prefetch js chunks and compile them without executing them on page load. It seems to be working great, except for FCP and LCP.

I have about 120 <link href="/..." rel="modulepreload"> tags in the html document. Could it be what's affecting FCP and LCP?

A:

Yes, the excessive number of <link rel="modulepreload"> tags in your HTML document can negatively impact the First Contentful Paint (FCP) and Largest Contentful Paint (LCP). Here’s why:

  1. FCP Delay Due to Resource Overload: When you include too many <link rel="modulepreload"> tags, the browser is forced to fetch and potentially parse a large number of JavaScript files before it can start rendering the content. Even though modulepreload is designed to prefetch and compile modules without blocking rendering, the sheer volume can still cause a bottleneck, as the browser prioritizes fetching these resources.

  2. LCP Affected by Delayed Rendering: Since LCP measures how long it takes for the largest visible content element to load, if the browser is busy fetching and compiling all these modules, it might delay the actual rendering of your main content. This delay in rendering the crucial content on the page will lead to a poor LCP score.

Potential Solutions:

  • Reduce the Number of modulepreload Links: Evaluate which modules are critical for the initial render and only preload those. Lazy-load the rest after the initial content has been rendered.

  • Use HTTP/2 or HTTP/3: Ensure your server uses HTTP/2 or HTTP/3, which handle multiple simultaneous requests more efficiently. However, even with these protocols, too many parallel requests can overwhelm the client.

  • Optimize Module Chunking: If possible, break down your JavaScript into fewer, more significant chunks that can be preloaded, rather than many smaller files. This reduces the number of requests and might improve load times.

  • Preload Only Critical Resources: Focus on preloading only the essential resources that are needed for the first paint, and defer the loading of non-critical resources until after the first content has been rendered.

  • Prioritize Critical CSS: Ensure that critical CSS is inlined or loaded as quickly as possible to allow the browser to start rendering the page sooner.

By refining your strategy for preloading modules and focusing on critical resources, you should see an improvement in both FCP and LCP scores.

#

...
Q:

I verified with

window.performance.getEntriesByType("resource")
.filter(resource => resource.renderBlockingStatus === "non-blocking")

and all the resources are "non-blocking". Are you sure modulepreload can delay rendering of the page?

A:

You're correct that <link rel="modulepreload"> is intended to be non-blocking by design. This means that, under normal circumstances, these resources should not directly delay the rendering of the page. However, the impact on First Contentful Paint (FCP) and Largest Contentful Paint (LCP) can still occur indirectly, depending on various factors:

How modulepreload Works

  • Non-blocking Nature: modulepreload is non-blocking for the main rendering path, meaning it should not delay the browser’s ability to render the initial HTML, CSS, or images. The browser will fetch and compile these modules but won’t execute them until they’re needed.

Potential Indirect Impact on FCP/LCP

  1. Resource Contention:

    • Even though modulepreload is non-blocking, it still consumes network resources. If there are a large number of requests (like in your case with 120+ module preloads), the browser might experience resource contention. This could lead to slower fetching of critical resources (like CSS or images) if they are not prioritized correctly, indirectly affecting FCP and LCP.
  2. CPU Overhead:

    • The browser may spend CPU cycles on parsing and compiling these preloaded modules, even if they aren't blocking rendering. This additional CPU load could slow down other tasks, such as rendering or executing critical scripts, thereby affecting your metrics.
  3. Network Saturation:

    • Even with HTTP/2 or HTTP/3, which are designed to handle multiple simultaneous requests more efficiently, there is still a limit to how many resources the browser can handle at once without some perform
ashen shell
ashen shell
#

one naive approach we could try for the POC:

window.addEventListener('load', function () {
    const moduleUrls = [
        '/build/q-CUIYT6X2.js',
        '/build/q-2Iv1XkL0.js',
        // add other module URLs here
    ];

    moduleUrls.forEach(url => {
        const link = document.createElement('link');
        link.rel = 'modulepreload';
        link.href = url;
        // Set priority to 'auto' or 'low'
        link.as = 'script';
        document.head.appendChild(link);
    });
});
#

basically add the link tags with modulepreload after DOM content has loaded

gaunt furnace
ashen shell
gaunt furnace
#

Reading all that, it doesn't seem that modulepreload is better than having a sw

sleek nimbus
#

I don't think the issue is with modulepreload as it but the current implementation of linkInsert strategy because it adds a bunch of modulepreload in the initial html response.
Maybe you can only rely on the qprefetch events in the client and not use the linkInsert at all.

#

Also set fetchpriority="low" in the link tag might reduce the impact at loadtime

sleek nimbus
#

I deployed my app with modulepreload and disable current qwikPrefetchServiceWorker :

prefetchStrategy: {
  implementation: {
    linkInsert: 'html-append',
    linkRel: 'modulepreload'
  }
},
qwikPrefetchServiceWorker: {
  include: false,
}

I've got 150 <link rel="modulepreload" /> at initial load and get 100 on lighthouse.

@ashen shell did you remove the current service worker with :

qwikPrefetchServiceWorker: {
  include: false,
}
gaunt furnace
#

awesome! and I didn't realize the service worker always gets inserted? 🤯

sleek nimbus
ashen shell
#

(with modulepreload only! with current SW it's ~95)
But such a bad result is only on lighthouse, it's much better on pagespeed insights, although it's still doesn't match SW

#

also Qwik UI is on 1.7.3 ^^

#

Reading about modulepreload more in depth, it seems every meta-framework is encountering FCP & LCP issues with it

sleek nimbus
#

I'm confused. Why this is so low compared to previous tests if you're using v1.7.3 ? What did you change ?

ashen shell
#

But on webpagetest it's much much better

ashen shell
#

I'm starting to think that this is an issue with lighthouse throttling 😱

#

Simulated throttling, which Lighthouse uses by default

#

Packet-level throttling tools are able to make the most accurate network simulation.

#

WebPageTest uses packet-level throttling.

#

I also noticed that enabling throttling (e.g. 3G) on network tab in chrome makes everything super slow in the 20s TTI while in firefox it's like 3s 💀

#

btw pagespeed uses lighthouse under the hood for throttling x)

sleek nimbus
#

For SW and modulepreload or just modulepreload?

ashen shell
#

just modulepreload

sleek nimbus
#

Ok. I think we can keep modulepreload for speculative prefetch after load, and fill the SW cache from q-manifest instead of relying on initial modulepreload links

ashen shell
#

ah the WebPageTest was on desktop 🙈

#

here on mobile:

#

which doesn't make more sense x)

#

And to add even more confusion, here's the lighthouse score using "devtools throttling":

ashen shell
#

Well I guess only way to get real world data is to setup modulepreload on qwik.dev and wait for a month to see the results 🫠

barren tiger
#

Especially mobile

#

I would expect the same in dev mode too when it gets the correct url

#

This FCP and LCP issue seems to be the tradeoff

ashen shell
barren tiger
gaunt furnace
sleek nimbus
#

What should be the next step ?

barren tiger
# sleek nimbus What should be the next step ?

Figuring out how we can get the same module preloading benefits without pagespeed insights complaining a bunch about FCP and LCP, since it seems to be a big difference in lighthouse score.

So first thing we need to do is test if pagespeed insights is accurate on this metric, since it doesn't really make sense.

sleek nimbus
#

We could set fetchpriority="low" on the modulepreload links. But this should require an update from qwik core.

ashen shell
sleek nimbus
#

I'm not sure about the changeset, let me know if I need to put something else

barren tiger
#

looks like CI failing because of that

sleek nimbus
#

alright I pushed. Thanks for the tip 👍

barren tiger
#

Check the pr

sleek nimbus
sleek nimbus
#

@ashen shell do you want to do your test with this version ?
If you want me test the page speed can you share your protocol (repo, branch, config to test) ?

#

I found the place where the url ressource is built. Do you know if we can know if we're in production or devmode in this context ?
https://github.com/QwikDev/qwik/blob/main/packages/qwik/src/server/prefetch-strategy.ts#L80
I was considering using something like

const isDev = !bundleFileName.startwith('q-');

but if you've got something more robust in mind I would rather 🙂

GitHub

Instant-loading web apps, without effort. Contribute to QwikDev/qwik development by creating an account on GitHub.

barren tiger
#

That is pretty niche, but something to keep in mind

ashen shell
#

I'm not sure what you mean

barren tiger
ashen shell
barren tiger
#

I think the more robust check would be seeing if there is a q:render attribute that includes dev in it @sleek nimbus

sleek nimbus
# ashen shell I'm not sure what you mean

The issue, in devmode, is the linkInsert are prefix with /build, which break modulepreload. Since modulepreload's big advantage is that it would speed up dev, we need to fix this prefix.

barren tiger
#

and this could be on an html tag, div, span, whatever the container element the user has configured in the renderToStream config

#

so ideally finding it only by attribute

ashen shell
sleek nimbus
sleek nimbus
barren tiger
#

curious on if there's any delays with this

summer dawn
#

inject <link /> after dom ready then everything is fixed

#

this is a chrome/lighthouse issue only because they're dumb for having prefetch and preloading as a requirement for dom ready events etc. but this has been an issue for 8 years

sleek nimbus
ashen shell
summer dawn
#

I didn’t read any of the conversation before I posted. Jack just gave me a link to this convo

ashen shell
#

haha 😛

summer dawn
#

But yes move injecting the scripts until later

ashen shell
#

Anything that fixes those lightouse scores will be good enough imo

barren tiger
summer dawn
#

The way we did it at Walmart was inject the scripts in an array on the page and another bit of code just reads the array on dom ready and inject everything

sleek nimbus
ashen shell
#

whatever works

ashen shell
sleek nimbus
#

Would be great to have the option to do it on the server for those who don't care about scores ^^

barren tiger
#

if it doesn't, then you have a higher lighthouse score 🤷‍♂️

ashen shell
#

btw can't test your fetchPriority PR with pkg.pr.new cause it's on 1.8.0 and qwik-ui can't upgrade to 1.8.0 for other reasons (bugs)

#

so I'll make another PR I guess x)

#

just to test it ^^

sleek nimbus
#

Ah.. sorry I didn't thought about that.

ashen shell
#

not your fault, but not easy to workaround either :/

sleek nimbus
#

I can update the poc to only add modulepreload in the client but it'll be difficult to use as a reference to test lighthouse scores... And I cannot use my app because I don't see the performance downgrade

ashen shell
#

Yeah I think easiest way is to checkout 1.7.3 state of Qwik, and then apply your changes

#

I'll do it

sleek nimbus
#

Oh ! We can just use js-append !

#

This is exactly this behavior

ashen shell
#

I have slightly better FCP LCP with js-append iirc but still not very good

sleek nimbus
#

Maybe js-append + fetchpriority low

ashen shell
#

I'll give it a try too

sleek nimbus
#

Thx for all the trials 😉

barren tiger
#

I think we still want priority to be high though

#

interactivity is definitely the priority

#

I think we just want to confirm whether it's a lighthouse issue or not

#

and if that's the case use whatever we can to workaround the lighthouse score, because it will save us a ton of time explaining why people are getting lower scores 😅 (then in the meantime we file an issue with the chrome team)

#

it's not a good look, even if it's a bug in lighthouse

ashen shell
#

I guess it just means the ordering of the chunks will be a bit different

barren tiger
#

@ashen shell what y'all could do is run the preview of the qwik docs with his fetchPriority changes and see if that affects local scores first. Since if I recall this was also bad locally w/ lighthouse?

#

wont be able to take another look until after work

ashen shell
#

lighthouse seems even less accurate in preview 🫠

#

I must try on qwik UI

barren tiger
#

Upgrading to 1.8, replacing accordion mdx route with tsx and import examples

#

Getting something like cursor or chat gpt

#

@modest narwhal think you could add another deploy on your site with 1.8 and the module preload?

barren tiger
#

It did

#

I see the pages deploy

ashen shell
barren tiger
#

Ah shouldn’t make a difference

#

Lots of stuff on the qwik site too

#

But yeah qwik ui would be a better direct comparison

ashen shell
#

🤞

barren tiger
#

I see it building in cf from my phone haha

#

Ah remove qwik nx plugin

#

Qwik nx vite

#

cc @ashen shell

sleek nimbus
#

So exciting ^^

modest narwhal
barren tiger
#

One deploy with that

modest narwhal
#

ok shouldn't be too long

barren tiger
#

which page do you think has the most interactivity @modest narwhal ?

#

it seems to me that there is some long requests. a bit of a side note, are you using the qwik ui modal? I don't seem to be able to close when opening up the top layer dialog element

modest narwhal
barren tiger
#

this is stuff we tackled in qwik ui headless

#
  • the aria
#

in case you were looking to improve that experience

#

bit of a side note though haha

#

yeah seems to me this fetchPriority low version seems to have had no impact on improving the FCP and LCP scores

#

@modest narwhal can you show real quick how you added it?

modest narwhal
modest narwhal
modest narwhal
barren tiger
#

It seems to me the interactions are snappy when there's not some sort of request or data being fetched

modest narwhal
#

oh sorry just re-read your message

#

to me it seems like it improved a bit FCP (from 4.9s to 3.0s), so back to how it was with 1.7.3

barren tiger
#

yeah there is a lot of external resources being loaded so it's hard to tell with interactivity

modest narwhal
#

yep I'm not sure my case is the best use case TBH as it's more like a unoptimized POC rather than a solid product

barren tiger
#

thanks for letting us test

modest narwhal
#

anytime!

#

also, I would still consider myself as a Qwik noob so if you have any recommendations of things I could/should change that would also make it a better use case for you to keep testing more changes, let me know

happy to serve as a guinea pig 🐹

#

(and yes, I'll gradually switch to qwik-ui headless 😉 )

void quail
barren tiger
void quail
#

I have a relatively large project, and I am having this problem, but I see it as a bug, because sometimes the same file is downloaded hundreds of times.

barren tiger
void quail
#

It is not that you are “prefetching” from another page, but that you are downloading hundreds of times the same file, usually an “onDocument” file or similar.

barren tiger
#

the sw prefetching included the downloading or "buffering" of the JS files

#

would read through the discussion

void quail
#

Oh okay, thanks, I will read the posts in this “thread”.

barren tiger
#

the TLDR is for the time being I'd recommend removing the SW in root.tsx, and in entry.ssr.tsx try modulepreload inside of renderToStream with html-append

#

(also don't change the fetchPriority)

void quail
#

Thanks!

modest narwhal
#

@void quail your entry.ssr.tsx should look similar to this:

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    // ... other options
    prefetchStrategy: {
      implementation: {
        linkInsert: "html-append",
        linkRel: "modulepreload",
      },
    },
  });
}
barren tiger
#

^

void quail
modest narwhal
#

@barren tiger as 1.8.0 is already out there, is there somewhere a release note/migration guide that would recommend to do this while upgrading to 1.8 (couldn't find any)? Otherwise, I would agree with @void quail that from the point of view of a developer who's just using the framework, it feels like a major bug.

void quail
#

Besides, in my case, the problem does not occur all the time, but sometimes, and with different files and download numbers. So it makes it look like a major bug.

barren tiger
#

I don't think there is a migration guide no. My understanding is it's a bug that has always existed, but changes to the optimizer have made it more apparent

#

It is a bug yes.

#

If you're using a SW I would recommend staying on 1.7.3 until the next release

modest narwhal
#

Ok I'll document this in the issue you referred caeg so at least it's written somewhere

void quail
#

Sorry, what do you mean by “If you're using a SW”?

modest narwhal
#

If in your root.tsx file you are using the ServiceWorkerRegister component that comes by default when creating a new project using qwik-city

void quail
#

Ah ok, haha, sorry, I didn't understand what the acronym “SW” referred to.

modest narwhal
#

Oh right, SW = Service Worker 😉 sorry!

modest narwhal
void quail
#

@modest narwhal I see everything fine.

#

One question, is a solution already planned for the next version, or is it not yet known how to solve the root cause of the problem?

barren tiger
#

the old one is eventually going to be deprecated in place of the new one

#

<ServiceWorkerRegister /> is the old one

#

<PrefetchServiceWorker /> and <PrefetchGraph /> are the newer components from core, and replace the old one

#

both service workers have the problem of trying to download the entire application 😬

#

they should instead only download on a route basis

#

modulepreload is the research we are doing to make the prefetching system more robust, so that this is not something consumers have to worry about.

#

it currently has 92% support, ideally in the future we will use it with a SW fallback and the FCP and LCP issues will no longer persist

#

we're currently analyzing whether these issues are lighthouse being annoying or an actual problem

void quail
#

Oh, okay, thank you very much for the answer.

barren tiger
sleek nimbus
#

I would like to summarize just to be sure :

  • SW + Speculative prefetch: currently interactivity is not too good + we cannot prefetch in dev mode
  • modulepreload + html-append: interactivity is good + support prefetch in dev mode (modulo a PR), but it increases FCP & LCP (event with fetchpriority: 'low')
  • modulepreload + js-append: same as html-append but worst

@ashen shell suggested to append the modulepreload after load instead of inserting them in the server. This is the behavior of js-append, but it currently adds an inline script (blocking). The best would be to update the js-append implementation run the code after load. The code is here :
https://github.com/QwikDev/qwik/blob/main/packages/qwik/src/server/prefetch-implementation.ts#L97
Note: even though the type of the script is module, it has no effect since the code is inlined.

GitHub

Instant-loading web apps, without effort. Contribute to QwikDev/qwik development by creating an account on GitHub.

ashen shell
#

It seems with fetchpriority: 'low' only LCP is bad, but I'm still trying to test on Qwik UI, not easy to get there 💀

#

If that indeed doesn't help for LCP, then we can try to append on load or DomContentLoaded which I understand is fired sooner

sleek nimbus
barren tiger
#

This also means potential delays for what seems to be a pretty small benefit

#

I think we want to try to change the js-append implementation

sleek nimbus
barren tiger
#

@ashen shell ^ currently away from home

gaunt furnace
#

I must say it would be great if this works, service workers are fickle. Do we have something that looks at the import graph to prevent waterfall fetches?

barren tiger
gaunt furnace
barren tiger
ashen shell
# gaunt furnace Sure, you need to read the imports of all the bundles you're prefetching, in the...

From MDN:

Preloading allows modules and their dependencies to be downloaded early, and can also significantly reduce the overall download and processing time. This is because it allows pages to fetch modules in parallel, instead of sequentially as each module is processed and its dependencies are discovered.

A browser may additionally also choose to automatically fetch any dependencies of the module resource. Note however that this is a browser-specific optimization — the only approach to ensure that all browsers will try to preload a module's dependencies is to individually specify them! Further, the events named load or error fire immediately following success or failure of loading the specified resources. If dependencies are automatically fetched, no additional events are fired in the main thread (although you might monitor additional requests in a service worker or on the server).

I guess that's a good idea 👀

sleek nimbus
barren tiger
ashen shell
#

Ah me neither 🙈

gaunt furnace
#

Yes perfect

#

Oh you should add my pr

sleek nimbus
sleek nimbus
#

I did a small test js-append (after DOMContentLoad) vs html-append

  • js-append (img 1) the script takes ~16ms (1 frame) to run, but it seems the LCP/FCP happens before the script is done. So for me the script has no impact on LCP/FCP.
  • html-append: (img 2) the requests are send from the start, it takes 9ms which postpone the LCP/FCP by 9ms. I read something about it, I think the browser analyze the HTML for img, link & script, before everything.

Overall with 130 modulepreload, locally, there is a ~10ms delay for LCP/FCP between js-append (after DOMContentLoad) & html-append in favor of js-append.

An the over hand, prefetch happens from the start on html-append and after DOMContentLoad (~160ms + 16ms) for js-append

#

This comparison has been done on a preview, with no cache, on chrome

ashen shell
#

Results for Qwik UI aren't very conclusive on pagespeed (did them there as default lighthouse results vary even more) 👇

Reference with Service Worker (service-worker.js):

Using modulepreload w/ SSR:

  1. modulepreload + html-append
  1. modulepreload + html-append + priority low
  1. modulepreload + js-append + priority low

Using modulepreload w/ DOMContentLoaded

  1. modulepreload + html-append
  1. modulepreload + html-append + priority low
  1. modulepreload + js-append + priority low

Ran the tests 7 times for each. Got one 91 and one 61, on average it's ~72

Conclusion
It's not easy to trick lighthouse and the best results seem be "no tricks" (just modulepreload + html-append).

The last thing I can think of to improve those lighthouse simulations scores is to add the modulepreload link tags on visible.

In any case I believe we should test this with real users to see if it's only the default lighthouse simulations or if it also happens on real user devices with real connections (no throttling). qwik.dev is the perfect candidate for doing that since it's not mission critical. This will take a month or more if we want to test more than 1 setup.

ashen shell
#

I'm confused, how is LCP before FCP in these graphs 😳

sleek nimbus
sleek nimbus
ashen shell
#

unless those results get exponentially mutliplied on slow 3G

sleek nimbus
ashen shell
#

but theoretically modulepreload is non render-blocking so shouldn't increase FCP LCP.
It could very well be that lighthouse/pagespeed/WebPageTest are wrong and just can't properly test sites with modulepreload since they're all simulations of slow network through different kinds of throttling

sleek nimbus
#

It's non render blocking but it increase time of the first parser (by a small fraction, maybe 1 or 2 frames).
I think it might also affect FCP if you've got an img in the content as it'll be prefetch at the same time as modulepreload.

barren tiger
#

Tried testing modulepreload + html append with browser stack (this is the cumulative result of 5 tests), FCP sure seems quite different than pagespeed. This is 4G throttled with an iPhone XS on Safari.

https://www.browserstack.com/docs/speedlab/guide/scoring-logic

although perhaps these are local scores so the testing is hardware based?

in either case, I haven't yet gotten any of the slowness that is reported with pagespeed insights in FCP and LCP.

When going to the page even with browser stack remote devices on 4G throttling (mobile) it seems to load pretty fast. Definitely not 2.4s to see the FCP. I usually see it pretty instantly, it may take that amount of time for the page to be fully finished loading.

sleek nimbus
#

So overall we can settle with modulepreload ?
If so, I think the remaining tasks would be :

  1. Support correct URL in dev mode
  2. Move the links into the <head> tag instead of <body>
  3. Implement the SW to cache requests from modulepreload (see PoC) in prod
sleek nimbus
#

Also, I feel that we don't need to add the <script q:type="prefetch-bundles"> tag in the DOM if we rely on <link rel="modulepreload">.
@barren tiger do you know if it's still needed ?

ashen shell
#

TLDR of our investigation so far:
WebPageTest simulation (Packet-level throttling) => worst FCP & LCP & TBT 👎
Default Lighthouse simulation (Simulated throttling) => worst FCP & LCP 👎
PageSpeed simulation (which is based on default lighthouse so simulated throttling) => worst FCP & LCP (although not as bad as lighouse) 👎
Lighthouse DevTools throttling simulation (Request-level throttling) => better TBT, unchanged FCP & LCP 👍
Browser stack simulation (not sure which kind of throttling) => good FCP 👍

You can learn more on the different approaches to throttling on lighthouse's docs: https://github.com/GoogleChrome/lighthouse/blob/main/docs/throttling.md

#

Plagiarizing myself:

the only thing I can think of to improve those lighthouse simulations scores is to add the modulepreload link tags on visible.
but that may come at the cost of degrading the real user experience or even lead to bugs.

If we want to recommend modulepreload as the go to approach:

we must test this with real users to see if it's only the default lighthouse simulations or if it also happens on real user devices with real connections (no throttling). qwik.dev is the perfect candidate for doing that since it's not mission critical. This will take a month or more if we want to test more than 1 setup.

sleek nimbus
#

"add the modulepreload link tags on visible." by "on visible" you mean after DOMContentLoad ?

ashen shell
#

Using Intersection Observer API

#

as it would reduce the number of preload tags added to the dom, which is what the simulations don't like I assume

sleek nimbus
#

This is what js-append is doing, adding all the required module for the interactivity of the current page.
In my PR I delay the append child until DOMContentLoad, which is a good spot to prevent any effect on FCP/LCP

ashen shell
ashen shell
#

also probably would have to wake up the framework which is anti Qwik

sleek nimbus
#

We can wait for another dom event that happen after DOMContentLoad.
I'm surprise that running a small script on DOMContentLoad has that much effect on the FCP/LCP.

ashen shell
sleek nimbus
#

Have you try with no prefetchStrategy at all ? Normally it should be the fastest

ashen shell
#

'm surprise that running a small script on DOMContentLoad has that much effect on the FCP/LCP.
Reading the results again, the effect of doing it on DOMContentLoaded seems to have almost no effect on the simulations.

ashen shell
sleek nimbus
#

I thought 1) was with htmk-append. I mean with no initial prefetching at all (no modulepreload, SW, nor web worker)

barren tiger
#

Try removing it and see if that changes lcp and fcp

ashen shell
ashen shell
#

I updated the "benchmark" #1276861655833383034 message with current Qwik UI with SW. No SW is actually kinda pointless since it's a terrible user experience to lazy load from network on every interaction.

#

As explained in #code-contributors, let's continue the discussion in there.

floral reef
#

sorry for jumping on your convesration but 😛 I see nobody has mentioned Qwik Insights, is that not necessity to solve the prefetching based on your current route? We try to use it and our insight projects eventually die and stop working entirely (maybe too much data).

Is there a best practise when it comes to prefetching and avoiding the loading of scripts on interaction? We tried alsorts and in the end just gave up and resorted to loading all scripts until we have time to figure it out:

prefetchStrategy: {
  implementation: {
    linkInsert: 'html-append',
    linkRel: 'prefetch',
    workerFetchInsert: 'no-link-support',
  },
  symbolsToPrefetch: ({ manifest }) => {
    const buildBase = extractBase(opts);
    return Object.keys(manifest.bundles).map((bundleFileName) => ({ url: `${buildBase}/${bundleFileName}`, imports: [] }));
  },
},
barren tiger
#

Also Qwik Insights is opt-in, so you could solve it that way, but then it's still broken for people not using insights (most).

If we could have a way for everyone to be on insights then that could work, but seems unlikely since the data needs to be stored and that's a potential cost.

#

I think the solution is module preloading enabled and then on unsupported browsers the SW grabs the link href tags (bundles) in the html and performs a fetch.

floral reef
#

Insights has never really worked for us 😛 if I create a new project it works, then eventually all of our projects return just:
{"type":"smart"}
Is there a better stop-gap solution to this to avoid interaction delay?

symbolsToPrefetch: ({ manifest }) => {
  const buildBase = extractBase(opts);
  return Object.keys(manifest.bundles).map((bundleFileName) => ({ url: `${buildBase}/${bundleFileName}`, imports: [] }));
},

We found it was a better user-experience to just prefetch 2mb of JS (until prefetching/insights works properly). It felt painful to merge that load everything change but I couldn't argue 😛 I keep telling my team it's the 1kb streaming framework but in reality I still haven't been able to prove it

barren tiger
#

then in fallback browsers you add in the way you've previously prefetched

#

I keep telling my team it's the 1kb streaming framework but in reality I still haven't been able to prove it

You want buffering. The downloading is the negligible part. JS execution is the expensive bit.

sleek nimbus
# floral reef Insights has never really worked for us 😛 if I create a new project it works, t...

It looks like you're already using modulepreload with webworker as fallback. I'm not sure how symbolsToPrefetch works but if adds link tag in the initial html I wouldn't do that because it'll impact your initial load.
The issue with html-append is that it doesn't prefetch on qprefetch event, which is why you're experiencing latency. Also I don't think it runs the service worker so, since modulepreload doesn't check browser cache, each request is sent to the server, which is slower.

Here is a code example to load the SW and add modulepreload on qprefetch
https://github.com/GrandSchtroumpf/qwik-hueeye/blob/main/src%2Fcomponents%2FuseModulePreload.tsx

And here is the service worker:
https://github.com/GrandSchtroumpf/qwik-hueeye/blob/main/public%2Fsw.js

floral reef
#

the above is an alternative to the new QwikPrefetchWorker, but the changes are in consideration to go into it officially after testing/verification right? as I read you can only have 1x SW on a URL at once

sleek nimbus
floral reef
#

I mean, QwikPrefetchWorker overfetching sounds better than what we do with load-everything, so maybe it would be good for us to move to that for now

gaunt furnace
sleek nimbus
modest narwhal
rich tide
#

after upgrading to 1.8.0 we also notice a lot of never ending pending scripts blocking the app fully loading, that does not happen in 1.5.7. Sharing if its any indication to the issue

ashen shell
rich tide
#

sure trying now

rich tide
#

yes that resolved it, thanks 🙌

floral reef
#

I wonder if the way we "prefetch everything" was the reason our Insights constantly break and we have to create a new project each time:

symbolsToPrefetch: ({ manifest }) => {
  const buildBase = extractBase(opts);
  return Object.keys(manifest.bundles).map((bundleFileName) => ({ url: `${buildBase}/${bundleFileName}`, imports: [] }));
},

That causes 1.7k JS requests on 1.8.0 (1k requests on 1.5.7) and maybe Insights is saying: "ok the homepage needs 1k JS files, the product page needs 1k JS files, the game page needs 1k JS files" and then it eventually chokes and gives up cause we send/download too much insight data

gaunt furnace
#

that makes sense yes

modest narwhal
# rich tide after upgrading to 1.8.0 we also notice a lot of never ending pending scripts bl...

See this and this for more info

GitHub

Which component is affected? Qwik Runtime Describe the bug I tried to update to qwik from 1.7.2 to 1.8.0 today (no other deps were updated). TLDR: In my opinion it seems like the service worker is ...

GitHub

use prefetch sw with docs

It was adding dynamic imports, which is not correct. Those should be handled by Link prefetching and Insights.
Furthermore, the graph is reduced by not including dependen...

ancient plover
#

Been reading through this and the links I think you may have missed something here…

First time I read this chat so it was a lot to take in but if you are using the
NavigationPreloadManager

https://developer.mozilla.org/en-US/docs/Web/API/NavigationPreloadManager

@sleek nimbus referenced this file

I’m just highlighting this line

https://github.com/QwikDev/qwik/blob/fix-prefetch/packages/qwik/src/server/prefetch-utils.ts#L5

And looking at @gaunt furnace commit I started looking through it I found this line

https://github.com/QwikDev/qwik/blob/fix-prefetch/packages/qwik/src/prefetch-service-worker/state.ts#53

Really don’t see where the fetch is being called anywhere *except the main SW listener just the type from ./state … I got from its usage in .
https://github.com/QwikDev/qwik/blob/fix-prefetch/packages/qwik/src/prefetch-service-worker/setup.ts#L21

directFetch function from ./direct-fetch.ts

I think you may need to add another method and filter / make the SW fetch listener determine wether the request is a preload request or a normal fetch request …

For example:

https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/preloadResponse#examples

I’m not 100% sure since I’ve only been reading this for like 45 mins but I don’t see that being used in the PR from @gaunt furnace
Not sure about @sleek nimbus repo since he’s removing the sw? But the utils are the same

Screen shot ref:
https://web.dev/blog/navigation-preload#header

MDN Web Docs

The NavigationPreloadManager interface of the Service Worker API provides methods for managing the preloading of resources in parallel with service worker bootup.

GitHub

Instant-loading web apps, without effort. Contribute to QwikDev/qwik development by creating an account on GitHub.

GitHub

Instant-loading web apps, without effort. Contribute to QwikDev/qwik development by creating an account on GitHub.

GitHub

Instant-loading web apps, without effort. Contribute to QwikDev/qwik development by creating an account on GitHub.

MDN Web Docs

The preloadResponse read-only property of the FetchEvent interface returns a Promise that resolves to the navigation preload Response if navigation preload was triggered, or undefined otherwise.

#

^^ the Warning ⚠️ is the important part

#

Seeing as everyone is complaining about the thing fetching multiple times

#

I don’t think that applys to all the prefetch strategies and I’m not sure which one is being used in the PR… the only thing that stood out to me was @barren tiger or someone mentioned the NavigationPreloadManager.

But as you navigate through the docs you you find

https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/preloadResponse

Which states that is only available in SW so dunno how that applies to the trickery your trying to do of getting the <link/> from the DOM call / html instead of using the SW directly…

That or some combination of the two… since @sleek nimbus was testing the <link/> with the DOM then passing to SW, maybe not all of it needs to be thrown in the DOM maybe just the ones relevant to that URL get conditionally rendered per URL endpoint and loaded to the DOM and the rest of the map / qdata.json gets pushed to the service worker that way you get the benifit of both and don’t have all those server requests to fetch all the <link/> on the main thread

  • it seems what I described above of combination may already be automatically done by the NavigationPreloadManager
    That doesn’t mean that it may or may not be better to serve all the <links /> to the Main Thread ( would have to if no SW)
MDN Web Docs

The preloadResponse read-only property of the FetchEvent interface returns a Promise that resolves to the navigation preload Response if navigation preload was triggered, or undefined otherwise.

gaunt furnace
ancient plover
#

that the FCP etc is slow due to a large amount of requests ? while using it? from what I gathered their PoC was using that ?

gaunt furnace
ancient plover
#

i dunno whats going on here

#

lol im doubting what I read earlier I thought they enabled that NavigationPreloadManager but im double checking

#

because it says if its "triggered" but what can "trigger" it is not mentioned (assuming JS / eventListener as the obvious "trigger") so Im checking to see if the <link rel="..." /> rel type "triggers" this or not, if they didn't add the NavigationPreloadManager then it wouldn't apply to this

ancient plover
#

but as I read it for the first time today, what I gathered is that even if the service worker is shutdown it'll start the prefetch before the service worker is active and the SW has some awaitable that receives it so I assume the Main thread receives and loads what it can before the SW is active

sleek nimbus
#

Concerning the navigationPreload I misunderstood how it was working.
We initialy added to the PoC thinking it would grab all request at navigation time. But it only work with the html documents at navigation time.
This is a powerful feature that should be analysis, but it's unrelated to speculative prefetch. So I removed it from the PoC since.

#

Currently qwik uses the service worker for caching the JS bundle ahead of time (speculative prefetch).
But it doesn't do document and asset caching, this is where Navigation Preload has its place.
I think these is room for a @qwil/pwa lib that would provide this kind of service (asset and documents caching strategy, versioning, offline support, installable app...). But it's not directly linked to speculative prefetch.

gaunt furnace
#

Right the way I understand it, any navigation fetch requests by the browser are normally blocked until the SW starts, and now they can be fetched while it loads but the SW still has to fetch the same things and return them.

#

So possibly that includes js files mentioned in link ?

#

Actually we should check with a delay in the SW

sleek nimbus
#

It's not exactly that. The initial request to the HTML file is delayed until SW boots. So every JS will be handled by the service worker, because it has already booted up.

If we use NavigationPreload, the browser will use the preloaded HTML file even before the SW boots. If we use html-append, then the SW might not be booted up at this moment, so we might either hit "disk cache" or Service worker (if booted up).

I removed the Navigation Preload from the PoC because it would require more work (SW versioning, ...) to prevent stale document to be kept preloaded. I think it's a powerfull tool, but we need to do it carefully to not introduce side effect. Since modulepreload is already under heavy discussion I don't want to add that to the conversation right now ^^

modest narwhal
gaunt furnace
#

BTW I changed the prefetch PR and now it does prefetch task qrls but at a lower priority

ancient plover
#

do you think its your for loops that are causing the bug ?

I remember seeing acouple for loops that used const instead of let... could that be where things are being "duped" ? since the for const creates multiple bindings vs for let with the single binding ?

gaunt furnace
ancient plover
# gaunt furnace Hmmm, no. I think it was just fetching way too much at the wrong priority

yeah I sarted reading up on the whole headers and cache thing alot of info there to digest, but it could just be how different people have their setups for example the default layout.tsx comes with

export const onGet: RequestHandler = async ({ cacheControl }) => {
  // Control caching for this request for best performance and to reduce hosting costs:
  // https://qwik.dev/docs/caching/
  cacheControl({
    // Always serve a cached response by default, up to a week stale
    staleWhileRevalidate: 60 * 60 * 24 * 7,
    // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
    maxAge: 5,
  });
};

which some people ( atleast on youtube for example just remove all the default stuff out after npm create) then there would be no cache directive persay and will default to something else which might cause the thing to fetch more

  • maybe some test needs to be made with all the cache control types for the request as a variable and the service workers different cache control types to see how they afffect eachother maybe its been done already by whoever wrote up the
    https://qwik.dev/docs/caching/
Qwik

Learn how to handle caching in your Qwik City app to keep your site as fast as possible.

sleek nimbus
#

You're talking about the cache header response from the backend. This is an indication to the CDN/server how to cache the response. The service worker won't be impacted by that.
The service worker acts as a server proxy, it leverage the Cache API fromt the browser, which is not managed by headers but by JavaScript.

chilly jackal