ckeditor / ckeditor5

Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.
https://ckeditor.com/ckeditor-5
Other
9.49k stars 3.7k forks source link

Improve tree-shaking of third-party dependencies #16395

Open filipsobol opened 5 months ago

filipsobol commented 5 months ago

This is a continuation of the task #16292, which you can refer to for more context and results of the initial work.

In it, we were able to improve tree shaking and reduce the number of side effects in ckeditor5 and ckeditor5-premium-features by about 80% by doing about 20% of the work (80/20 rule strikes again). This task focuses on the remaining work needed to make CKEditor bundles as small and performant as possible.

ckeditor5

The following code results in a bundle size of 197 Kib for Rollup/Vite and 365 KiB in esbuild:

import 'ckeditor5';

This is the treemap chart generated from the esbuild bundle:

Treemap chart showing how much each packages added to the bundle

It shows that 280 out of 364 KiB come from CKEditor code, with almost 99% coming from ckeditor5-engine and ckeditor5-utils, which are probably needed in all CKEditor projects anyway.

The remaining 85 KiB come from external dependencies:

The first three packages are also likely to be used in all CKEditor projects, so there's no point in optimizing them.

However, the same cannot be said for marked and turndown, as these dependencies are only needed when using the ckeditor5-markdown-gfm plugin. These two packages have side effects, so esbuild is not able to remove them from the bundle (Rollup can tree-shake turndown). We should look for ways to remove them from the bundle, either by contributing to them, forking them, or replacing them with alternatives that provide better tree-shaking.

ckeditor5-premium-features

The following code results in a bundle size of 64 KiB for Rollup/Vite and 74 KiB in esbuild (with all ckeditor5 packages marked as external):

import 'ckeditor5-premium-features';

This is the treemap chart generated from the esbuild bundle:

Treemap chart showing how much each packages added to the bundle

It shows that 64 out of 74 KiB come from external dependencies and only 10 KiB from CKEditor itself (which can be ignored). Of those 64 KiB, 36 KiB are from socket.io or its dependencies and 21 KiB are from protobufjs.

The problem with these two packages is that both are only needed when using RTC features, but are added to the bundle regardless of whether RTC is used or not. In addition, the protobufjs contains an eval which causes Rollup/Vite to log a warning when bundling, causes issues with Content Security Policy (CSP), and can raise security alerts in some automated tools. The size of the compiledmessages.js file generated by protobufjs it also an issue.

An alternative to protobufjs called protobuf-es claims that it is fully tree-shakeable and produces small bundle size, but since I have very little knowledge of protobuf and how we use it, I can only confirm the "fully tree-shakeable" claim, and can't tell if we can use it instead of protobufjs (treat this as an uninformed suggestion).

filipsobol commented 1 week ago

While lodash-es is not causing so many issues as some other libraries, we should consider replacing it. It hasn't received an update in the last ~4 years and because it still supports IE10 and IE11, it doesn't make use of modern JavaScript features, which affects both the bundle size and performance.

Fortunately, there's a modern alternative called es-toolkit. It has a smaller bundle size and better performance.

It comes in two “flavors” — es-toolkit and es-toolkit/compact. The former offers better performance and smaller bundle size, while the latter aims at providing full compatibility with lodash.

I checked the bundle size of all functions used by us that are supported by both libraries to see the bundle size difference, and these are the results:

Below is the list of all lodash functions used in our codebase and compatibility with es-toolkit.

Method Compatibility Additional comment
debounce
cloneDeep
isEqual
set
unset
throttle
isObject
omit
mapValues
isFunction
isPlainObject
isEqualWith
mergeWith
startCase
isString
escape
unescape 📝
clone 📝
escapeRegExp 📝
upperFirst 📝
map It's used only in one place, can be easily be worked around
identity It's used only in one place, can probably be worked around
isElement It can probably be replaced with this
cloneDeepWith
extend It's used in two places, can probably be worked around, renamed to assignIn in both lodash and es-toolkit

Compatibility explanation from es-toolkit website

The following emojis indicate the status of each feature:

✅: Completed (The function is fully implemented and has passed all tests with lodash test code.)
📝: In Review (The function is implemented but hasn't been tested with lodash test code yet.)
❌: Not Implemented (The function hasn't been implemented.)

Even if a feature is marked "in review," it might already be under review to ensure it matches lodash perfectly, and it could already offer the same functionality.