vuejs / vue-cli

🛠️ webpack-based tooling for Vue.js Development
https://cli.vuejs.org/
MIT License
29.76k stars 6.33k forks source link

Common chunks not separated per page, production is different from development #2381

Open doits opened 6 years ago

doits commented 6 years ago

Version

3.0.1

Reproduction link

https://github.com/doits/vue-chunk-example

Node and OS info

Node 10.9.0 / yarn 1.9.4

Steps to reproduce

I have a app with two pages in vue.config.js:

module.exports = {
  pages: {
    public: {
      entry: "src/main.js",
      template: "public/index.html",
      filename: "index.html",
      title: "Index Page"
    },
    swagger: {
      entry: "src/swagger.js",
      template: "public/swagger.html",
      filename: "swagger.html",
      title: "Swagger"
    }
  }
}

public is the main page and swagger is just an empty page. What now happens in production ist:

I'd think that there would be two "common chunks", one for each page. So those pages are really completely separated. Eg. chunk-vendors-[pagename]-[hash].{js,css}, which is only used on that page. (removed this, since this does not make sense)

What makes this worse: You only see this in production. In development, common chunks are not applied, so everything works as expected, but when compiling it in production, the common chunks get applied and the result is different from development.

What is expected?

Make multiple common chunks, one per page.

What is actually happening?

Common chunks are global, and each page imports them, even when not needed.

yyx990803 commented 6 years ago

It would no longer be a "common" chunk if it's one per page... I'm not sure what you are really asking for.

doits commented 6 years ago

Maybe I don't understand what pages is about. I though it is about having two separate pages, where separate means they are really separate.

Now I have two pages, where the second one loads all common chunks from the first one, even it will never use it and never imported them (this is one reason why this is separate page: it does not need everything). With the result, that some css styles get applied to the second page, even though they should only be applied to the first page. They are only imported in the first page, never in the second page.

Maybe I have a different understanding of the pages option? What should or shouldn't it do?

Edit: So from my understanding, a module is only common if I really import it in both pages. Otherwise I don't need it as a common chunk everywhere. I think this confuses me.

Edit 2: I striked through the part about chunk-vendors-[pagename]-[hash].{js,css} in the OP, because I think I got the common chunks idea wrong while writing this. The rest still applies though.

doits commented 6 years ago

To give a condensed example with the common axios module:

module.exports = {
  pages: {
    public: {
      entry: "src/main.js",
      template: "public/index.html",
      filename: "index.html",
      title: "Index Page"
    },
    swagger: {
      entry: "src/swagger.js",
      template: "public/swagger.html",
      filename: "swagger.html",
      title: "Swagger"
    }
  }
}
...
import "axios"
...

yarn build builds a dist/js/chunk-vendors.[hash].js with axios in it, and this chunk is loaded in both public and swagger pages, even though swagger page does not use it at all (it is never imported there, it is not required). It bloats the page. (same with vue in this case)

In development, axios is not loaded at swagger page (like expected), only in production (due to the vendor chunk).

IMO this is not how it should be – or should it?

coun7zero commented 6 years ago

I agree with @doits. I think we both think about something like that: https://github.com/webpack/webpack/tree/master/examples/common-chunk-and-vendor-chunk

Btw chunks: ['chunk-vendors', 'chunk-common', 'index'] doesn't work correctly. What if I would like to exclude some chunks?

harryhorton commented 6 years ago

This would also be valuable to me in my project. I'm working on a full SPA. Along with this SPA is a secondary "page" that I'm ultimately treating as a separate app for a single critical use case. It would be nice to be able to keep the separate app as a separate "page" and somehow specify that some of the large modules used in the primary app's vendor chunk aren't necessary.

However, the "pages" does prevent all of the unnecessary page(vue-router) chunks from being loaded.

ALTERNATIVE(and probably correct) SOLUTION

As a solution for now, if the two "pages" you're working on shouldn't share a common vendor chunk, then just create a separate vue-cli app. They're probably best handled as separate apps then anyway.

For instance, I have src/client and src/server(ts backend). While it would be nice to only have src/client, it's not the end of the world to add src/client-[alternate app identifier]. It will require some extra package.json scripts to make it easy to work with at the root, but if you're not sharing those chunks, then you've got a separate app. At least, that's what I'm telling myself.

doits commented 6 years ago

As a solution for now, if the two "pages" you're working on shouldn't share a common vendor chunk, then just create a separate vue-cli app. They're probably best handled as separate apps then anyway.

Only makes sense, well, if they are really two apps. Because it has the drawback:

In another case of mine, I have a "frontoffice" and "backoffice" section: Frontoffice just for "regular users" and backoffice for "admins". They connect to the same api (so use @/api/*), both use some of the @/components and they even use the same @/App.vue, but after that the backoffice has much more logic, different routes, and requires more vendor gems than the frontoffice. I don't think making them two separate apps is sensible in this case.

Akryum commented 6 years ago

Here is an example configuration to have common vendors and per-page vendors chunks:

https://gist.github.com/Akryum/ece2ca512a1f40d70a1d467566783219

(Didn't try with HtmlPlugin which I don't use on this project, so if some can try to configure it and share!)

vlahanas commented 6 years ago

ALTERNATIVE(and probably correct) SOLUTION

As a solution for now, if the two "pages" you're working on shouldn't share a common vendor chunk, then just create a separate vue-cli app. They're probably best handled as separate apps then anyway.

This way, you either have to maintain two codebases with the same code files, or end-up using "ugly" imports that point outside of your working directory. Indeed, it seems to be the only solution, but it is also contradictory to be using such a mixed setup with a tool (Vue-cli) that is there to make your life easier.

I am no expert in webpack, but my understanding is that there must be a way to tell webpack which module should go where. And I believe this is the minChunks option.

Edit: I missed @Akryum response. Indeed, I 've tested the solution and it seems to work fine!

Aymkdn commented 6 years ago

Using @Akryum solution doesn't work well for me. For example, if I import another module (e.g. import Excel from 'exceljs/dist/es5/exceljs.browser.js') it just silently fails, meaning it compiles but nothing shows up (the page is blank) and no error is returned. I have no idea why...

If I remove the @Akryum code, then everything works correctly, expect that I get some code in my page that I never asked for (but that is used by my second page).

I have difficulties to understand the different options used.... I tried different combinaisons, and finally the one that worked is :

common: {
            name: 'chunk-common',
            priority: -20,
            chunks: 'initial',
            minChunks: 2,
            reuseExistingChunk: false, // false instead of true
            enforce: false, // false instead of true
}
harryhorton commented 6 years ago

In order to get this to work, I had to remove the vendor cache group from @Akryum example. After that, I had to explicitly define the chunks to include in each "page"

const options = module.exports
        const pages = options.pages
        const pageKeys = Object.keys(pages)

        const IS_VENDOR = /[\\/]node_modules[\\/]/
        config.optimization.splitChunks({
            cacheGroups: {
                ...pageKeys.map((key) => ({
                    name: `chunk-${key}-vendors`,
                    priority: -11,
                    chunks: (chunk) => chunk.name === key,
                    test: IS_VENDOR,
                    enforce: true
                })),
                common: {
                    name: 'chunk-common',
                    priority: -20,
                    chunks: 'initial',
                    minChunks: 2,
                    reuseExistingChunk: true,
                    enforce: true
                }
            }
        })
pages: {
        app: {
            entry: 'src/main.ts',
            template: 'public/entry.html',
            filename: `../public/entry.html`,
            chunks: [ 'chunk-common', 'chunk-app-vendors', 'app']
        },
        'help-app': {
            entry: 'src-caller/main.ts',
            template: 'public/help/index.html',
            filename: `../public/help/index.html`,
            chunks: [ 'chunk-common', 'chunk-help-app-vendors', 'help-app']
        }
    }

Without explicitly including chunk-[pagename]-vendors it would fail silently for me

vlahanas commented 6 years ago

@Aymkdn the reason it fails silently, is that by adding the extra dependency, an extra chunk (probably chunk-{page}-vendors) is created. Please see below:

@Johnhhorton in order to make @Akryum solution work you need, as you said, to explicitly add all the necessary chunks in each of your "pages" configurations. But the code provided by @Akryum goes one step ahead, creating a vendors file that is common, and a vendors file that is specific to a page. It is a better (in my opinion) approach. To apply is to your configuration, you just need to also add the 'chunk-vendor' in your chunks array:

chunks: ['chunk-vendor', 'chunk-common', 'chunk-{pagename}-vendors', '{pagename}'] 
chadsaun commented 6 years ago

FYI - In my chunks array I had to use chunk-vendors instead of the singular form chunk-vendor

chunks: ['chunk-vendors', 'chunk-common', 'chunk-{pagename}-vendors', '{pagename}'] 
icewind7030 commented 5 years ago

Just try the config talked above, as we still set common.minChunks config as 2, and still include chunk-common in every page's entry, is it still has the chance to include node_modules which doesn't need for the page?

duziten commented 5 years ago

inside vuecli-3, splitChunksPlugin build 3 default chunk:chunk-vendors, chunk-common, {pagename}, add pass all of them to htmlWebpackPlugin,if you override the config of splitChunksPlugin use above methods ,you should set chunks array manually. you can find it here https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/config/app.js

appsparkler commented 5 years ago

This is what worked for me:

In the vue.config.js:

// vue.config.js
module.exports = {
  // ...
  configureWebpack: {
    optimization: {
      splitChunks: {
        minSize: 1
      }
    }
  }
  // ...
}

And then ['chunk-vendors', 'chunk-common', '{pagename}'] should work out-of-the-box.

References :

Good Luck...

benjaminprojas commented 5 years ago

I am having the same issue as the OP ( @doits ). One page imports vuetify, the other does not. However, the build for the second page always includes vuetify, which messes with the styling of that page.

I've tried numerous combinations to modify the cacheGroups as previous users have posted, but most just freeze the build process without any errors.

Has anyone else gotten this to work? I'd really like to eliminate the extra bloat of vuetify (and other unnecessary dependencies) loading on the second page.

benjaminprojas commented 5 years ago

ok, take that back. If I use @appsparkler solution, it does seem to work for me. I can see that it no longer generates a chunk-vendors.js file, but there are a lot of other vendor specific files that are created. Is there better documentation on why this would work?

Aymkdn commented 5 years ago

If it can help someone, here is a simplified version of my vue.config.js, using:

const webpack = require('webpack');
const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin');

module.exports = {
  runtimeCompiler:true,
  transpileDependencies: ["vuetify"],
  configureWebpack: config => {
    if (!config.plugins) config.plugins=[];

    // for Vuetify
    config.plugins.push(new VuetifyLoaderPlugin());
  },
  chainWebpack: config => {
    // https://github.com/vuejs/vue-cli/issues/2381#issuecomment-425038367
    const IS_VENDOR = /[\\/]node_modules[\\/]/
    config.optimization.splitChunks({
      cacheGroups: {
        index: {
          name: `chunk-index-vendors`,
          priority: -11,
          chunks: chunk => chunk.name === 'index',
          test: IS_VENDOR,
          enforce: true,
        },
        form: {
          name: `chunk-form-vendors`,
          priority: -11,
          chunks: chunk => chunk.name === 'form',
          test: IS_VENDOR,
          enforce: true,
        },
        common: {
          name: 'chunk-common',
          priority: -20,
          chunks: 'initial',
          minChunks: 2,
          reuseExistingChunk: true,
          enforce: true,
        }
      }
    })
  },
  'pages': { // for multi-page see https://cli.vuejs.org/config/#pages
    'form': {
      entry: 'src/form.js',
      filename: 'form.html',
      template: 'template/form.html',
      chunks: ['chunk-common', 'chunk-form-vendors', 'form']
    },
    'index': {
      entry: 'src/index.js',
      filename: 'index.html',
      template: 'template/index.html',
      chunks: ['chunk-common', 'chunk-index-vendors', 'index']
    }
  }
}

In index I don't want/need to use Vuetify:

// source: src/index.js
import Vue from 'vue'
// load main App
import App from './app/Index.vue'

new Vue({
  render: h => h(App)
}).$mount('#app')

In form I do need Vuetify:

// source src/form.js
import Vue from 'vue'
// for Vuetify
import Vuetify from 'vuetify/lib'
import { VBtn } from 'vuetify/lib' // because we use it often in Dialogs, so to avoid loading issues
import 'vuetify/src/stylus/app.styl'
Vue.use(Vuetify, {
  components:{
    VBtn
  }
});

// load main App
import App from './app/Form.vue'

// dynamic components
//const MaskedInput = (resolve) => {
// import(/* webpackChunkName: "maskedinput" */'vue-masked-input')
// .then(AsyncComponent => {
//   resolve(AsyncComponent.default);
// });
//}
//Vue.component('masked-input', MaskedInput)

new Vue({
  el: '#app',
  render: h => h(App)
})

And the HTML templates (index.html and form.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
    <title>Form</title>
    <link href='https://fonts.googleapis.com/css?family=Material+Icons' rel="stylesheet">
  </head>
  <body>
    <noscript>
      <strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
benjaminprojas commented 5 years ago

@Aymkdn Thanks for posting that! I tried copying your config file verbatim, and it still did not work for me.

Turns out I had another configuration error in that I never had a chunk-common.js generated before, so I was not including it (I'm using a manifest.json and generating the output a little differently than the default build). Because I didn't have chunk-common.js included, the page wouldn't load.

Your code set me in the right direction, so thank you!

icewind7030 commented 5 years ago

@benjaminprojas Here is part of my config in vue.config.js inside chainWebpack function. The minChunks key is the point, which means modules used more than certain time would be split into common chunk and would be loaded by every page. For me, I set the minChunks value equals to my count of pages pages.length.

config.optimization
        .splitChunks({
          cacheGroups: {
            common: {
              name: 'chunk-common',
              priority: -20,
              chunks: 'initial',
              minChunks: pages.length, //count of multi pages
              reuseExistingChunk: true,
              enforce: true
            }
          },
        })

Wish this would help.

benjaminprojas commented 5 years ago

@icewind7030 thanks! I may give that a shot. That is very helpful and definitely helps me understand how this works a little better. I appreciate your input!

libin1991 commented 5 years ago

@benjaminprojas Here is part of my config in vue.config.js inside chainWebpack function. The minChunks key is the point, which means modules used more than certain time would be split into common chunk and would be loaded by every page. For me, I set the minChunks value equals to my count of pages pages.length.

config.optimization
        .splitChunks({
          cacheGroups: {
            common: {
              name: 'chunk-common',
              priority: -20,
              chunks: 'initial',
              minChunks: pages.length, //count of multi pages
              reuseExistingChunk: true,
              enforce: true
            }
          },
        })

Wish this would help.

try

configureWebpack: config => {

    config.optimization.splitChunks.cacheGroups.common={ 
            name: 'chunk-common',
              priority: -20,
              chunks: 'initial',
              minChunks: 200, //   Pages. length still generates chunk-common files. Use a big number, or it 
                                          //     generates chunk-common files.
              reuseExistingChunk: true,
              enforce: true
        }
       ...

or

Webpack will default to chunk-vendors for commonChunk. So you need to delete the webpack configuration.

chainWebpack: config => {
  config.optimization.delete('splitChunks')
}
qqw78901 commented 5 years ago

@Aymkdn Thanks for posting that! I used the same problem. Now the problem was solved with your solution.

ybroch commented 4 years ago

In order to get this to work, I had to remove the vendor cache group from @Akryum example. After that, I had to explicitly define the chunks to include in each "page"

const options = module.exports
        const pages = options.pages
        const pageKeys = Object.keys(pages)

        const IS_VENDOR = /[\\/]node_modules[\\/]/
        config.optimization.splitChunks({
            cacheGroups: {
                ...pageKeys.map((key) => ({
                    name: `chunk-${key}-vendors`,
                    priority: -11,
                    chunks: (chunk) => chunk.name === key,
                    test: IS_VENDOR,
                    enforce: true
                })),
                common: {
                    name: 'chunk-common',
                    priority: -20,
                    chunks: 'initial',
                    minChunks: 2,
                    reuseExistingChunk: true,
                    enforce: true
                }
            }
        })
pages: {
        app: {
            entry: 'src/main.ts',
            template: 'public/entry.html',
            filename: `../public/entry.html`,
            chunks: [ 'chunk-common', 'chunk-app-vendors', 'app']
        },
        'help-app': {
            entry: 'src-caller/main.ts',
            template: 'public/help/index.html',
            filename: `../public/help/index.html`,
            chunks: [ 'chunk-common', 'chunk-help-app-vendors', 'help-app']
        }
    }

Without explicitly including chunk-[pagename]-vendors it would fail silently for me

and then, how can i split each vendors into pieces such as 'chunk-help-app-vendors' be splited into 'vue' , 'vuetify' ...

gcacars commented 3 years ago

For me nothing works. But after hours and with some fixes I could make this work. Based on the @Akryum answer, the key is the priority and reuseExistingChunk:

reuseExistingChunk: false,
priority: -1, // The priority of per page vendor should be greater then the all vendors (I think...)

So, the optimation config should looks like:

config.optimization
      .splitChunks({
        cacheGroups: {
          vendors: {
            name: 'chunk-vendors',
            priority: -10,
            chunks: 'initial',
            minChunks: 1,
            test: IS_VENDOR,
            reuseExistingChunk: false, //        <<< THIS
            enforce: true,
          },
          ...pageKeys.map((key) => ({
            name: `chunk-${key}-vendors`,
            priority: -1, //                     <<< THIS
            chunks: (chunk) => chunk.name === key,
            minChunks: 1,
            test: IS_VENDOR,
            reuseExistingChunk: false, //        <<< THIS
            enforce: true,
          })),
          common: {
            name: 'chunk-common',
            priority: -20,
            chunks: 'initial',
            minChunks: 2,
            reuseExistingChunk: true,
            enforce: true,
          },
        },
      });
llyp618 commented 3 years ago

hey guys, i think the config "pages" is not good enough to handle multiple pages,it cannot intelligently identify the dependencies of each entry and inject chunks (In fact, I haven’t studied the internal principles). But with webpack, i just add multi entrys and html-webpack-plugin, it works. so, i directly configure it with webpackChain:

//overwrite the default optimization config
config.optimization = {
    runtimeChunk: true,
    splitChunks: {
      chunks: "all",
      name: false
    }
  };
//...
//two entry  "pc" and "h5"
config.entryPoints
      .delete("app");
config
      .entry("pc")
      .add("./src/main.ts");
config
      .entry("h5")
      .add("./src_h5/main.ts");
//...
// html-webpack-plugin
config.plugins.delete("html");
config
      .plugin("html-pc")
      .before("copy")
      .use(HtmlWebpackPlugin, [{
        template: "public/pc.html",
        filename: "pc.html",
        title: "pc",
        chunks: ["pc"]
      }]);
config
      .plugin("html-h5")
      .before("copy")
      .use(HtmlWebpackPlugin, [{
        template: "public/h5.html",
        filename: "h5.html",
        title: "h5",
        chunks: ["h5"]
      }]);
// after configure, it may miss "BASE_URL" variable, u can pass throuth with html-webpack-plugin

This works for me, hope it works for you too

ux-engineer commented 3 years ago

Having problem optimizing chunks as well. I'm having a page for auth flow which should have minimal bundle size but currently vendors chunk is added there with all the scripts even though I'd need load only one dep at that point...