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

Improve tree-shaking in bundles for new installation methods #16292

Closed
filipsobol opened this issue Apr 26, 2024 · 2 comments
Closed

Improve tree-shaking in bundles for new installation methods #16292

filipsobol opened this issue Apr 26, 2024 · 2 comments
Assignees
Labels
squad:core Issue to be handled by the Core team. type:task This issue reports a chore (non-production change) and other types of "todos".
Milestone

Comments

@filipsobol
Copy link
Member

filipsobol commented Apr 26, 2024

When working on the new installation methods (NIM), we noticed that the bundle size in the consuming projects was larger than we expected. Investigation showed that there was a lot of code in various core and commercial packages that was not properly tree-shaken.

Although we noticed this while working on NIM, this is not a new problem. Because in the old installation methods the plugins are imported directly from their respective packages, it was much less likely that the code that wasn't properly tree-shaken would end up unused. However, in NIM, all core and commercial packages are imported from the ckeditor5 and ckeditor5-premium-feature packages. This means that pulling code from one plugin will also pull side-effects from all other plugins in these packages.

How significant is this issue? When using the 0.0.0-nightly-20240426.0 build, the following imports will result in a bundle size of 1390.65 kB in Rollup and esbuild (minified, not gzipped):

import 'ckeditor5';
import 'ckeditor5-premium-features';

In a perfect world, the bundle size should be 0.0 kB because we didn't import anything specific. However, about 550 kB of that 1390.65 kB are essential packages (engine, ui, core, utils, watchdog) that are used by every plugin. While we could try to make this part tree-shakable, that's not the main focus of this task. The main focus now is to make the other 840 kB tree-shakable.

Our initial tests show that we can reduce the bundle size to 738 kB (or 182 kB minzipped) without major code changes, with the potential to reduce the size by another 150 kB. This would be a 58% reduction from the current state.

Problems

Below are the main problems that we identified.

Static methods and fields

In our codebase, we make heavy use of static methods and fields. While support for static public methods was introduced in ES2015, support for static private methods and static fields (public and private) was added in ES2022.

Because support for all four combinations has long been available in TypeScript, it and other bundlers had to transpile them to support the older browser. Unfortunately, these workarounds make the output code less tree-shakable. For a simple example, consider the following code:

export class Test {
  public static field = 123;

  public static method() {
    return 123;
  }
}

Since public static methods are well supported, the public static method() is transpiled to static method(). However, the public static field is transpiled to code that cannot be tree-shaken, and the output varies between transpilers. See the following links for TypeScript, esbuild, and SWC outputs:

Change the output target to ES2022 to see how the workarounds disappears to something that can be easily analyzed and tree-shaken.

We can't change the target in the entire codebase because that would break support for webpack 4 (that's why we only target ES2019 for now), but we can change the target only in our new CLI tool to only affect new installation methods.

Before we can do that, however, we need to fix the problem described in #14703 and #16386, because part of our code is not valid and only works because of the transpilation.

Mixins

We use mixins in our codebase to share code between classes. However, the current implementation of mixins in TypeScript is not tree-shakable. Consider the following code:

class FileLoader extends ObservableMixin() {}

When statically analyzing the code, it's impossible for bundlers to know what ObservableMixin() will return and if it contains side effects. This makes (some) classes that extend mixins not tree-shakable.

This can be easily fixed by adding the #__PURE__ comment to all mixin calls, like this:

class FileLoader extends /* #__PURE__ */ ObservableMixin() {}

Alternatively (or even preferably), we could try annotating the mixin functions with #__NO_SIDE_EFFECTS__, but we need to make sure that this comment works when using webpack, Vite, and esbuild.

Other problems

  • Remove code added for backward compatibility with mix.
  • Make it so that protobufjs and compiled messages can be tree-shaken when real-time plugins are not used.
  • (Lower priority) To get the types for the is methods right, we used some workarounds which are not tree-shakable. We should move the is implementations to static methods without breaking the API / types.
  • (Lower priority) The way we use turndown and marked makes the bundlers unable to tree-shake them. We should refactor the code to fix it.
  • (Lower priority) Replace enums with regular JavaScript objects where possible.

How to debug

(One time) Setup repository

  1. Clone this repository.
  2. Create the external folder in the root of the repository.
  3. Clone the ckeditor5-dev repository into the external folder.
  4. Open the external/ckeditor5-dev/packages/ckeditor5-dev-build-tools/src/config.ts file and add the experimentalLogSideEffects: true property to the object returned from the getRollupConfig function.
  5. Open the external/ckeditor5-dev/packages/ckeditor5-dev-build-tools/src/build.ts file and add preserveModules: true to the object passed to the build.write in the build function (not the generateUmdBuild function).
  6. Run yarn reinstall in the root of the ckeditor5 repository. This should install all dependencies and build the changes we made in the ckeditor5-dev repository.

(One time) Setup Verdaccio

  1. Install Verdaccio.
  2. Create .npmrc file and add registry=http://localhost:4873/ to it.
  3. Run npm adduser --registry http://localhost:4873/. Make sure to name the user ckeditor (this is required by one of our internal scripts).

(One time) Setup test repository

  1. Clone the new-installation-methods repository.
  2. In the vite directory create the .npmrc file and add registry=http://localhost:4873/ to it.
  3. Run npm install in the vite directory.
  4. Change the contents of vite/src/main.ts to:
    import 'ckeditor5';
    import 'ckeditor5-premium-features';

Test changes

In the ckeditor5 repository:

  1. Make the changes in the code.
  2. Stage the changes (git add .).
  3. Run npm run release:prepare-packages -- --nightly in the root of the ckeditor5 repository (you can comment out some of the steps in the scripts/release/preparepackages.js file to speed up the process).
  4. Run npm run release:publish-packages -- --nightly to publish the changes to Verdaccio.
  5. Revert all unstaged changes (git restore .).

In the new-installation-methods repository:

  1. Reinstall the dependencies in the vite directory.
  2. Run npm run build to see the changes in the bundle size.
  3. Run the following command and check the bundle size:
    npx esbuild ./src/main.ts \
      --bundle \
      --minify \
      --format=esm \
      --outfile=custom/bundle.js \
      --metafile=meta.json \
      --legal-comments=none \
      --target=es2022
  4. Upload the meta.json file to the esbuild analyzer for a more detailed analysis.
@filipsobol filipsobol added the type:task This issue reports a chore (non-production change) and other types of "todos". label Apr 26, 2024
filipsobol added a commit that referenced this issue May 8, 2024
Fix: Improve tree-shaking by prefixing mixin calls with `/* #__PURE__ */` magic comment. See #16292.
@filipsobol
Copy link
Member Author

filipsobol commented May 10, 2024

Important

This comment shows progress (from top to bottom) of how each change improved the JavaScript bundle size. The bundles are minified, but not gzipped.

Side-effects

Side-effect code
import 'ckeditor5';
import 'ckeditor5-premium-features';
Name Version Vite esbuild webpack
Baseline 0.0.0-nightly-20240426.0 1377 kB 1412 kB 1411 kB
#__PURE__ before mixins in core 0.0.0-nightly-20240509.0 1374 kB 1396 kB 1411 kB
Bumping target to ES2022 0.0.0-nightly-20240510.0 679 kB 1295 kB 1296 kB
#__PURE__ before mixins in commercial 0.0.0-nightly-20240515.0 685 kB 1081 kB 1159 kB
#__PURE__ for objects in global scope 0.0.0-nightly-20240516.0 615 kB 1076 kB 1159 kB
0.0.0-nightly-20240517.0 568 kB 1034 kB 1117 kB
Improve tree-shaking of CCSC 0.0.0-nightly-20240521.0 304 kB 440 kB 471 kB
Improve tree-shaking of markdown plugin Pending... 290 kB 439 kB 471 kB

Build

Build code
import {
    ClassicEditor,
    Essentials,
    CKFinderUploadAdapter,
    Autoformat,
    Bold,
    Italic,
    BlockQuote,
    CKBox,
    CKFinder,
    EasyImage,
    Heading,
    Image,
    ImageCaption,
    ImageStyle,
    ImageToolbar,
    ImageUpload,
    PictureEditing,
    Indent,
    Link,
    List,
    MediaEmbed,
    Paragraph,
    PasteFromOffice,
    Table,
    TableToolbar,
    TextTransformation,
    CloudServices,
    Mention
} from 'ckeditor5';

import { CaseChange, SlashCommand } from 'ckeditor5-premium-features';

import 'ckeditor5/index.css';
import 'ckeditor5-premium-features/index.css';

ClassicEditor.create( document.querySelector( '#editor' ) as HTMLElement, {
    plugins: [
  	  Essentials,
  	  CKFinderUploadAdapter,
  	  Autoformat,
  	  Bold,
  	  Italic,
  	  BlockQuote,
  	  CKBox,
  	  CKFinder,
  	  CloudServices,
  	  EasyImage,
  	  Heading,
  	  Image,
  	  ImageCaption,
  	  ImageStyle,
  	  ImageToolbar,
  	  ImageUpload,
  	  Indent,
  	  Link,
  	  List,
  	  MediaEmbed,
  	  Paragraph,
  	  PasteFromOffice,
  	  PictureEditing,
  	  Table,
  	  TableToolbar,
  	  TextTransformation,
  	  Mention,

  	  CaseChange,
  	  SlashCommand
    ],
    licenseKey: '<LICENSE_KEY>', // Replace this with your license key.
    toolbar: {
  	  items: [
  		  'undo', 'redo',
  		  '|', 'heading',
  		  '|', 'bold', 'italic',
  		  '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed',
  		  '|', 'bulletedList', 'numberedList', 'outdent', 'indent', 'caseChange'
  	  ]
    },
    image: {
  	  toolbar: [
  		  'imageStyle:inline',
  		  'imageStyle:block',
  		  'imageStyle:side',
  		  '|',
  		  'toggleImageCaption',
  		  'imageTextAlternative'
  	  ]
    },
    table: {
  	  contentToolbar: [
  		  'tableColumn',
  		  'tableRow',
  		  'mergeTableCells'
  	  ]
    },
    language: 'en'
} );
Name Version Vite esbuild webpack
Baseline 0.0.0-nightly-20240426.0 1819 kB 1779 kB 1793 B
#__PURE__ before mixins in core 0.0.0-nightly-20240509.0 1820 kB 1773 kB 1794 kB
Bumping target to ES2022 0.0.0-nightly-20240510.0 1537 kB 1715 kB 1734 kB
#__PURE__ before mixins in commercial 0.0.0-nightly-20240515.0 1556 kB 1608 kB 1693 kB
#__PURE__ for objects in global scope 0.0.0-nightly-20240516.0 1490 kB 1608 kB 1693 kB
0.0.0-nightly-20240517.0 1428 kB 1567 kB 1651 kB
Improve tree-shaking of CCSC 0.0.0-nightly-20240521.0 1187 kB 1094 kB 1260 kB
Improve tree-shaking of markdown plugin Pending... 1173 kB 1093 kB 1260 kB

filipsobol added a commit that referenced this issue May 15, 2024
Internal: Improve tree-shaking by adding `#__PURE__` comments before function calls in global scope. See #16292.
filipsobol added a commit that referenced this issue May 20, 2024
…cts-and-arrays-in-global-scope

Other: Add `#__PURE__` comments before objects and arrays in global scope. Related to #16292.
@filipsobol
Copy link
Member Author

This task is complete. I created a new one describing how we can further improve the bundle size.

#16395

@Witoso Witoso added the squad:core Issue to be handled by the Core team. label May 24, 2024
@Witoso Witoso added this to the upcoming milestone May 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
squad:core Issue to be handled by the Core team. type:task This issue reports a chore (non-production change) and other types of "todos".
Projects
None yet
Development

No branches or pull requests

3 participants