Vue-cli: Common chunks not separated per page, production is different from development

Created on 28 Aug 2018  Â·  25Comments  Â·  Source: vuejs/vue-cli

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:

  • public has a lot of imports and therefore some common chunks are exported to chunk-vendors-[hash].{js,css}, which is nice
  • but those common chunks are included in both pages, public and swagger, even though swagger does not use any of the common chunks at all (see swagger.html - it also imports the common chunks with bootstrap styles).

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.

Most helpful comment

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"
...
// empty file, no axios import

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?

All 25 comments

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

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.

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"
...
// empty file, no axios import

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?

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?

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.

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:

  • It does not share any common chunk (even those which should be shared because they are used in both)
  • You cannot easily use same sources in both app (for example if you have api functions in @/api/ and both apps should connect to the same api)

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.

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!)

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!

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
}

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

@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}'] 

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}'] 

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?

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

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...

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.

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?

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

  • vue-cli
  • vuetify à la carte
  • vuetify-loader
  • 2 pages (index.html and form.html)
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>

@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!

@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.

@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!

@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')
}

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

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' ...

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,
          },
        },
      });
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jgribonvald picture jgribonvald  Â·  3Comments

chasegiunta picture chasegiunta  Â·  3Comments

brandon93s picture brandon93s  Â·  3Comments

CodeApePro picture CodeApePro  Â·  3Comments

Gonzalo2683 picture Gonzalo2683  Â·  3Comments