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(mc-scripts): manually group some vendor chunks #3615

Merged
merged 12 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/grumpy-fireants-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@commercetools-frontend/mc-scripts': minor
---

We're introducing a change the way `build` command works in terms of bundles generation.
We've updated the configuration in order to reduce the size of the application's entry point bundle in favor of three four smaller bundles (which can be downloaded in parallel by the browser).

This change has been applied to the build process no matter if you use `webpack` or `vite`.
Remember `webpack` is used by default and you can opt-in to `vite` by using this environment variable:

```bash
ENABLE_EXPERIMENTAL_VITE_BUNDLER=true
```

Also, if you are using `vite`, we've also added a couple of plugins you can use to analyze the built bundle sizes:

- [vite-bundle-analyzer](https://github.com/KusStar/vite-bundle-visualizer)
- [rollup-plugin-visualizer](https://github.com/btd/rollup-plugin-visualizer) (tree visualization)

You can use them by setting this environment variables:

```bash
# vite-bundle-analyzer
ANALYZE_BUNDLE=true
# rollup-plugin-visualizer
ANALYZE_BUNDLE_TREE=true
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@
"@typescript-eslint/parser": "^5.52.0"
},
"patchedDependencies": {
"[email protected]": "patches/[email protected]"
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]"
Copy link
Contributor

Choose a reason for hiding this comment

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

vite-bundle-analyzer has a rollup very flexible peer dependency (2.X -> 4.X) but it actually requires 3.X at least because of a typescript type usage.
Since we're using rollup 2.X in the repo (force by rollup), the alternative I took is to change the type usage (very simple change)

}
},
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions packages/mc-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"querystring-es3": "^0.2.1",
"react-dev-utils": "12.0.1",
"react-refresh": "0.14.2",
"rollup-plugin-visualizer": "^5.12.0",
"serve-handler": "6.1.5",
"shelljs": "0.8.5",
"style-loader": "3.3.4",
Expand All @@ -108,6 +109,7 @@
"thread-loader": "3.0.4",
"url": "^0.11.0",
"vite": "~4.5.3",
"vite-bundle-analyzer": "0.12.1",
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if the patch will be used here when someone installs the mc-scripts package.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair question. Couldn't find documentation on it. I must say I assume it would be applied.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know either but it won't matter in this case: the change in the source code is about a Typescript type which is not generic in the rollup version we're using but the dependency code expects it to be.
So, the generated code is exactly the same (no typings go there).

"webpack": "5.94.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-dev-server": "4.15.2",
Expand Down
17 changes: 15 additions & 2 deletions packages/mc-scripts/src/commands/build-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import path from 'path';
import pluginGraphql from '@rollup/plugin-graphql';
import pluginReact from '@vitejs/plugin-react';
import fs from 'fs-extra';
import { build, type Plugin } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
import { build, PluginOption, type Plugin } from 'vite';
import { analyzer } from 'vite-bundle-analyzer';
import { packageLocation as applicationStaticAssetsPath } from '@commercetools-frontend/assets';
import { generateTemplate } from '@commercetools-frontend/mc-html-template';
import { getViteCacheGroups } from '../config/optimizations';
import paths from '../config/paths';
import pluginDynamicBaseAssetsGlobals from '../vite-plugins/vite-plugin-dynamic-base-assets-globals';
import pluginI18nMessageCompilation from '../vite-plugins/vite-plugin-i18n-message-compilation';
Expand All @@ -28,6 +31,11 @@ async function run() {
// Write `index.html` (template) into the `/public` folder.
fs.writeFileSync(paths.appIndexHtml, html, { encoding: 'utf8' });

const appDependencies = require(paths.appPackageJson).dependencies as Record<
string,
string
>;

await build({
root: paths.appRoot,
base: './', // <-- Important to allow configuring the runtime base path.
Expand All @@ -43,7 +51,8 @@ async function run() {
// NOTE that after the build, Vite will write the `index.html` (template)
// at the `/public/public/index.html` location. See `fs.renameSync` below.
input: paths.appIndexHtml,
// Reduce the memory footpring when building sourcemaps.
output: { manualChunks: getViteCacheGroups(appDependencies) },
// Reduce the memory footprint when building sourcemaps.
// https://github.com/vitejs/vite/issues/2433#issuecomment-1361094727
cache: false,
},
Expand Down Expand Up @@ -79,6 +88,10 @@ async function run() {
pluginSvgr(),
pluginDynamicBaseAssetsGlobals(),
pluginI18nMessageCompilation(),
process.env.ANALYZE_BUNDLE === 'true' &&
Copy link
Contributor

@CarlosCortizasCT CarlosCortizasCT Oct 10, 2024

Choose a reason for hiding this comment

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

This plugin create the bundles size visualization:
Screenshot 2024-10-08 at 18 31 02

analyzer({ defaultSizes: 'parsed', openAnalyzer: true }),
process.env.ANALYZE_BUNDLE_TREE === 'true' &&
Copy link
Contributor

Choose a reason for hiding this comment

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

This plugin creates a bundles dependency visualization:
Screenshot 2024-10-08 at 18 32 05

(visualizer({ open: true, template: 'network' }) as PluginOption),
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import createPostcssConfig from './create-postcss-config';
import hasJsxRuntime from './has-jsx-runtime';
// https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros
import momentLocalesToKeep from /* preval */ './moment-locales';
import { getWepbackCacheGroups } from './optimizations';
import paths from './paths';
import vendorsToTranspile from './vendors-to-transpile';

Expand Down Expand Up @@ -57,6 +58,11 @@ function createWebpackConfigForDevelopment(
},
} as Required<TWebpackConfigOptions<'development'>>;

const appDependencies = require(paths.appPackageJson).dependencies as Record<
string,
string
>;

return {
// https://webpack.js.org/concepts/#mode
mode: 'development',
Expand All @@ -71,17 +77,15 @@ function createWebpackConfigForDevelopment(
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
// https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a
optimization: {
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
splitChunks: {
chunks: 'all',
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
cacheGroups: getWepbackCacheGroups(appDependencies),
},
moduleIds: 'named',
chunkIds: 'deterministic',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import createPostcssConfig from './create-postcss-config';
import hasJsxRuntime from './has-jsx-runtime';
// https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros
import momentLocalesToKeep from /* preval */ './moment-locales';
import { getWepbackCacheGroups } from './optimizations';
import paths from './paths';
import vendorsToTranspile from './vendors-to-transpile';

Expand Down Expand Up @@ -64,6 +65,10 @@ function createWebpackConfigForProduction(
...options.toggleFlags,
},
} as Required<TWebpackConfigOptions<'production'>>;
const appDependencies = require(paths.appPackageJson).dependencies as Record<
string,
string
>;

return {
// Don't attempt to continue if there are any errors.
Expand Down Expand Up @@ -128,6 +133,9 @@ function createWebpackConfigForProduction(
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
cacheGroups: getWepbackCacheGroups(appDependencies),
},
moduleIds: 'named',
chunkIds: 'deterministic',
},
Expand Down
69 changes: 69 additions & 0 deletions packages/mc-scripts/src/config/optimizations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import chalk from 'chalk';

// Dependencies to be split/grouped into separate chunks.
// This is useful to reduce the main bundle size and have more
// dedicated caching strategy for specific chunks.
const manualChunks = {
'commercetools-uikit-icons': ['@commercetools-uikit/icons'],
moment: ['moment', 'moment-timezone'],
'app-shell': [
'@commercetools-frontend/application-shell',
'@commercetools-frontend/application-shell-connectors',
],
};

const removeNonExistantDependencies = (
Copy link
Contributor

Choose a reason for hiding this comment

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

This is just to make sure the packages we're defining in the manualChunks map actually exists in the application to build.
This is a safety measure so in case one of them are remove (eg: moment) the build does not fail but will only emit a warning about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the comment. I was fearful we'd remove something else but those we specify at first.

manualChunks: Record<string, string[]>,
appDependencies: string[]
) => {
return Object.entries(manualChunks).reduce(
(chunkGroups, [chunkName, vendors]) => {
const existingVendors = vendors.filter((vendor) =>
appDependencies.includes(vendor)
);
if (existingVendors.length > 0) {
chunkGroups[chunkName] = existingVendors;
} else {
console.log(
chalk.yellow(
`\nFiltering out chunk "${chunkName}" because its configured dependencies does not exist in the application: ${vendors}\n`
)
);
}
return chunkGroups;
},
{} as Record<string, string[]>
);
};

// Reference: https://rollupjs.org/configuration-options/#output-manualchunks
export function getViteCacheGroups(appDependencies: Record<string, string>) {
return removeNonExistantDependencies(
manualChunks,
Object.keys(appDependencies)
);
}

// Reference: https://webpack.js.org/plugins/split-chunks-plugin/
export function getWepbackCacheGroups(appDependencies: Record<string, string>) {
const filteredDependencies = removeNonExistantDependencies(
manualChunks,
Object.keys(appDependencies)
);

const result = Object.entries(filteredDependencies).reduce(
(previousChunks, [chunkName, vendors]) => {
return {
...previousChunks,
[chunkName]: {
test: new RegExp(`[\\/]node_modules[\\/](${vendors.join('|')})[\\/]`),
name: chunkName,
chunks: 'all',
},
};
},
{}
);

return result;
}
63 changes: 63 additions & 0 deletions patches/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 6a491b2b03b5bf351e1ff07e2ac8659bd47c54c5..40d52d026125492824f8fa35f141a00fa9f3eec3 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,34 +1,37 @@
-import { Plugin } from 'vite';
-import { ZlibOptions } from 'zlib';
-import { Plugin as Plugin$1 } from 'rollup';
+import { Plugin } from "vite";
+import { ZlibOptions } from "zlib";
+import { Plugin as Plugin$1 } from "rollup";

-type AnalyzerMode = 'static' | 'json' | 'server';
-type DefaultSizes = 'stat' | 'parsed' | 'gzip';
+type AnalyzerMode = "static" | "json" | "server";
+type DefaultSizes = "stat" | "parsed" | "gzip";
interface BasicAnalyzerPluginOptions {
- summary?: boolean;
- analyzerMode?: AnalyzerMode;
- reportTitle?: string;
- defaultSizes?: DefaultSizes;
- gzipOptions?: ZlibOptions;
+ summary?: boolean;
+ analyzerMode?: AnalyzerMode;
+ reportTitle?: string;
+ defaultSizes?: DefaultSizes;
+ gzipOptions?: ZlibOptions;
}
interface AnalyzerPluginOptionsWithServer extends BasicAnalyzerPluginOptions {
- analyzerMode?: 'server';
- analyzerPort?: number | 'auto';
- openAnalyzer?: boolean;
+ analyzerMode?: "server";
+ analyzerPort?: number | "auto";
+ openAnalyzer?: boolean;
}
interface AnalyzerPluginOptionsWithJson extends BasicAnalyzerPluginOptions {
- analyzerMode?: 'json';
- fileName?: string;
+ analyzerMode?: "json";
+ fileName?: string;
}
interface AnalyzerPluginOptionsWithStatic extends BasicAnalyzerPluginOptions {
- analyzerMode?: 'static';
- analyzerPort?: number | 'auto';
- openAnalyzer?: boolean;
- fileName?: string;
+ analyzerMode?: "static";
+ analyzerPort?: number | "auto";
+ openAnalyzer?: boolean;
+ fileName?: string;
}
-type AnalyzerPluginOptions = AnalyzerPluginOptionsWithServer | AnalyzerPluginOptionsWithStatic | AnalyzerPluginOptionsWithJson;
+type AnalyzerPluginOptions =
+ | AnalyzerPluginOptionsWithServer
+ | AnalyzerPluginOptionsWithStatic
+ | AnalyzerPluginOptionsWithJson;

-declare function adapter(userPlugin: Plugin): Plugin$1<any>;
+declare function adapter(userPlugin: Plugin): Plugin$1;

declare function analyzer(opts?: AnalyzerPluginOptions): Plugin;

Loading