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 to define minimum chunk size limit #4705

Merged
merged 18 commits into from Nov 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion .eslintrc.js
Expand Up @@ -74,7 +74,11 @@ module.exports = {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', ignoreRestSiblings: true, varsIgnorePattern: '^_' }
],
'arrow-body-style': ['error', 'as-needed'],
'dot-notation': 'error',
'import/no-unresolved': [
'error',
Expand Down
2 changes: 1 addition & 1 deletion browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@rollup/browser",
"version": "3.2.5",
"version": "3.3.0-0",
"description": "Next-generation ES module bundler browser build",
"main": "dist/rollup.browser.js",
"module": "dist/es/rollup.browser.js",
Expand Down
10 changes: 10 additions & 0 deletions docs/999-big-list-of-options.md
Expand Up @@ -1890,6 +1890,16 @@ Type: `number`<br> CLI: `--experimentalCacheExpiry <numberOfRuns>`<br> Default:

Determines after how many runs cached assets that are no longer used by plugins should be removed.

#### experimentalMinChunkSize

Type: `number`<br> CLI: `--experimentalMinChunkSize <size>`<br> Default: `0`

Set a minimal chunk size target in Byte for code-splitting setups. When this value is greater than `0`, Rollup will try to merge any chunk that does not have side effects when executed, i.e. any chunk that only contains function definitions etc., and is below this size limit into another chunk that is likely to be loaded under similar conditions.

This will mean that the generated bundle will possibly load code that is not required yet in order to reduce the number of chunks. The condition for the merged chunks to be side effect free ensures that this does not change behaviour.

Unfortunately, due to the way chunking works, chunk size is measured before any chunk rendering plugins like minifiers ran, which means you should use a high enough limit to take this into account.

#### perf

Type: `boolean`<br> CLI: `--perf`/`--no-perf`<br> Default: `false`
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "rollup",
"version": "3.2.5",
"version": "3.3.0-0",
"description": "Next-generation ES module bundler",
"main": "dist/rollup.js",
"module": "dist/es/rollup.js",
Expand Down
9 changes: 7 additions & 2 deletions src/Bundle.ts
Expand Up @@ -158,7 +158,8 @@ export default class Bundle {
bundle: OutputBundleWithPlaceholders,
getHashPlaceholder: HashPlaceholderGenerator
): Promise<Chunk[]> {
const { inlineDynamicImports, manualChunks, preserveModules } = this.outputOptions;
const { experimentalMinChunkSize, inlineDynamicImports, manualChunks, preserveModules } =
this.outputOptions;
const manualChunkAliasByEntry =
typeof manualChunks === 'object'
? await this.addManualChunks(manualChunks)
Expand All @@ -177,7 +178,11 @@ export default class Bundle {
? [{ alias: null, modules: includedModules }]
: preserveModules
? includedModules.map(module => ({ alias: null, modules: [module] }))
: getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) {
: getChunkAssignments(
this.graph.entryModules,
manualChunkAliasByEntry,
experimentalMinChunkSize
)) {
sortByExecutionOrder(modules);
const chunk = new Chunk(
modules,
Expand Down
2 changes: 1 addition & 1 deletion src/Module.ts
Expand Up @@ -219,6 +219,7 @@ export default class Module {
readonly info: ModuleInfo;
isExecuted = false;
isUserDefinedEntryPoint = false;
declare magicString: MagicString;
declare namespace: NamespaceVariable;
needsExportShim = false;
declare originalCode: string;
Expand All @@ -241,7 +242,6 @@ export default class Module {
private exportNamesByVariable: Map<Variable, string[]> | null = null;
private readonly exportShimVariable = new ExportShimVariable(this);
private readonly exports = new Map<string, ExportDescription>();
private declare magicString: MagicString;
private readonly namespaceReexportsByName = new Map<
string,
[variable: Variable | null, indirectExternal?: boolean]
Expand Down
16 changes: 8 additions & 8 deletions src/finalisers/es.ts
Expand Up @@ -62,11 +62,11 @@ function getImportBlock(dependencies: ChunkDependency[], { _ }: GenerateCodeSnip
} else if (importedNames.length > 0) {
importBlock.push(
`import ${defaultImport ? `${defaultImport.local},${_}` : ''}{${_}${importedNames
.map(specifier => {
return specifier.imported === specifier.local
.map(specifier =>
specifier.imported === specifier.local
? specifier.imported
: `${specifier.imported} as ${specifier.local}`;
})
: `${specifier.imported} as ${specifier.local}`
)
.join(`,${_}`)}${_}}${_}from${_}${pathWithAssertion}`
);
}
Expand Down Expand Up @@ -105,11 +105,11 @@ function getImportBlock(dependencies: ChunkDependency[], { _ }: GenerateCodeSnip
if (namedReexports.length > 0) {
importBlock.push(
`export${_}{${_}${namedReexports
.map(specifier => {
return specifier.imported === specifier.reexported
.map(specifier =>
specifier.imported === specifier.reexported
? specifier.imported
: `${specifier.imported} as ${specifier.reexported}`;
})
: `${specifier.imported} as ${specifier.reexported}`
)
.join(`,${_}`)}${_}}${_}from${_}${pathWithAssertion}`
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/rollup/types.d.ts
Expand Up @@ -639,6 +639,7 @@ export interface OutputOptions {
dynamicImportInCjs?: boolean;
entryFileNames?: string | ((chunkInfo: PreRenderedChunk) => string);
esModule?: boolean | 'if-default-prop';
experimentalMinChunkSize?: number;
exports?: 'default' | 'named' | 'none' | 'auto';
extend?: boolean;
externalImportAssertions?: boolean;
Expand Down Expand Up @@ -691,6 +692,7 @@ export interface NormalizedOutputOptions {
dynamicImportInCjs: boolean;
entryFileNames: string | ((chunkInfo: PreRenderedChunk) => string);
esModule: boolean | 'if-default-prop';
experimentalMinChunkSize: number;
exports: 'default' | 'named' | 'none' | 'auto';
extend: boolean;
externalImportAssertions: boolean;
Expand Down
167 changes: 156 additions & 11 deletions src/utils/chunkAssignment.ts
@@ -1,21 +1,26 @@
import ExternalModule from '../ExternalModule';
import Module from '../Module';
import { getOrCreate } from './getOrCreate';
import { concatLazy } from './iterators';
import { timeEnd, timeStart } from './timers';

type DependentModuleMap = Map<Module, Set<Module>>;
type ChunkDefinitions = { alias: string | null; modules: Module[] }[];

export function getChunkAssignments(
entryModules: readonly Module[],
manualChunkAliasByEntry: ReadonlyMap<Module, string>
manualChunkAliasByEntry: ReadonlyMap<Module, string>,
minChunkSize: number
): ChunkDefinitions {
const chunkDefinitions: ChunkDefinitions = [];
const modulesInManualChunks = new Set(manualChunkAliasByEntry.keys());
const manualChunkModulesByAlias: Record<string, Module[]> = Object.create(null);
for (const [entry, alias] of manualChunkAliasByEntry) {
const chunkModules = (manualChunkModulesByAlias[alias] =
manualChunkModulesByAlias[alias] || []);
addStaticDependenciesToManualChunk(entry, chunkModules, modulesInManualChunks);
addStaticDependenciesToManualChunk(
entry,
(manualChunkModulesByAlias[alias] ||= []),
modulesInManualChunks
);
}
for (const [alias, modules] of Object.entries(manualChunkModulesByAlias)) {
chunkDefinitions.push({ alias, modules });
Expand Down Expand Up @@ -87,7 +92,11 @@ export function getChunkAssignments(
}

chunkDefinitions.push(
...createChunks([...entryModules, ...dynamicEntryModules], assignedEntryPointsByModule)
...createChunks(
[...entryModules, ...dynamicEntryModules],
assignedEntryPointsByModule,
minChunkSize
)
);
return chunkDefinitions;
}
Expand Down Expand Up @@ -163,15 +172,93 @@ function getDynamicDependentEntryPoints(
return dynamicallyDependentEntryPointsByDynamicEntry;
}

interface ChunkDescription {
alias: null;
modules: Module[];
signature: string;
size: number | null;
}

interface MergeableChunkDescription extends ChunkDescription {
size: number;
}

function createChunks(
allEntryPoints: readonly Module[],
assignedEntryPointsByModule: DependentModuleMap
assignedEntryPointsByModule: DependentModuleMap,
minChunkSize: number
): ChunkDefinitions {
const chunkModulesBySignature = getChunkModulesBySignature(
assignedEntryPointsByModule,
allEntryPoints
);
return minChunkSize === 0
? Object.values(chunkModulesBySignature).map(modules => ({
alias: null,
modules
}))
: getOptimizedChunks(chunkModulesBySignature, minChunkSize);
}

function getOptimizedChunks(
chunkModulesBySignature: { [chunkSignature: string]: Module[] },
minChunkSize: number
) {
timeStart('optimize chunks', 3);
const { chunksToBeMerged, unmergeableChunks } = getMergeableChunks(
chunkModulesBySignature,
minChunkSize
);
for (const sourceChunk of chunksToBeMerged) {
chunksToBeMerged.delete(sourceChunk);
let closestChunk: ChunkDescription | null = null;
let closestChunkDistance = Infinity;
const { signature, size, modules } = sourceChunk;

for (const targetChunk of concatLazy(chunksToBeMerged, unmergeableChunks)) {
const distance = getSignatureDistance(
signature,
targetChunk.signature,
!chunksToBeMerged.has(targetChunk)
);
if (distance === 1) {
closestChunk = targetChunk;
break;
} else if (distance < closestChunkDistance) {
closestChunk = targetChunk;
closestChunkDistance = distance;
}
}
if (closestChunk) {
closestChunk.modules.push(...modules);
if (chunksToBeMerged.has(closestChunk)) {
closestChunk.signature = mergeSignatures(signature, closestChunk.signature);
if ((closestChunk.size += size) > minChunkSize) {
chunksToBeMerged.delete(closestChunk);
unmergeableChunks.push(closestChunk);
}
}
} else {
unmergeableChunks.push(sourceChunk);
}
}
timeEnd('optimize chunks', 3);
return unmergeableChunks;
}

const CHAR_DEPENDENT = 'X';
const CHAR_INDEPENDENT = '_';
const CHAR_CODE_DEPENDENT = CHAR_DEPENDENT.charCodeAt(0);

function getChunkModulesBySignature(
assignedEntryPointsByModule: Map<Module, Set<Module>>,
allEntryPoints: readonly Module[]
) {
const chunkModules: { [chunkSignature: string]: Module[] } = Object.create(null);
for (const [module, assignedEntryPoints] of assignedEntryPointsByModule) {
let chunkSignature = '';
for (const entry of allEntryPoints) {
chunkSignature += assignedEntryPoints.has(entry) ? 'X' : '_';
chunkSignature += assignedEntryPoints.has(entry) ? CHAR_DEPENDENT : CHAR_INDEPENDENT;
}
const chunk = chunkModules[chunkSignature];
if (chunk) {
Expand All @@ -180,8 +267,66 @@ function createChunks(
chunkModules[chunkSignature] = [module];
}
}
return Object.values(chunkModules).map(modules => ({
alias: null,
modules
}));
return chunkModules;
}

function getMergeableChunks(
chunkModulesBySignature: { [chunkSignature: string]: Module[] },
minChunkSize: number
) {
const chunksToBeMerged = new Set() as Set<MergeableChunkDescription> & {
has(chunk: unknown): chunk is MergeableChunkDescription;
};
const unmergeableChunks: ChunkDescription[] = [];
const alias = null;
for (const [signature, modules] of Object.entries(chunkModulesBySignature)) {
let size = 0;
checkModules: {
for (const module of modules) {
if (module.hasEffects()) {
break checkModules;
}
size += module.magicString.toString().length;
if (size > minChunkSize) {
break checkModules;
}
}
chunksToBeMerged.add({ alias, modules, signature, size });
continue;
}
unmergeableChunks.push({ alias, modules, signature, size: null });
}
return { chunksToBeMerged, unmergeableChunks };
}

function getSignatureDistance(
sourceSignature: string,
targetSignature: string,
enforceSubset: boolean
): number {
let distance = 0;
const { length } = sourceSignature;
for (let index = 0; index < length; index++) {
const sourceValue = sourceSignature.charCodeAt(index);
if (sourceValue !== targetSignature.charCodeAt(index)) {
if (enforceSubset && sourceValue === CHAR_CODE_DEPENDENT) {
return Infinity;
}
distance++;
}
}
return distance;
}

function mergeSignatures(sourceSignature: string, targetSignature: string): string {
let signature = '';
const { length } = sourceSignature;
for (let index = 0; index < length; index++) {
signature +=
sourceSignature.charCodeAt(index) === CHAR_CODE_DEPENDENT ||
targetSignature.charCodeAt(index) === CHAR_CODE_DEPENDENT
? CHAR_DEPENDENT
: CHAR_INDEPENDENT;
}
return signature;
}
10 changes: 10 additions & 0 deletions src/utils/iterators.ts
@@ -0,0 +1,10 @@
/**
* Concatenate a number of iterables to a new iterable without fully evaluating
* their iterators. Useful when e.g. working with large sets or lists and when
* there is a chance that the iterators will not be fully exhausted.
*/
export function* concatLazy<T>(...iterables: Iterable<T>[]) {
for (const iterable of iterables) {
yield* iterable;
}
}
1 change: 1 addition & 0 deletions src/utils/options/mergeOptions.ts
Expand Up @@ -232,6 +232,7 @@ async function mergeOutputOptions(
dynamicImportInCjs: getOption('dynamicImportInCjs'),
entryFileNames: getOption('entryFileNames'),
esModule: getOption('esModule'),
experimentalMinChunkSize: getOption('experimentalMinChunkSize'),
exports: getOption('exports'),
extend: getOption('extend'),
externalImportAssertions: getOption('externalImportAssertions'),
Expand Down
1 change: 1 addition & 0 deletions src/utils/options/normalizeOutputOptions.ts
Expand Up @@ -50,6 +50,7 @@ export async function normalizeOutputOptions(
dynamicImportInCjs: config.dynamicImportInCjs ?? true,
entryFileNames: getEntryFileNames(config, unsetOptions),
esModule: config.esModule ?? 'if-default-prop',
experimentalMinChunkSize: config.experimentalMinChunkSize || 0,
exports: getExports(config, unsetOptions),
extend: config.extend || false,
externalImportAssertions: config.externalImportAssertions ?? true,
Expand Down