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

Fix treeshaking #2232

Closed
wants to merge 8 commits into from
Closed

Fix treeshaking #2232

wants to merge 8 commits into from

Conversation

redallen
Copy link
Contributor

@redallen redallen commented Jul 31, 2019

https://jira.coreos.com/browse/CONSOLE-1651

Let's go down a rabbit hole. Currently, our vendor bundle has a > 10MB stat size and looks like this:

image

It should NOT include all of patternfly-react since we don't use all of patternfly-react. Likewise, it should not include the huge c3 dependency of patternfly-react's charts, which are unused in any files under the frontend folder. It also shouldn't include all of @patternfly/react-core and numerous other sections of packages.

This critical line of the webpack config is causing NONE of our code to be treeshaken: exclude: /node_modules/,. This is because of yarn workspaces -- it can most obviously be seen right in the entrypoint of the webpack config: entry: [...'@console/app',]. Well whoops, @console/app is under node_modules, if only by a symlink to packages/console-app so it will never be tree shaken.

This is easily solvable with a regex like /node_modules\/(?!@console\/)/, but new problems arise:

  1. We still have loader={() => import('patternfly-react').then(m => m.Switch)} of which Webpack only sees the import and then (ironically) loads the whole module synchronously for the bundle. This can be solved.
  2. The Kubevirt plugin imports kubevirt-web-ui-components, which only has a js export and can't be tree shaken. If esm modules are provided, the bundle size decreases by half to 5.5MB:
    Screenshot from 2019-08-06 15-43-22

I'm currently venturing upstream to kubevirt-web-ui-components to provide some ESM exports. @suomiy @mareklibra .

@openshift-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: redallen
To complete the pull request process, please assign rhamilto
You can assign the PR to them by writing /assign @rhamilto in a comment when ready.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci-robot
Copy link
Contributor

Hi @redallen. Thanks for your PR.

I'm waiting for a openshift member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@openshift-ci-robot openshift-ci-robot added needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Jul 31, 2019
@alecmerdler
Copy link
Contributor

/ok-to-test

@openshift-ci-robot openshift-ci-robot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Jul 31, 2019
@alecmerdler alecmerdler self-requested a review July 31, 2019 15:17
@christianvogt
Copy link
Contributor

This doesn't seem right to me to have ts-loader process node_modules. Is tree shaking completely broken right now or only some particular modules?

@redallen
Copy link
Contributor Author

Yeah, this one is actually looking more complicated than I thought a few hours ago.

Currently, this PR results in bundles like this:

image (1)

where the vendors~main chunk seems to be correctly shaken down from 10MB to 5.33MB. However, there's now additional chunks that just contain those modules that should be tree shaken, so I thought this was a fix too soon.

This issue can most prevalently be seen by c3 and d3 which are a part of patternfly-react charts, but aren't used anywhere. All dependencies seem to be wholly included, whether used or not.

In a test webpack project, patternfly-react is tree shaken just fine using JS. I'll slowly build my minimum test case scenario to have a configuration like yours.

@christianvogt
Copy link
Contributor

@redallen is there an issue tracking this with specific details as to what modules are affected and captures the difference between the current and the expected modules to include to some extent?

d3 is being used by dev-console. Perhaps we can look into better ways to tree shake it. But first an answer to the above would be great.

@redallen
Copy link
Contributor Author

redallen commented Aug 2, 2019

I just opened #2251 to track this. c3 is the largest offender that can only included by patternfly-react (it depends on it for charts):

➜  frontend git:(upmaster) yarn why c3 
yarn why v1.15.2
[1/4] Why do we have the module "c3"...?
[2/4] Initialising dependency graph...
[3/4] Finding dependency...
[4/4] Calculating file sizes...
=> Found "c3@0.4.23"
info Reasons this module exists
   - "_project_#patternfly" depends on it
   - Hoisted from "_project_#patternfly#c3"
   - Hoisted from "_project_#patternfly-react#react-c3js#c3"
   - Hoisted from "_project_#patternfly-react#patternfly#c3"
   - Hoisted from "_project_#patternfly-react-extensions#patternfly#c3"

@redallen redallen changed the title have ts-loader parse node_modules for webpack to treeshake Don't use node module resolution for packages we want to treeshake Aug 6, 2019
@spadgett spadgett added this to the v4.2 milestone Aug 9, 2019
@openshift-ci-robot openshift-ci-robot added component/core Related to console core functionality component/kubevirt Related to kubevirt-plugin component/olm Related to OLM component/sdk Related to console-plugin-sdk size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Aug 16, 2019
@openshift-ci-robot openshift-ci-robot added the component/dev-console Related to dev-console label Aug 16, 2019
@redallen
Copy link
Contributor Author

redallen commented Aug 16, 2019

Giving this another shot since kubevirt/web-ui-components#536 and kubevirt/web-ui-components#539 were merged. Bundle size isn't as good as I hoped for since kubevirt-plugin/src/plugin is larger than anticipated (4MB), but it still improves the vendor bundle by 3.2 MB down to 7.53MB and shaves nearly 300kb off the gzipped bundle:
image

@redallen redallen changed the title Don't use node module resolution for packages we want to treeshake Fix treeshaking Aug 16, 2019
@spadgett
Copy link
Member

With code splitting, I wouldn't expect any kubevirt dependency to appear in the main vendor bundle. We should try to track down what's pulling it in.

@openshift-ci-robot
Copy link
Contributor

openshift-ci-robot commented Aug 16, 2019

@redallen: The following test failed, say /retest to rerun them all:

Test name Commit Details Rerun command
ci/prow/e2e-aws-console-olm ab7335b link /test e2e-aws-console-olm

Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

@@ -331,7 +332,7 @@ export const CreateOperandForm: React.FC<CreateOperandFormProps> = (props) => {
}
if (field.capabilities.includes(SpecCapability.booleanSwitch)) {
return <AsyncComponent
loader={() => import('patternfly-react').then(m => m.Switch)}
loader={() => Switch}
Copy link
Member

Choose a reason for hiding this comment

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

No reason to use AsyncComponent now. Are we certain this change is needed?

We might just switch to the PF4 switch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's needed or we need to make a Webpack chunk for it so all of patternfly-react isn't included.

Copy link
Member

Choose a reason for hiding this comment

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

It's needed or we need to make a Webpack chunk for it so all of patternfly-react isn't included.

Right, I see it now 👍

@@ -56,7 +57,7 @@ const BooleanSwitch: React.FC<SpecCapabilityProps> = (props) => {

return <div className="co-spec-descriptor--switch">
<AsyncComponent
loader={() => import('patternfly-react').then(m => m.Switch)}
loader={() => Switch}
Copy link
Member

Choose a reason for hiding this comment

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

No reason to use AsyncComponent any longer if we're already importing Switch

@@ -39,7 +36,7 @@ const config: webpack.Configuration = {
{ test: /\.glsl$/, loader: 'raw!glslify' },
{
test: /(\.jsx?)|(\.tsx?)$/,
exclude: /node_modules/,
exclude: /node_modules\/(?!(@console)\/)/,
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure this is right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm sure this is the magic line. The current exclude excludes everything since all @console/* references actually are in node_modules/@console/*.

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 sure this is the magic line. The current exclude excludes everything since all @console/* references actually are in node_modules/@console/*.

I'm still not sure. If I only bump kubevirt-web-ui-components with no other changes, I get a main vendor bundle size of 7.52MB, which is the same as #2232 (comment)

@spadgett
Copy link
Member

/cc @vojtechszocs

@spadgett
Copy link
Member

OK I see what's pulling kubevirt-web-ui-components into the main vendor bundle.

kubevirt-plugin/src/index.tsx ->
kubevirt-plugin/src/components/dashboards-page/overview-dashboard/inventory ->
kubevirt-web-ui-components

Commenting that out with no other changes results in this:

Screen Shot 2019-08-16 at 4 42 00 PM

@openshift-ci-robot
Copy link
Contributor

@redallen: PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@openshift-ci-robot openshift-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Aug 17, 2019
@spadgett
Copy link
Member

Thanks for your work on this!

I think the right approach is to make these changes separately and check how each impacts bundle sizes. This PR has several different changes, and it's hard to evaluate what impact each has in a single PR.

I hope you don't mind, but I opened #2383 specifically for the Switch change. I removed AsyncComponent in those files entirely as it's not needed.

I suspect the two big wins will be:

  1. Enabling dead code elimination for kubevirt-web-ui-components (already done now, thanks @redallen!)
  2. Removing kubevirt-web-ui-components from the main vendor bundle

I looked at (2). It's getting pulled in for the inventory card on the dashboard. Unfortunately, it needs kubevirt-web-ui-components for getVmStatus. This is not easy to just copy into the kubevirt plugin. It relies on a bunch of helpers spread across several modules. I think the kubevirt team will need to investigate.

In general, we should look at code splitting static plugins. Ideally, inactive plugins should not increase the vendor bundle size.

I also want to add a check in CI that prevents us from accidentally blowing up the main vendor bundle. It's far too easy to do, and it's difficult to track back and find the change that caused the problem initially.

@rawagner
Copy link
Contributor

Thanks for your work on this!

I think the right approach is to make these changes separately and check how each impacts bundle sizes. This PR has several different changes, and it's hard to evaluate what impact each has in a single PR.

I hope you don't mind, but I opened #2383 specifically for the Switch change. I removed AsyncComponent in those files entirely as it's not needed.

I suspect the two big wins will be:

  1. Enabling dead code elimination for kubevirt-web-ui-components (already done now, thanks @redallen!)
  2. Removing kubevirt-web-ui-components from the main vendor bundle

I looked at (2). It's getting pulled in for the inventory card on the dashboard. Unfortunately, it needs kubevirt-web-ui-components for getVmStatus. This is not easy to just copy into the kubevirt plugin. It relies on a bunch of helpers spread across several modules. I think the kubevirt team will need to investigate.

I'll create a PR to move getVmStatus to kubevirt-plugin

In general, we should look at code splitting static plugins. Ideally, inactive plugins should not increase the vendor bundle size.

I also want to add a check in CI that prevents us from accidentally blowing up the main vendor bundle. It's far too easy to do, and it's difficult to track back and find the change that caused the problem initially.

@redallen
Copy link
Contributor Author

Closing in favor of #2383 #2372, and a follow-up PR to move getVmStatus to kubevirt-plugin.

@vojtechszocs
Copy link
Contributor

vojtechszocs commented Aug 19, 2019

In general, we should look at code splitting static plugins. Ideally, inactive plugins should not increase the vendor bundle size.

@spadgett It depends on what you mean by "active" plugin.

The current definition of "active" plugin is:

  • detected via packages/console-app/package.json or provided via CONSOLE_PLUGINS env. variable (override the standard detection mechanism)
  • included in the webpack-generated bundle
  • loaded as part of Console startup (public/plugins.ts) => part of initial main-chunk

Arguably, the above definition feels closer to "bundled" or "loaded" plugin, so maybe we should rename that.

Anyway, a Console plugin contributes N extensions, each of which may be gated by feature flags (based on #2269). It's perfectly possible to have one extension not gated by anything and another one gated by something. However, flags used to gate (potentially) every extension can only be resolved after you load the plugin's entry module.

@spadgett Can you please elaborate on the "inactive" plugin concern?

@spadgett
Copy link
Member

By active, I mean a plugin that has at least one extension point whose required flag is set.

However, flags used to gate (potentially) every extension can only be resolved after you load the plugin's entry module.

That's currently true. Can there be a static declaration that says only load the plugin if at least one of these flags is set? Ideally no plugin code runs until we know it's needed.

Otherwise, we need a rule that a plugin entry module cannot pull in any 3rd party dependencies, otherwise they're included in the main vendor bundle even when not needed. I'm not sure how to enforce this, though.

@vojtechszocs
Copy link
Contributor

By active, I mean a plugin that has at least one extension point whose required flag is set.

This can be solved by loading plugins at build time, which is already possible, e.g. in plugin-stats.js

loadActivePlugins(resolvePluginPackages())

Some extensions are always-on (*), the rest is gate-able by flags via required & disallowed arrays.

(*) ModelFeatureFlag, ModelDefinition

At Console startup, we can load the extensions which are either 1, always-on or 2, whose flags constraint is empty (no required or disallowed flags specified). The remaining extensions can be put to use as soon as their flags constraint is satisfied.

However, flags used to gate (potentially) every extension can only be resolved after you load the plugin's entry module.

That's currently true. Can there be a static declaration that says only load the plugin if at least one of these flags is set? Ideally no plugin code runs until we know it's needed.

We could extend Console plugin definition in package.json like so:

"consolePlugin": {
  "entry": "src/plugin.ts",
  "entryFlags": ["FOO", "BAR"]
}

so that the plugin as a whole would be loaded only when FOO and BAR flags are not pending anymore, i.e. flagValue !== undefined.

However, this way, we exclude extensions which are either 1, always-on or 2, whose flags constraint is empty.

A much better approach IMO is, at build time, to read/process extensions into logical categories, treating "Console plugin" only as a developer / build-time representation of extensions.

Otherwise, we need a rule that a plugin entry module cannot pull in any 3rd party dependencies, otherwise they're included in the main vendor bundle even when not needed. I'm not sure how to enforce this, though.

I'd add that rule regardless of the above. It makes perfect sense. Plugin entry module should only contribute (export) extension objects. This module on its own shouldn't have any external dependencies or trigger any side-effects.

I've talked about this with @christianvogt some time ago, having the plugin entry module as JSON file would be ideal from extension definition perspective (but less practical from code perspective).

To enforce this, we can use ESLint rules that forbid static or dynamic imports.

@christianvogt
Copy link
Contributor

There's a reason plugin frameworks typically use a declarative, non-code, approach.
We need to be very strict that a plugin should never import code that has real size to it. Any code references should be using dynamic imports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component/core Related to console core functionality component/dev-console Related to dev-console component/kubevirt Related to kubevirt-plugin component/olm Related to OLM component/sdk Related to console-plugin-sdk needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants