vitejs / vite

Next generation frontend tooling. It's fast!
http://vite.dev
MIT License
68.4k stars 6.17k forks source link

Scripts set in manualChunks are loaded directly in front page, instead to be lazy loaded when needed #5189

Open a-tonchev opened 3 years ago

a-tonchev commented 3 years ago

Describe the bug

To manage well the bundle, I am setting the packages into manualChunks e.g.:

The package fabric is used only in the admin area of my app, that is why I don't need it to be loaded directly in the front page.

If I don't set it in manualChunks, it works good and it will not be loaded in the front page, but my chunks then are too large, because vite place it automatically together with others in a huge backend chunk. Then as soon I open the backend it lazy loads all the other chunks, including the one that contains fabric. So this is the expected behavior.

If I set it in manualChunks, e.g.:

    rollupOptions: {
      output: {
        manualChunks: {
          fabric: ['fabric'],
        }
     }
   }

the fabric chunk is loaded directly on the front page. Even if I am not admin.

You can see the differences when I include fabric or not:

Not included in manualChunks image

Included in manualChunks: image

Expected behavior is: fabric should only be loaded when really used, else it creates only network traffic and lowers the Lighthouse score because of unused code!

NOTE: I am using an example with fabric here, but in my project I have a bunch of other libraries that have the same issue with manualChunks.

Reproduction

I created a small reproduction in this repo:

https://github.com/a-tonchev/react-boilerplate/tree/vite_no_lazy_load

Steps to reproduce:

  1. Install deps (yarn)

Try bug:

  1. yarn build && yarn serve
  2. Open localhost:4000 and see in Network -> JS, the fabric script is loaded

To try without, open vite.config.js: https://github.com/a-tonchev/react-boilerplate/blob/vite_no_lazy_load/vite.config.js

and comment out line 40

  1. yarn build && yarn serve
  2. Open localhost:4000 and see in Network -> JS, the fabric script is no more loaded,

System Info

Windows 10 + WSL (Ubuntu)

Vite version "^2.3.8"

Used Package Manager

yarn

Logs

No response

Validations

0biWanKenobi commented 2 years ago

I can confirm that this issue occurs. Also, it seems that even dependencies I've never imported are passed to manualChunks, raising the chunks size.

a-tonchev commented 2 years ago

On the other hand, it is good to have the option to decide which chunks should be loaded immediately, because thus we will avoid the waterfall-effect on chunks that are always needed. The <link preload is not always working like expected and is more difficult to maintain, that is why this rule would be quite nice improvement of the loading times.

I suggest to have either two rules manualChunks + preloadChunks or just to put this option in the manualChunks e.g. { chunkUrl, preload: true }

TrickyPi commented 2 years ago

I have some confusion about this issue, i use build.rollupOptions.output.manualChunks and import() in other project. Finally, i didn't find this issue. And, the reproducible repo isn't simple.

tomascubeek commented 2 years ago

Is something happening with this issue? @TrickyPi if you are using it without any issue in other projects, can you show us your example? Are you sure those manual chunks are really lazy-loaded? ... because this really does not work as expected. Once you start using manualChunks, lazy-routes are not lazy-loaded at all, they load together with index (first page) - utilizing the last version of vite and vue-router.

nicooprat commented 2 years ago

I can confirm this too in our Vue app. We didn't have the issue before migrating from Webpack, using /* webpackChunkName: "foo" */ feature. We tried to mimic that, but for now it's counter productive. (we're using vite 2.8.6).

a-tonchev commented 2 years ago

Yes the issue is still there and since it is build issue, I can not just make a code-sandbox example.

The repository I mentioned in my first post shows clearly the problem.

TrickyPi commented 2 years ago

I created a minimal repo, and it works.

step

  1. git clone -b vite-issue-5189-temp https://github.com/TrickyPi/issue-repo
  2. see reproduce.md.
bluwy commented 2 years ago

I think this should be fixed now in Vite 2.9 (changelog). Would appreciate if y'all can verify this, and whether this can be closed.

TrickyPi commented 2 years ago

I think this "issue" is unrelated to the Vite's default chunking strategy.

nicooprat commented 2 years ago

No change with Vite 2.9.1 unfortunately, all chunks are still loaded via <link rel="modulepreload" href="xxx"> tags in <head>.

CaptainLiao commented 2 years ago

I use your reproduction and add the plugin vite-plugin-webpackchunkname, that resolve the issue.

your repo vite.config.js:

import {manualChunksPlugin} from 'vite-plugin-webpackchunkname'

export default defineConfig({
  plugins: [
    ....
    manualChunksPlugin(),
  ],
})
jesuismaxime commented 2 years ago

Any fix in view (from Vite - no extra packages solution please!)?

exodusanto commented 2 years ago

Same edge-case: I need to chuck opencv and lazy load only at some point, with this preload feature always enable an user 'll download 2.5MB gzip at app bootstrap, in background yes but it's 2.5MB

booellean commented 1 year ago

I've been banging my head against the wall with this error. It appears that returning void like the documentation shows is causing the error. When returning void, Vite will chunk synchronous modules with asynchronous modules, causing the chunk to be preloaded. If you return a default value to catch everything else, then Vite will not do this. Thankfully, that default return value will still pass over asynchronous components you define with defineAsyncComponent, but it will catch everything else, possibly causing large builds. There may be a work around, I'm still playing with a solution.

I also finally found a workaround that allows for custom chunk names similar to webpack's magic comments. I developed this because the plugin vite-plugin-webpackchunkname was throwing errors. Please note the last line and comment of the example

  manualChunks: (id) => {

      // This is how I do dynamic chunk naming.
      // Example uses:
      // import("some/module/index?chunkName=my-custom-chunk-name")
      // import MyComponent from "some/component.vue?chunkName=restricted_path/my-custom-chunk-name"
      if(id.includes("?chunkName=") || id.includes("&chunkName=")){
          let name = id.match(/(?<=chunkName=)(.*)(?=&)|(?<=chunkName=)(.*)/gm)
          if(name[0]) return name[0];
      }

      // Vendor Chunks
      if(id.includes('node_modules')) return 'vendor';

      // Here's the odd fix. You cannot return void, it must return a string
      return "index";
  },

I'm still cleaning this up and will post updates as it develops. Noteable issues:

  1. Typescript is throwing errors with the query in the url string. It's possible that Javascript will too.
  2. Chunking does not work if you are importing from a generic package in node_modules. You must import a file. Does not work: "node_module_package_name?chunkName=package" Does work: "node_module_package_name/index?chunkName=package"
pfdgithub commented 1 year ago

Ignore modulepreload <link rel="modulepreload" href="xxx">

https://github.com/vitejs/vite/blob/main/docs/config/build-options.md#buildmodulepreload

{
  build: {
    modulePreload: {
      resolveDependencies: (url, deps, context) => {
        return [];
      }
    }
  }
}

Ignore type error Cannot find module 'xxx' or its corresponding type declarations.ts(2307)

https://www.typescriptlang.org/docs/handbook/modules.html#wildcard-module-declarations https://github.com/microsoft/TypeScript/issues/38638

Do not use the & character, otherwise the development mode will throw error:
TypeError: Failed to fetch dynamically imported module: http://localhost:5173/src/module/index.tsx?chunkName=name&

// e.g. import("./module?chunkName=name#")
declare module "*#" {
  const value: any;
  export = value;
}

Grouping Components in the Same Chunk

https://github.com/vitejs/vite/commit/849e8456e1f7609040030c87cdc51e2fc76235b7 https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/splitVendorChunk.ts#L114

{
  build: {
    rollupOptions: {
      output: {
        sourcemapExcludeSources: true,
        manualChunks:(id) => {
          const url = new URL(id, import.meta.url);
          const chunkName = url.searchParams.get("chunkName");
          if (chunkName) {
            return chunkName;
          }
          // return void will invoke the built-in `viteManualChunks`
        }
      }
    }
  },
  plugins: [
    splitVendorChunkPlugin()
  ],
}
huseyindeniz commented 1 year ago

Hi everyone, I experienced the same problem and thanks to @pfdgithub , the following update solved the issue. Both build size and Lighthouse score ok now.

{
  build: {
    modulePreload: {
      resolveDependencies: (url, deps, context) => {
        return [];
      }
    },
    rollupOptions: {
      output: {
        sourcemap: false,
        manualChunks: {
          ethers: ['ethers'],
          router: ['react-router-dom'],
          rtk: ['@reduxjs/toolkit'],
          redux: ['react-redux'],
          chakra: ['@chakra-ui/react'],
        },
      },
    },    
  }
}
sabbirrahman commented 10 months ago

I created a minimal repo, and it works.

step

  1. git clone -b vite-issue-5189-temp https://github.com/TrickyPi/issue-repo
  2. see reproduce.md.

Hello @TrickyPi, I've cloned this repo and tried it. Yes, it works with Vite 2.9.1. But when I updated Vite to the latest 5.0.10, it was not working as expected anymore.

Edit: The issue exists from Vite 3.0.0, everything is good until 2.9.16, so something broke this while transitioning from Vite 2 to 3.

magicyoda commented 9 months ago

@huseyindeniz I tried following : (code formatting did not work?)

export default defineConfig({ optimizeDeps: { include: ["@vue/runtime-dom"], exclude: ["flowbite"], }, resolve: { alias: { vue: "@vue/runtime-dom", }, }, build: { modulePreload: { resolveDependencies: (url, deps, context) => { return []; }, }, rollupOptions: { output: { manualChunks(id) { if (id.includes("node_modules")) { return id .toString() .split("node_modules/")[1] .split("/")[0] .toString(); } }, }, }, },

I'm getting a chunk / module BUT, all chunks are loaded immediately on page load. They are not loaded when required, which results in a bad lighthouse score. What am I doing wrong? Thank you for your help.

huseyindeniz commented 9 months ago

hi @magicyoda did you add the following to your index.html file, if not, can you try

<script> window.global = window; </script>

daniilgri commented 9 months ago

Why is this issue still opened? I've just checked it in vite@5 and the issue is still there.

jessejamesrich commented 8 months ago

This appears to be unresolved in the latest version of Vite as far as I can tell, and none of the above solutions seem to address it. All manual chunks are eagerly loaded which has catastrophic effects on performance.

jbool24 commented 7 months ago

BUMP!!

Anything going on here?? Related issues breaks vue-router dynamic imports. Cannot find module with dynamic imports that are loaded on direct navigation to a URL.

alt1o commented 7 months ago

Still.

a-tonchev commented 7 months ago

Yes, that is the reason I don't use manualChunks since 2021, it kind a kill my website performance and it makes no sense to use them

jiikoosol commented 6 months ago

Any news on this one? It really should be fixed.

jiikoosol commented 5 months ago

Bump.

jesuismaxime commented 5 months ago

2 years later: bump!

Would be nice to have, at least, a feedback!

sirtimid commented 5 months ago

This is still happening. Any news to resolve it?

jiikoosol commented 5 months ago

We really need to get this fixed.

divmgl commented 5 months ago

Is it not possible to load bundles async in Vite? I'm a bit confused. I need lazy loading and I was under the assumption Vite handled this through manualChunks and import. Can someone on the Vite team advise if this is user error or an actual bug?

a-tonchev commented 5 months ago

Is it not possible to load bundles async in Vite? I'm a bit confused. I need lazy loading and I was under the assumption Vite handled this through manualChunks and import. Can someone on the Vite team advise if this is user error or an actual bug?

It is an actual bug, else manualChunks would be total useless concept since all the chunks are loaded at the beginning.

divmgl commented 5 months ago

@a-tonchev I was able to figure it out and I actually don't agree. You provide manual chunks with an undefined return and it will do its own internal processing.

The async loading using import definitely works but you have to make sure the module is not loaded synchronously.

a-tonchev commented 5 months ago

@a-tonchev I was able to figure it out and I actually don't agree. You provide manual chunks with an undefined return and it will do its own internal processing.

The async loading using import definitely works but you have to make sure the module is not loaded synchronously.

Okay then what would be your suggestion to solve the problem, can you give an example?

jiikoosol commented 5 months ago

@a-tonchev I was able to figure it out and I actually don't agree. You provide manual chunks with an undefined return and it will do its own internal processing.

The async loading using import definitely works but you have to make sure the module is not loaded synchronously.

async loading with an import works yes, but the question is that if you want to group multiple async components together to a certain chunks via manualChunks (just like you can do in Webpack with a magic comment "webpackChunkName"), it doesn't work as then the defined chunk is loaded always regardless of is the component used or not.

a-tonchev commented 5 months ago

@a-tonchev I was able to figure it out and I actually don't agree. You provide manual chunks with an undefined return and it will do its own internal processing. The async loading using import definitely works but you have to make sure the module is not loaded synchronously.

async loading with an import works yes, but the question is that if you want to group multiple async components together to a certain chunks via manualChunks (just like you can do in Webpack with a magic comment "webpackChunkName"), it doesn't work as then the defined chunk is loaded always regardless of is the component used or not.

Yes and that is the problem. Creating chunks makes only sense to load different parts of the JS on demand. If you load all the chunks at the beginning - code splitting makes no sense at all, you can just load the whole JS. That is why this bug makes the manualChunks useless

Tschuck commented 5 months ago

Tested around and had the same issue. I wanted to split out the jsoneditor library from my vendor file, but my vendor.js was always importing the jsoneditor file as well as the index.html. When analyzing the bundles not minified, I've recognized that it was just the commonjsHelpers.js that was included in my jsoneditor file, what was then required by the vendor file and the index. So my final solution was the following:

import { defineConfig, splitVendorChunkPlugin } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths(), splitVendorChunkPlugin()],
  server: {
    port: 3000,
  },
  build: {
    rollupOptions: {
      output: {
        sourcemapExcludeSources: true,
        manualChunks(id: string) {
          if (id.includes("jsoneditor")) {
            return "jsoneditor";
          }
          if (id.includes("node_modules") || id.includes("commonjsHelper")) {
            return "vendor";
          }
        },
      },
    },
  },
});

Example code of my jsoneditor usage:

    async function setupJsonEditor() {
      const JsonEditorLib = (await import("jsoneditor")).default;
      await import("jsoneditor/dist/jsoneditor.min.css");

      const editor = new JsonEditorLib(
        ...,

Initial page load:

image

I hope it helps someone! :)

pfdgithub commented 5 months ago

@Tschuck The known common chunk:

    "vite/preload-helper",
    "vite/modulepreload-polyfill",
    "vite/dynamic-import-helper",
    "commonjsHelpers",
    "commonjs-dynamic-modules",
    "__vite-browser-external",

For manual chunk, be sure to split them carefully to avoid circular dependencies.

plessing-te commented 4 months ago

Thanks to @pfdgithub, I have made this work for our project.

It appears that the issue was that while I was configuring a set of files for a specific manualChunk, rollup was spreading dependencies around all the chunks. So it seems that defining a manualChunk for some dependencies but not all means that all the dependencies you specify will be in the chunks you specify, but the ones that are not defined may still end up in those chunks as well - and if those dependencies are common, they will be eagerly loaded.

As an example, I was trying to split a number of mock/test-related files out to a manual chunk which should not be loaded outside of the test environments. But rollup was adding sentry into the same chunk, so as soon as the application loaded it would load my test code file.

The solution was to specify for all 3rd-party chunks where they should go, especially the ones mentioned by @pfdgithub:

{
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          // List of modules that rollup sometimes bundles with manual chunks, causing those chunks to be eager-loaded
          const ROLLUP_COMMON_MODULES = [
            'vite/preload-helper',
            'vite/modulepreload-polyfill',
            'vite/dynamic-import-helper',
            'commonjsHelpers',
            'commonjs-dynamic-modules',
            '__vite-browser-external'
          ];

          // Split out mock code into its own chunk
          // (THIS IS AN EXAMPLE, REPLACE WITH WHATEVER YOU NEED)
          if (id.includes('/node_modules/@mswjs/')) {
            return 'mock';
          }

          // Bundle all other 3rd-party modules into a single vendor.js module
          if (
            id.includes('node_modules') ||
            ROLLUP_COMMON_MODULES.some((commonModule) => id.includes(commonModule))
          ) {
            return 'vendor';
          }

          // All other files are project source files and are allowed to be split whichever way rollup wants
        }
      }
    }
  }
}

I hope this helps someone else.

is-jonreeves commented 4 months ago

I was recently reading about a new vite-compatible bundler called Farm. It has an interesting set of options in what it calls Partial Bundling. Might be worth a look for those who are flexible with their build tools.

I've not personally tried it myself, but hope to soon, as hand-tuning manualChunks is a real pain

victortrusov commented 2 months ago

I found a way to drastically improve FP/FCP using manualChunks.

The idea is to remove script lines with rel="modulepreload" from appearing in HTML for generated JS manual chunks. Because the main chunk that loads on WS first always reference these manual chunks it's easy to do and it doesn't break anything.

I have these manual chunks:

manualChunks: (id) => {
  if (id.includes('/src/pages/index')) return '01_main';
  if (id.includes('/src/pages/r/') || id.includes('/src/pages/recommendations/[recommendationId]') || id.includes('/src/layout')) return '02_recommendation';
},
  1. I use 01, 02 prefixes because it appears the order matters to balance the size of manual chunks
  2. It's a Nuxt 3 app so I'm creating chunks for pages and layouts, that's the only way I found to not break anything by creating manual chunks + it does make sense if you want to optimize the page that is most commonly used
  3. I don't create the separate vendor chunk because I would rather split node_modules by usage between pages

Now the waterfall looks like this (main chunk + manual chunks loads at the same time and blocks FP/FCP):

image

But if I add a module that sets preload=false for these chunks in .output/server/chunks/build/client.manifest.mjs:

  modules: [
   ...
    async (_, nuxt) => {
      nuxt.hooks.hook('build:manifest', (manifest) => {
        for (const item of Object.values(manifest)) {
          if (item.name?.includes('01_main') || item.name?.includes('02_recommendation')) {
            item.preload = false;
          }
        }
      });
    },
  ],

As you can see the main JS chunk loads first and then it loads all manual chunks. Unfortunately works only with JS:

image

The good thing is that FP/FCP happens after all sources from HEAD are being loaded, so it's much faster now:

image image

(thanks to https://github.com/johannschopplich/nuxt-vitalizer for the example of build:manifest hook usage)

tcper commented 2 weeks ago

Same problem, use onnxruntime-web, the library loaded at the first always.

A quick look into load-script, vite mistakenly resolve the library dependency, onnxruntime-web is imported at the first time, so load it.

I think it is not a simple problem.