vitejs / vite

Next generation frontend tooling. It's fast!
http://vitejs.dev
MIT License
67.23k stars 6.05k forks source link

[✨Feature Request]: Add `prefetch` option for async chunks #10600

Open xsjcTony opened 1 year ago

xsjcTony commented 1 year ago

Description

English:

Webpack enables developer to prefetch an async chunk using magic comment /* webpackPrefetch: true */ which will make a <link rel="prefetch" src="..."> tag in <head> Code Splitting | webpack

// LoginButton.jsx
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

will result in <link rel="prefetch" href="login-modal-chunk.js">

It's nice to have the same feature in Vite, but I currently find no alternative.

I tried adding those <link> tags manually using IntersectionObserver whenever an internal link goes into the user's view that the user is likely to click on, just like what VitePress did. (https://vitejs.dev/ did this)

But the thing is, I'm NOT able to access the hash of the bundled chunk file, hence it's pretty hard to do unless I remove the hash from bundled chunk file (so the filename is predictable) but I don't think it's a good way to do so, as it may result in more problems (naming conflict, etc.).

Thus, I think the only solution is that vite can implement this feature internally. (maybe an extra feature for something similar to /* webpackPreload: true */ as well which result in <link rel="preload" ...> tag)

I personally think adding prefetch for lazy loading routes is pretty important to UX, but I just can't implement it at the moment.


中文:

Webpack 可以通过 /* webpackPrefetch: true */ 让开发者 prefetch 一些异步模块, 通过 <head> 中的 <link rel="prefetch" src="..."> 实现. Code Splitting | webpack

// LoginButton.jsx
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

结果为 <link rel="prefetch" href="login-modal-chunk.js">

我希望 Vite 也有类似的功能, 但是我没有找到相关的内容.

我试着使用 IntersectionObserver 手动添加这些 <link> 标签, 每当有app内部的超链接 (路由跳转之类的) 出现在用户的 viewport 里, 也就是说用户有概率点击这个链接. (就像 VitePress 使用的那样, 使用在了 Vite 的官网上).

但是我无法做到, 因为我没有办法访问打包之后异步模块的 hash, 所以我不知道应该加载的文件名 (确实我也不应该知道, 因为这是在开发中不可预测的). 唯一的解法是在打包时不使用 hash, 但我并不认为这是一个好的选项, 因为他会导致更多的问题 (比如命名冲突).

所以我觉得如果只能让 Vite 开发这个功能 (因为能够获取打包之后的 hash (大概吧?)), 然后让用户通过一些方式使用. (可能也可以顺便开发一个类似 /* webpackPreload: true */ 的功能, 结果为 <link rel="preload" ...> 标签)

我个人认为针对懒加载路由做 prefetch 是挺重要的, 但是我目前没法实现.


Related: #5818

Suggested solution

Something similar to this

// LoginButton.jsx
import(/* vitePrefetch: true */ './path/to/LoginModal.js');

will result in <link rel="prefetch" href="login-modal-chunk-with-hash.js"> in <head>

Alternative

No response

Additional context

No response

Validations

bluwy commented 1 year ago

Currently Vite automatically preloads dynamic imports by default (rel="preload"), which should work better than prefetch in most case. Does this feature currently doesn't work for you? Or if there's a reason you're looking for prefetch specifically?

xsjcTony commented 1 year ago

I think it doesn't. If you mean modulepreload, it only applies to entry chunks and their direct imports. and preload is very different from prefetch,

where preload is

specifying resources that your page will need very soon, which you want to start loading early in the page lifecycle, before browsers' main rendering machinery kicks in.

and prefetch is

a hint to browsers that the user is likely to need the target resource for future navigations, and therefore the browser can likely improve the user experience by preemptively fetching and caching the resource.

Vite definitely won't prefetch the resources of potential future navigations by users. A use case can be lazying-loading routes, #5818 is a great example.

Also, if you go to Vite | Next Generation Frontend Tooling, and you will fine those in the <head> element: image The technical that VitePress uses is adding a prefetch <link> tag dynamically when IntersectionObserver finds a new link that appears in the user's viewport, hence the resources are prefetched.

I'm not sure and have not tried yet to use the same strategy to get the build file name of chunks as VitePress did (which is stored in window.__VP_HASH_MAP__), but I mean it's way too complex to implement that. It would be good if vite provides a native feature that I can specify which lazy loaded route is going to be prefetched during runtime.

xsjcTony commented 1 year ago

Corresponding VitePress source code: https://github.com/vuejs/vitepress/blob/main/src/client/app/composables/preFetch.ts#L66-L69 https://github.com/vuejs/vitepress/blob/4b656feafe45e35cf86bc4b47203ec4e0bc6bdc5/src/client/app/utils.ts#L44 https://github.com/vuejs/vitepress/blob/4b656feafe45e35cf86bc4b47203ec4e0bc6bdc5/src/client/app/router.ts#L120-L122

fr0stf0x commented 1 year ago

any news?

bepan commented 1 year ago

Is this feature be implemented?

As @xsjcTony said, prefetch is important because it start to load chunk files only after the page completely loads. It means that start to load the file on browser idle time and not in parallel with the main chunks.

enjoy-wind commented 1 year ago

@xsjcTony 🧚‍♀️,你是否可以在项目中基于requestIdleCallback和IntersectionObserver及动态import函数,封装prefetch函数,来完成对应需求

xsjcTony commented 1 year ago

@enjoy-wind Yes I can, and I've already achieved it. The problem is how can I get the hash map, like, I've no idea which js file to prefetch. Is there any way to get over it?

enjoy-wind commented 1 year ago

1.定义一个usePrefetch.js

export default (componentName) => {
    const element = useCurrentElement()
    const io = new IntersectionObserver(
        entries => {
            const {isVisible} = entries[0]
            let delay = 500;
            if (!isVisible) {
                delay = 2000
            }
            setTimeout(() => {
                window.requestIdleCallback(() => {
                    requestComponent(componentName)
                    io.disconnect()
                })
            }, delay)

        },
        {
            threshold: [1]
        }
    );
    watchOnce(element, (value) => {
        const target = value.nextElementSibling || value
        io.observe(target);
    })
}

2.定义一个useLoadAsyncComponents.js

//todo期待通过语法糖支持动态参数变量导入
const requestComponent = async (key) => {
  if (key === 'SFMonacoEditor') {
    return import('~/components/special/SMonacoEditor/form/SFMonacoEditor.js')
  }
  if (key === 'BFUpload') {
    return import('~/components/base/BUpload/form/BFUpload')
  }
}

3.使用 usePrefetch('SFMonacoEditor')

@xsjcTony

xsjcTony commented 1 year ago

@enjoy-wind Yeah it seems like a valid way, appreciate that.

But I may be misleading in my descriptions, what I actually want is to achieve the same thing VitePress did, like, globally fetch every link comes into the viewport.

So basically I still need the hashmap, since it doesn't make sense to use the custom hook everywhere in my code (like whenever there's a new link, I need to use it in the corresponding component)

enjoy-wind commented 1 year ago

@xsjcTony 是否可以加个微信,我们一起看看怎么解决

agualis commented 1 year ago

@xsjcTony @enjoy-wind Did you finally implement this solution?

anderskiaer commented 1 year ago

So basically I still need the hashmap

We use a hack which appears to work (it assumes some internal workings of vite I guess :see_no_evil: ). In order to get the hashed assets of e.g. a dynamic import () => import("./pages/some_page") we utilize that e.g.

(() => import("./pages/some_page")).toString()

gives us a string

()=>vt((()=>import("./index-fcec6b38.js")),["assets/index-fcec6b38.js","assets/index.esm-d8722078.js"])

when running the built application, which can be parsed to get the assets corresponding to the dynamic import (and then prefetch them when suitable).

However - we would very much like to remove this hack and get the hash-map through an official vite supported method.

enjoy-wind commented 1 year ago

@xsjcTony @enjoy-wind Did you finally implement this solution? @agualis

通过import.meta.glob获取类似组件的混淆后的hash,然后通过正则命中Key,完成动态导入.

const formComponent = import.meta.glob('~/components/*/*/form/*.(jsx|js|vue)')

const getReg = (key) =>
  new RegExp(`src/components/[^/]*/[^/]*/form/${key}.(jsx|js|vue)$`)

const requestComponent = async (key) => {
  const reg = getReg(key)
  const k = Object.keys(formComponent).find((k) => reg.test(k))
  return formComponent[k]()
}
DavidRNogueira commented 9 months ago

Any updates on this? I would love to convert from Webpack to Vite but without this, it will be tough.

vitorbertolucci commented 9 months ago

I found this blogpost that suggests a workaround: https://medium.com/@kiranv07/how-to-prefetch-javascript-bundle-in-a-vite-js-react-app-d38de7fe34fc

But for what I can tell it will just load the splitted bundles in parallel, so the benefits of prefetching are lost. I think this is a very important feature for vite to have, I'm looking forward to see if it will be implemented :).

xsjcTony commented 9 months ago

Yeah exactly as you said, it's just downloading packages in parallel, even if the link is not in user's viewport that user is not likely going to click it

niltonxp2 commented 9 months ago

I found this article with an approuche that worked for me!

Insted of this

const Home = React.lazy(() => import(/* webpackPrefetch: true */ "./Home"));

Do this

const homePromise = import("./Home");
const Home = React.lazy(() => homePromise);

Thx [Kiran V](Kiran V)

nicooprat commented 8 months ago

Here our approach for Vue: blog post & gist

We created a Vue plugin that monkey patch the router-link component:

Concerning the OP issue with hashmap, we bypass it by actually loading the page file (the component property of the route, which is just a promise we can call whenever we want); which is a bit too much, but good enough in our case.

It could certainly be improved though!

dreambo8563 commented 8 months ago

I create a package to append assets into index.html as prefetch Link. https://github.com/dreambo8563/vite-plugin-bundle-prefetch @bepan @xsjcTony @DavidRNogueira @agualis have a try with it , I think it can meet most of Scenario. I test it on vite@4xxx.

shinyruo-nmsl commented 2 weeks ago

if we can get the generated html file during build, then use the regex to match the link we need and add prefetch to this link, can it works ? i haven't tried, this is just a idea

daviareias commented 2 weeks ago

I found this article with an approuche that worked for me!

Insted of this

const Home = React.lazy(() => import(/* webpackPrefetch: true */ "./Home"));

Do this

const homePromise = import("./Home");
const Home = React.lazy(() => homePromise);

Thx [Kiran V](Kiran V)

It seems that this method will still block other components in the same file

The only way I found around this is using something like this to load the component only after the page is loaded, but im not sure if it's a good solution.

Also in this example I assume the component will be needed as soon as the page loads and everything else is loaded.

Example

const fetchComponentOnLoad = new Promise((resolve, _reject) => {
  window.addEventListener("load", () => resolve(import("./components/MyComponent")));
});
const MyComponent = lazy(() => fetchComponentOnLoad);
cszhjh commented 1 week ago

hi~我也需要这个功能, 为此我写了一个Vite Plugin, 它将会分析 import() 中的 /* vitePrefetch: true */, 并将其注入到 index.htmlhttps://github.com/cszhjh/vite-plugin-magic-preloader

gloompiq commented 1 week ago

Is there any development on this? We also need to be able to load packages in parallel and as soon as possible. /* vitePreload: true */ would be the best solution.

wille commented 1 week ago

I built vite-preload which will let you inject and Link headers dynamically based on which dynamic imports was rendered.

With this plugin, there is no need to manually do something, any dynamically imported react component that gets rendered will be preloaded.