Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: avoid splitting vendor chunk by default #6534

Merged
merged 10 commits into from Mar 4, 2022
14 changes: 14 additions & 0 deletions docs/guide/build.md
Expand Up @@ -43,6 +43,20 @@ module.exports = defineConfig({

For example, you can specify multiple Rollup outputs with plugins that are only applied during build.

## Chunking Strategy

You can configure how chunks are split using `build.rollupOptions.manualChunks` (see [Rollup docs](https://rollupjs.org/guide/en/#outputmanualchunks)). Until Vite 2.7, the default chunking strategy divided the chunks into `index` and `vendor`. It is a good strategy for some SPAs, but it is hard to provide a general solution for every Vite target use case. From Vite 2.8, `manualChunks` is no longer modified by default. You can continue to use the Split Vendor Chunk strategy by adding the `splitVendorChunkPlugin` in your config file:

```js
// vite.config.js
import { splitVendorChunkPlugin } from 'vite'
module.exports = defineConfig({
plugins: [splitVendorChunkPlugin()]
})
```

This strategy is also provided as a `splitVendorChunk({ cache: SplitVendorChunkCache )` factory, in case composition with custom logic is needed. `cache.reset()` needs to be called at `buildStart` for build watch mode to work correctly in this case.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk what the use case for custom reset is, but we could add an onReset callback instead of using a custom class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache used by splitVendorChunk needs to be cleaned up on every buildStart. I don't understand the onReset callback, how would that work?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the plugin would call cache.clear on the Map object and then call onReset callback after that in case there's any custom logic needed. But as I said, I have zero idea why anyone would need custom reset logic here. Perhaps you have an idea? 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh here's a use case. A plugin that wraps the splitVendorChunk plugin may need to be reset its own cache.

function extraChunkPlugin() {
  const anotherCache = new Map()
  return splitVendorChunkPlugin({
    manualChunks() {
      // Insert custom chunking logic.
    },
    onReset() {
      anotherCache.clear()
    }
  })
}

This example needs manualChunks and onReset options to be added to splitVendorChunkPlugin though. Not sure if this is what you had in mind for the reset method though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user is already doing a new plugin, I think it is better that they use the function splitVendorChunk and they can reset their caches at the same time as resetting the SplitVendorChunkCache (on buildStart). I don't think we need to have an API for the plugin. The plugin is there so users can add in a single line the old strategy, and we are exposing the low level function because it was requested several times before to compose it with other logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay that's fair, but I'm still confused why the SplitVendorChunkCache class is necessary if you can just call cache.clear on the Map object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, you said:

I don't know if we will need to expand the cache later. I wanted to expose the cache for the method, not a particular Map that is an implementation detail and may change

That doesn't really make sense to me. If we decide not to use a Map in the future (not sure why we would tbh), then we can handle that then.

But if you really believe the cache class is the right move, I'll defer to your judgment, since this isn't a huge deal or anything. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just feels to me that we are leaking an implementation detail if we share the Map. In this case it isn't even something useful like a map of modules, it is an internal cache used by the algorithm. I also don't like having to create a class, but it feels safer here to avoid the need for a breaking change.

patak-dev marked this conversation as resolved.
Show resolved Hide resolved

## Rebuild on files changes

You can enable rollup watcher with `vite build --watch`. Or, you can directly adjust the underlying [`WatcherOptions`](https://rollupjs.org/guide/en/#watch-options) via `build.watch`:
Expand Down
3 changes: 2 additions & 1 deletion packages/playground/vue/vite.config.ts
@@ -1,4 +1,4 @@
import { defineConfig } from 'vite'
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import { vueI18nPlugin } from './CustomBlockPlugin'

Expand All @@ -12,6 +12,7 @@ export default defineConfig({
vuePlugin({
reactivityTransform: true
}),
splitVendorChunkPlugin(),
vueI18nPlugin
],
build: {
Expand Down
59 changes: 0 additions & 59 deletions packages/vite/src/node/build.ts
Expand Up @@ -12,8 +12,6 @@ import type {
OutputOptions,
RollupOutput,
ExternalOption,
GetManualChunk,
GetModuleInfo,
WatcherOptions,
RollupWatcher,
RollupError,
Expand All @@ -37,7 +35,6 @@ import { dataURIPlugin } from './plugins/dataUri'
import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild'
import { resolveSSRExternal, shouldExternalizeForSSR } from './ssr/ssrExternal'
import { ssrManifestPlugin } from './ssr/ssrManifestPlugin'
import { isCSSRequest } from './plugins/css'
import type { DepOptimizationMetadata } from './optimizer'
import { scanImports } from './optimizer/scan'
import { assetImportMetaUrlPlugin } from './plugins/assetImportMetaUrl'
Expand Down Expand Up @@ -509,13 +506,6 @@ async function doBuild(
// #1048 add `Symbol.toStringTag` for module default export
namespaceToStringTag: true,
inlineDynamicImports: ssr && typeof input === 'string',
manualChunks:
!ssr &&
!libOptions &&
output?.format !== 'umd' &&
output?.format !== 'iife'
? createMoveToVendorChunkFn(config)
: undefined,
...output
}
}
Expand Down Expand Up @@ -642,55 +632,6 @@ function getPkgName(root: string) {
return name?.startsWith('@') ? name.split('/')[1] : name
}

function createMoveToVendorChunkFn(config: ResolvedConfig): GetManualChunk {
const cache = new Map<string, boolean>()
return (id, { getModuleInfo }) => {
if (
id.includes('node_modules') &&
!isCSSRequest(id) &&
staticImportedByEntry(id, getModuleInfo, cache)
) {
return 'vendor'
}
}
}

function staticImportedByEntry(
id: string,
getModuleInfo: GetModuleInfo,
cache: Map<string, boolean>,
importStack: string[] = []
): boolean {
if (cache.has(id)) {
return cache.get(id) as boolean
}
if (importStack.includes(id)) {
// circular deps!
cache.set(id, false)
return false
}
const mod = getModuleInfo(id)
if (!mod) {
cache.set(id, false)
return false
}

if (mod.isEntry) {
cache.set(id, true)
return true
}
const someImporterIs = mod.importers.some((importer) =>
staticImportedByEntry(
importer,
getModuleInfo,
cache,
importStack.concat(id)
)
)
cache.set(id, someImporterIs)
return someImporterIs
}

export function resolveLibFilename(
libOptions: LibraryOptions,
format: ModuleFormat,
Expand Down
5 changes: 5 additions & 0 deletions packages/vite/src/node/index.ts
Expand Up @@ -7,6 +7,10 @@ export { send } from './server/send'
export { createLogger, printHttpServerUrls } from './logger'
export { transformWithEsbuild } from './plugins/esbuild'
export { resolvePackageEntry } from './plugins/resolve'
export {
splitVendorChunkPlugin,
splitVendorChunk
} from './plugins/splitVendorChunk'
export { resolvePackageData } from './packages'
export { normalizePath } from './utils'

Expand Down Expand Up @@ -91,3 +95,4 @@ export type { Terser } from 'types/terser'
export type { RollupCommonJSOptions } from 'types/commonjs'
export type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars'
export type { Matcher, AnymatchPattern, AnymatchFn } from 'types/anymatch'
export type { SplitVendorChunkCache } from './plugins/splitVendorChunk'
109 changes: 109 additions & 0 deletions packages/vite/src/node/plugins/splitVendorChunk.ts
@@ -0,0 +1,109 @@
import type { UserConfig } from '../../node'
import type { Plugin } from '../plugin'
import type { OutputOptions, GetManualChunk, GetModuleInfo } from 'rollup'
import { isCSSRequest } from './css'

// Use splitVendorChunkPlugin() to get the same manualChunks strategy as Vite 2.7
// We don't recommend using this strategy as a general solution moving forward

// splitVendorChunk is a simple index/vendor strategy that was used in Vite
// until v2.8. It is exposed to let people continue to use it in case it was
// working well for their setups.
// The cache needs to be reset on buildStart for watch mode to work correctly
// Don't use this manualChunks strategy for ssr, lib mode, and 'umd' or 'iife'

export class SplitVendorChunkCache {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class seems unnecessary. The Map object has a clear method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we will need to expand the cache later. I wanted to expose the cache for the method, not a particular Map that is an implementation detail and may change

cache: Map<string, boolean>
constructor() {
this.cache = new Map<string, boolean>()
}
reset() {
this.cache = new Map<string, boolean>()
}
}

export function splitVendorChunk({
cache = new SplitVendorChunkCache()
}): GetManualChunk {
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
return (id, { getModuleInfo }) => {
if (
id.includes('node_modules') &&
!isCSSRequest(id) &&
staticImportedByEntry(id, getModuleInfo, cache.cache)
) {
return 'vendor'
}
}
}

function staticImportedByEntry(
id: string,
getModuleInfo: GetModuleInfo,
cache: Map<string, boolean>,
importStack: string[] = []
): boolean {
if (cache.has(id)) {
return cache.get(id) as boolean
}
if (importStack.includes(id)) {
// circular deps!
cache.set(id, false)
return false
}
const mod = getModuleInfo(id)
if (!mod) {
cache.set(id, false)
return false
}

if (mod.isEntry) {
cache.set(id, true)
return true
}
const someImporterIs = mod.importers.some((importer) =>
staticImportedByEntry(
importer,
getModuleInfo,
cache,
importStack.concat(id)
)
)
cache.set(id, someImporterIs)
return someImporterIs
}

export function splitVendorChunkPlugin(): Plugin {
const caches: SplitVendorChunkCache[] = []
function createSplitVendorChunk(output: OutputOptions, config: UserConfig) {
const cache = new SplitVendorChunkCache()
caches.push(cache)
const build = config.build ?? {}
const format = output?.format
if (!build.ssr && !build.lib && format !== 'umd' && format !== 'iife') {
return splitVendorChunk({ cache })
}
}
return {
name: 'vite:split-vendor-chunk',
config(config) {
let outputs = config?.build?.rollupOptions?.output
if (outputs) {
outputs = Array.isArray(outputs) ? outputs : [outputs]
for (const output of outputs) {
output.manualChunks = createSplitVendorChunk(output, config)
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
return {
build: {
rollupOptions: {
manualChunks: createSplitVendorChunk({}, config)
}
}
}
}
},
buildStart() {
caches.forEach((cache) => cache.reset())
}
}
}