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

Allow specific entrypoints in splitChunks.chunks option #6717

Closed
Hypnosphi opened this issue Mar 9, 2018 · 15 comments
Closed

Allow specific entrypoints in splitChunks.chunks option #6717

Hypnosphi opened this issue Mar 9, 2018 · 15 comments

Comments

@Hypnosphi
Copy link
Contributor

Hypnosphi commented Mar 9, 2018

Do you want to request a feature or report a bug?
A feature

What is the current behavior?

configuration.optimization.splitChunks.chunks should be one of these:
      "initial" | "async" | "all"

It would be handy to be able to specify particular entry points here.

If this is a feature request, what is motivation or use case for changing the behavior?
In Storybook, we have two entry points: "manager" mostly contains our library code, and "preview" contains the user code, and is loaded in an iframe. So, for purposes of long-term caching it only makes sense to split vendor chunk in "preview" entry point. Plus, there's currently no easy way to use HtmlWebpackPlugin with multiple entry points, each of which has split chunks, see jantimon/html-webpack-plugin#882

@wanglam
Copy link

wanglam commented Mar 14, 2018

It's seems like can filter entry points by pass test option.
My solutions:

function filterByEntryPoint(entry){
	return function(module, chunks){
		for (let i=0;i<chunks.length;i++) {
			const chunk = chunks[i];
			if (chunk.groupsIterable) {
				for (const group of chunk.groupsIterable) {
					if (group.getParents()[0] && group.getParents()[0].name === entry) {
						return true;
					}
				}
			}
		}
		return false;
	}
}

then put the function to test option.

splitChunks:{
			cacheGroups: {
				default: false,
				vendors: false,
				desktop: {
					name: "desktop-common",
					minChunks: 3,
					chunks: "async",
					test: filterByEntryPoint("index")
				},
				mobile: {
					name: "m-common",
					minChunks: 3,
					chunks: "async",
					test: filterByEntryPoint("indexMobile")
				}
			}
		},

@sokra
Copy link
Member

sokra commented Mar 17, 2018

Nice idea but this doesn't have the same effect as the chunks option. Passing a selector function to chunks makes sense so send a PR.

@ooflorent
Copy link
Member

Fixed by #6791

@Izhaki
Copy link

Izhaki commented May 17, 2018

I so wish the documentation would reflect this change. It's a super important feature.

@milewski
Copy link

is it released already? webpack config throw an error if i use anything other than "initial" | "async" | "all"

@Izhaki
Copy link

Izhaki commented May 25, 2018

Works for us ("webpack": "4.8.1"):

  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: {
      cacheGroups: {
          // In dev mode, we want all vendor (node_modules) to go into a chunk,
          // so building main.js is faster.
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendors",
            // Exclude pre-main dependencies going into vendors, as doing so
            // will result in webpack only loading pre-main once vendors loaded.
            // But pre-main is the one loading vendors.
            // Currently undocument feature:  https://github.com/webpack/webpack/pull/6791
            chunks: chunk => chunk.name !== "pre-main.min"
          }
      }
    }
  },

@devineloper
Copy link

Thanks @Izhaki, exactly what I needed!

@Junyan
Copy link

Junyan commented Nov 30, 2019

@wanglam I think it's better.

const FILTER_CHUNK_TYPE = {
  ALL: 'all',
  ASYNC: 'async',
  INITIAL: 'initial'
};
function filterChunkByEntryPoint({ chunk, entryName, chunkType } = {}) {
  const validateMap = {
    [FILTER_CHUNK_TYPE.ALL]: () => true,
    [FILTER_CHUNK_TYPE.ASYNC]: () => !chunk.canBeInitial(),
    [FILTER_CHUNK_TYPE.INITIAL]: () => chunk.canBeInitial()
  };

  if (validateMap[chunkType] && validateMap[chunkType]() && chunk.groupsIterable) {
    for (const group of chunk.groupsIterable) {
      let currentGroup = group;

      while (currentGroup) {
        const parentGroup = currentGroup.getParents()[0];

        if (parentGroup) {
          currentGroup = parentGroup;
        } else {
          break;
        }
      }

      // entrypoint
      if (currentGroup.name === entryName) {
        return true;
      }
    }
  }

  return false;
}
cacheGroups: {
  'app-common': {
    name: 'app-common',
    test: /node_modules/,
    chunks: (chunk) => filterChunkByEntryPoint({
      chunk,
      entry: 'app',
      chunkType: FILTER_CHUNK_TYPE.INITIAL
    })
  }
}

@Joel-James
Copy link

@Izhaki You mentioned webpack 4.8.1. Are you sure? I find the latest version of webpack as 4.43.0

@Izhaki
Copy link

Izhaki commented May 13, 2020

That post of mine with 4.8.1 is from 2 years ago.

@Joel-James
Copy link

@Izhaki Still, can you confirm the version?

@Izhaki
Copy link

Izhaki commented May 13, 2020

I'm pretty sure that the post from two years ago is correct - we were using 4.8.1 then. You can find it here: https://www.npmjs.com/package/webpack/v/4.8.1

Currently we're running 4.40.1 with this:

    splitChunks: {
      cacheGroups: {
        // We want all vendor (node_modules) to go into a chunk.
        vendors: {
          test(module, chunks) {
            // The (production) vendors bundle only includes modules that are
            // referenced from the main chunk.
            // Modules under node_modules that are referenced from the test bundle should not
            // be included (they are bundled into the test bundle).
            const isInChunk = chunkName => chunks.some(chunk => chunk.name === chunkName);
            const vendorExcludes = ['bootstrap', 'pre-main', 'start', 'analytics'];
            return (
              isVendorsModule(module) &&
              isInChunk('app') &&
              !vendorExcludes.some(bundle => isInChunk(bundle))
            );
          },
          name: 'vendors',
          // 'all' is confusing - it basically means test all modules regardless if they
          // are statically (via require(x)/import 'x'; ie 'initial' option)
          // or dynamically (via import('x'); ie 'async' option)
          // loaded.
          chunks: 'all'
        }
      }
    }

@valoricDe
Copy link

valoricDe commented Sep 27, 2022

@Izhaki what is the function content of isVendorsModule?

@Izhaki
Copy link

Izhaki commented Sep 28, 2022

@Izhaki what is the function content of isVendorsModule?

I really can't remember and no longer have access to the code. As I have been using NextJS for a while now, I haven't done DIY webpack chunk optimisations for years now...

@d-ph
Copy link
Contributor

d-ph commented Oct 11, 2022

I realise this ticket is already closed, however since it's so easy to end up reading it by finally inputing certain keywords in google, and since the ticket is quite helpful in understanding how to approach "bespoke" code splitting, I'll just leave here how I ended up configuring the SplitChunksPlugin.js in my particular use case, for the next googler to have an easier time.

Use case: Multi-page web app. Multiple entrypoints. Webapp divided into a "site" and an "admin". A desire to split out vendor js code (node_modules) + common webapp code out of entrypoints. A need to get the smallest possible vendor and common "chunk" on the "site" homepage, to satisfy google PageSpeed. Webpack v4.

// webpack.config.js

/*
 * The slashes most likely won't work on Windows. Investigate if needed.
 */
const nodeModulesRegex = /\/node_modules\//;

class MyWebpackUtils {
  /**
   * @private
   */
  static forEachModuleEntryPointName(module, forEachFn) {
    for (const moduleChunk of module.chunksIterable) {
      for (const moduleChunkGroup of moduleChunk.groupsIterable) {
        if (
          /*
           * ::isInitial() is another way of saying "is the ChunkGroup an instance of the (webpack) Entrypoint class".
           */
          moduleChunkGroup.isInitial()
          && moduleChunkGroup.name
        ) {
          const result = forEachFn(moduleChunkGroup.name);

          /*
           * Short-circuit the loop(s) if the callback requested so. In principle, "false" = "break; / return;"
           */
          if (result === false) {
            return;
          }
        }
      }
    }
  }

  static isModuleUsedInEntryPointWithPrefix(module, entryPointNamePrefix) {
    let result = false;

    this.forEachModuleEntryPointName(module, (iteratedEntryPointName) => {
      if (iteratedEntryPointName.startsWith(entryPointNamePrefix)) {
        result = true;
        return false;
      }
    });

    return result;
  }

  static isModuleUsedInEntryPointWithName(module, entryPointName) {
    let result = false;

    this.forEachModuleEntryPointName(module, (iteratedEntryPointName) => {
      if (iteratedEntryPointName === entryPointName) {
        result = true;
        return false;
      }
    });

    return result;
  }

  static isModuleFromNodeModules(module) {
    /*
     * Copy&paste from webpack's SplitChunksPlugin.js, the "regexp" case.
     */

    if (module.nameForCondition && nodeModulesRegex.test(module.nameForCondition())) {
      return true;
    }
    for (const chunk of module.chunksIterable) {
      if (chunk.name && nodeModulesRegex.test(chunk.name)) {
        return true;
      }
    }

    return false;
  };
}

// (...)

const primarySiteEntryPointsNames = [
  'site/entry1',
  'site/entry2',
  'site/entry3',
];

// (...)

    splitChunks: {
      chunks: 'all',
      name: (module, chunks, cacheGroupKey) => {
        const isVendor = MyWebpackUtils.isModuleFromNodeModules(module);
        const isSiteModule = MyWebpackUtils.isModuleUsedInEntryPointWithPrefix(module, 'site/');
        const isAdminModule = MyWebpackUtils.isModuleUsedInEntryPointWithPrefix(module, 'admin/');

        let isModuleUsedInPrimarySiteEntryPoint = false;
        for (let primarySiteEntryPointName of primarySiteEntryPointsNames) {
          if (MyWebpackUtils.isModuleUsedInEntryPointWithName(module, primarySiteEntryPointName)) {
            isModuleUsedInPrimarySiteEntryPoint = true;

            break;
          }
        }

        if (isVendor) {
          /*
           * Note: this if-statement catches both: site-only modules, and site&admin modules. This is what leads "site chunks"
           * to be needed in admin. This can be solved properly only by having separate webpack.config.js files.
           */
          if (isSiteModule) {
            return isModuleUsedInPrimarySiteEntryPoint ? 'vendors-site-primary' : 'vendors-site-secondary';
          }

          if (isAdminModule) {
            return 'vendors-admin';
          }
        }

        if (isSiteModule) {
          return isModuleUsedInPrimarySiteEntryPoint ? 'common-site-primary' : 'common-site-secondary';
        }

        if (isAdminModule) {
          return 'common-admin';
        }

        /*
         * The following is supposed to do something "sensible and default" as a fallback. The code was copied&pasted from
         * webpack docs: https://v4.webpack.js.org/plugins/split-chunks-plugin/
         */

        const moduleFileName = module.identifier().split('/').reduceRight(item => item);
        const allChunksNames = chunks.map((item) => item.name).join('~');

        return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
      }
    }

Comments:

  1. Having a separate webpack.config.js for each distinct section of a webapp ("site", "admin", etc.) is much preferred. This is because one cannot configure the split plugin to put a js module in 2 separate chunks (or I gave up too easily).
  2. The snippet above does not use cacheGroups, and does not use the ::filter option. I found it more understandable to tell the split plugin to put modules in desired chunks via the ::name root option (via a callback).
  3. The result of the above is that the "site" homepage requires "vendor-site" and "common-site-primary" (+ any entrypoints required).
  4. This particular use case may have multiple webpack entrypoints loaded on a single webpage (and the homepage's entrypoints are mentioned in the "primarySiteEntryPointsNames" variable). There are reasons for it.

I hope the above saves someone's time when configuring the split plugin beyond the defaults -- which do work on their own quite well, when one has complete control over the js setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests