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

How new installation methods can replace DLLs #15739

Open
filipsobol opened this issue Jan 23, 2024 · 0 comments
Open

How new installation methods can replace DLLs #15739

filipsobol opened this issue Jan 23, 2024 · 0 comments
Labels
type:improvement This issue reports a possible enhancement of an existing feature.

Comments

@filipsobol
Copy link
Member

This document is a follow-up to the RFC for new installation methods, which contains the context necessary to understand the changes we discuss here, so read it first if you haven’t already.

While DLLs are only one of the installation methods we currently support, we've decided to write a separate document for them because they are the most unique and distinct installation method, and they are often used in CMS platforms with large ecosystems of their own.

In this document, we will explain why we introduced DLLs in the first place, what problem they solved, what challenges they introduced, and how the new installation methods might solve them.

Why do we have a DLL setup❓

Before the DLLs, we offered two main installation methods: predefined builds and custom builds.

The predefined builds are pre-generated JavaScript files that allow developers to avoid the build step and run CKEditor directly in the browser. To keep a good balance between bundle size and feature completeness, we decided to include only a selected set of features in them. However, since it's not possible to dynamically add more features to an editor bundle, this didn't suit those who wanted more features than the builds originally shipped with.

On the other hand, custom builds give full control over the features added to the bundle, but at the cost of introducing a build step. Unfortunately, this can be difficult or impossible to do, depending on the type of project, development environment, and corporate policies.

We needed to give developers another way to install CKEditor that didn't require the build step, while still giving them control over the editor's features.

That's why we introduced DLLs. In simple terms, webpack's DllPlugin allows you to split a large codebase into smaller, dynamically linked bundles. In our setup, a base bundle contains the engine and all the core features of the editor, while the additional individual editor features are separated into many smaller bundles. Since these bundles are pre-generated and the developers decide which bundles to load, it offered the benefits of both the predefined builds and the custom builds.

Is this the happy ending? Unfortunately, no.

DLLs drawbacks 💔

If DLLs offer the benefits of both installation methods, then what's the problem? Unfortunately, DLLs have some significant drawbacks and limitations. Let's discuss some of them.

Bundler lock-in

The CKEditor 5 repository was created nearly 10 years ago. Since then, the front-end development tools have grown and matured a lot. We, and many developers in our ecosystem, want to use these great tools to improve our workflow, development speed, and overall experience, but also to make contributing easier and more welcoming.

Since DLLs are webpack-specific, we and any downstream project that uses DLLs to create custom CKEditor plugins must use webpack. While this was not a problem a few years ago when it was the de facto standard, now every major frontend framework has migrated to Vite or Turbopack. This means that some developers already have projects with powerful bundlers, but have to set up webpack on the side just because they use CKEditor. Can you imagine if all your dependencies had requirements like this?

Being forced to use a specific bundler is not only inconvenient from a development perspective, but also dangerous from a business perspective. Webpack's development has slowed down recently, probably due to the growing popularity of other tools and the introduction of Rspack. Imagine the position it would put us and our community in if it came to a complete halt.

We cannot allow such lock-in if the project is to be maintained for another 10 years and beyond. We need to make sure that we don't use bundler-specific features, so that when other tools dominate the scene in a few years, our customers and community can migrate to them without worrying about CKEditor.

One feature ≥ one file

Let's see how we can use DLLs to create an editor with some commonly used features.

<div id="editor">Lorem ipsum...</div>

<script src="/dlls/ckeditor5-dll.js"></script>
<script src="/dlls/editor-classic.js"></script>
<script src="/dlls/essentials.js"></script>
<script src="/dlls/adapter-ckfinder.js"></script>
<script src="/dlls/autoformat.js"></script>
<script src="/dlls/basic-styles.js"></script>
<script src="/dlls/block-quote.js"></script>
<script src="/dlls/ckfinder.js"></script>
<script src="/dlls/easy-image.js"></script>
<script src="/dlls/heading.js"></script>
<script src="/dlls/image.js"></script>
<script src="/dlls/indent.js"></script>
<script src="/dlls/link.js"></script>
<script src="/dlls/list.js"></script>
<script src="/dlls/media-embed.js"></script>
<script src="/dlls/paste-from-office.js"></script>
<script src="/dlls/table.js"></script>
<script src="/dlls/cloud-services.js"></script>
<script src="/dlls/html-embed.js"></script>

<script>
CKEditor5.editorClassic.ClassicEditor.create( document.querySelector( '#editor' ), {
  plugins: [
    CKEditor5.essentials.Essentials,
    CKEditor5.autoformat.Autoformat,
    CKEditor5.basicStyles.Bold,
    CKEditor5.basicStyles.Italic,
    CKEditor5.basicStyles.Underline,
    CKEditor5.basicStyles.Code,
    CKEditor5.blockQuote.BlockQuote,
    CKEditor5.cloudServices.CloudServices,
    CKEditor5.heading.Heading,
    CKEditor5.image.Image,
    CKEditor5.image.ImageCaption,
    CKEditor5.image.ImageStyle,
    CKEditor5.image.ImageToolbar,
    CKEditor5.image.ImageUpload,
    CKEditor5.indent.Indent,
    CKEditor5.link.Link,
    CKEditor5.list.List,
    CKEditor5.mediaEmbed.MediaEmbed,
    CKEditor5.pasteFromOffice.PasteFromOffice,
    CKEditor5.table.Table,
    CKEditor5.table.TableCaption,
    CKEditor5.table.TableProperties,
    CKEditor5.table.TableCellProperties,
    CKEditor5.table.TableToolbar,
    CKEditor5.typing.TextTransformation,
    CKEditor5.upload.Base64UploadAdapter,
    CKEditor5.htmlEmbed.HtmlEmbed
  ]
} );
</script>

Okay, not bad. We made 19 requests for JavaScript files that weigh about 348kB. Now, since I'm Polish, let's add Polish translations.

<div id="editor">Lorem ipsum...</div>

<script src="/dlls/ckeditor5-dll.js"></script>
<script src="/dlls/editor-classic.js"></script>
<script src="/dlls/essentials.js"></script>
<script src="/dlls/adapter-ckfinder.js"></script>
<script src="/dlls/autoformat.js"></script>
<script src="/dlls/basic-styles.js"></script>
<script src="/dlls/block-quote.js"></script>
<script src="/dlls/ckfinder.js"></script>
<script src="/dlls/easy-image.js"></script>
<script src="/dlls/heading.js"></script>
<script src="/dlls/image.js"></script>
<script src="/dlls/indent.js"></script>
<script src="/dlls/link.js"></script>
<script src="/dlls/list.js"></script>
<script src="/dlls/media-embed.js"></script>
<script src="/dlls/paste-from-office.js"></script>
<script src="/dlls/table.js"></script>
<script src="/dlls/cloud-services.js"></script>
<script src="/dlls/html-embed.js"></script>
+ <script src="/dlls/ckeditor5-dll-pl.js"></script>
+ <script src="/dlls/basic-styles-pl.js"></script>
+ <script src="/dlls/block-quote-pl.js"></script>
+ <script src="/dlls/heading-pl.js"></script>
+ <script src="/dlls/image-pl.js"></script>
+ <script src="/dlls/indent-pl.js"></script>
+ <script src="/dlls/link-pl.js"></script>
+ <script src="/dlls/list-pl.js"></script>
+ <script src="/dlls/media-embed-pl.js"></script>
+ <script src="/dlls/table-pl.js"></script>
+ <script src="/dlls/html-embed-pl.js"></script>

<script>
CKEditor5.editorClassic.ClassicEditor.create( document.querySelector( '#editor' ), {
  plugins: [
    CKEditor5.essentials.Essentials,
    CKEditor5.autoformat.Autoformat,
    CKEditor5.basicStyles.Bold,
    CKEditor5.basicStyles.Italic,
    CKEditor5.basicStyles.Underline,
    CKEditor5.basicStyles.Code,
    CKEditor5.blockQuote.BlockQuote,
    CKEditor5.cloudServices.CloudServices,
    CKEditor5.heading.Heading,
    CKEditor5.image.Image,
    CKEditor5.image.ImageCaption,
    CKEditor5.image.ImageStyle,
    CKEditor5.image.ImageToolbar,
    CKEditor5.image.ImageUpload,
    CKEditor5.indent.Indent,
    CKEditor5.link.Link,
    CKEditor5.list.List,
    CKEditor5.mediaEmbed.MediaEmbed,
    CKEditor5.pasteFromOffice.PasteFromOffice,
    CKEditor5.table.Table,
    CKEditor5.table.TableCaption,
    CKEditor5.table.TableProperties,
    CKEditor5.table.TableCellProperties,
    CKEditor5.table.TableToolbar,
    CKEditor5.typing.TextTransformation,
    CKEditor5.upload.Base64UploadAdapter,
    CKEditor5.htmlEmbed.HtmlEmbed
  ],
+    language: 'pl'
} );
</script>

The weight hasn't changed much, it went up to 355kB. But we're now making 30 requests, and that's just the editor. What about the rest of the application? How many more requests would the browser have to make if I add more features or create custom ones?

This does not scale well unless you start bundling these files. However, we want the editor to be performant by default, regardless of the number of features used.

Development requirements

We support all installation methods, create all builds, and run our internal development environment from a single source. Ideally, every tool we use should work from this source without directly affecting it, which is the case for everything except DLLs.

DLLs enforce a certain project structure, and they affect how we and authors of custom plugins write our code. We also have to maintain several eslint rules just to make sure we or plugin authors don't accidentally break DLLs. In addition, we have too often said, "Oh... that won't work with DLLs".

All of this discourages and makes it harder for the community to contribute to our codebase or create and release custom plugins. It also affects our development experience, and therefore everyone's experience with CKEditor, as we have to limit ourselves or find workarounds instead of working on new features or fixing bugs.

New “browser build” to the rescue? 🛟

Now that we are on the same page about the challenges posed by DLLs, we can start talking about how new installation methods can help solve them.

As we explained in the RFC for new installation methods, we will gradually replace the current installation methods with NPM and browser builds. In this document, we'll focus on the latter because it's more similar to DLLs.

The new browser build consists of a single JavaScript file containing all of our open-source code, one CSS file and optional translations. This setup makes heavy use of a fairly new browser feature called “import maps”, which allows you to map a specifier to a file and use that specifier in the imports everywhere on the page.

Let's see how it works. First, we need to define an import map, like this:

<script type="importmap">
{
  "imports": {
    "ckeditor5": "/assets/ckeditor.browser.js",
    "ckeditor5/": "/assets/"
  }
}
</script>

The entries we’ve registered in the imports object tell the browser which files to load when we use the ckeditor5 or ckeditor5/* specifiers.

Now in the JavaScript code, we can use these specifiers instead of the file paths, like this:

<script type="module">
import { ClassicEditor } from 'ckeditor5'; // Mapped to "/assets/ckeditor.browser.js"
import translations from 'ckeditor5/languages/pl.js'; // Mapped to "/assets/languages/pl.js"
</script>

This is very similar to how you reference a dependency from node_modules, instead of using a file path like ../../../node_modules/@ckeditor/ckeditor5-core/src/index.js, you use easy-to-remember specifier like @ckeditor/ckeditor5-core.

Benefits of DLLs without bundler lock-in

The new browser build has both of the main advantages of DLLs. By default, it doesn't require a build step, and it gives you the flexibility to choose which editor features you use, without locking you into using webpack.

Let's see what the above setup looks like with the new browser build.

<link rel="stylesheet" href="/assets/ckeditor.css">

<div id="editor">Lorem ipsum...</div>

<script type="importmap">
{
  "imports": {
    "ckeditor5": "/assets/ckeditor.browser.js",
    "ckeditor5/": "/assets/"
  }
}
</script>
<script type="module">
import {
  ClassicEditor,
  Essentials,
  Autoformat,
  Bold,
  Italic,
  Underline,
  Code,
  BlockQuote,
  CloudServices,
  Heading,
  Image,
  ImageCaption,
  ImageStyle,
  ImageToolbar,
  ImageUpload,
  Indent,
  Link,
  List,
  MediaEmbed,
  PasteFromOffice,
  Table,
  TableCaption,
  TableProperties,
  TableCellProperties,
  TableToolbar,
  TextTransformation,
  Base64UploadAdapter,
  HtmlEmbed
} from 'ckeditor5';
import translations from 'ckeditor5/languages/pl.js';

ClassicEditor.create( document.querySelector( '#editor' ), {
  plugins: [
    Essentials,
    Autoformat,
    Bold,
    Italic,
    Underline,
    Code,
    BlockQuote,
    CloudServices,
    Heading,
    Image,
    ImageCaption,
    ImageStyle,
    ImageToolbar,
    ImageUpload,
    Indent,
    Link,
    List,
    MediaEmbed,
    PasteFromOffice,
    Table,
    TableCaption,
    TableProperties,
    TableCellProperties,
    TableToolbar,
    TextTransformation,
    Base64UploadAdapter,
    HtmlEmbed
  ],
  translations
} );
</script>

If, for whatever reason, you like the CKEditor5 global object from the DLLs, you can create a similar object using import * as syntax like this:

<link rel="stylesheet" href="/assets/ckeditor.css">

<div id="editor">Lorem ipsum...</div>

<script type="importmap">
{
  "imports": {
    "ckeditor5": "/assets/ckeditor.browser.js",
    "ckeditor5/": "/assets/"
  }
}
</script>
<script type="module">
import * as CKEditor5 from 'ckeditor5';
import translations from 'ckeditor5/languages/pl.js';

CKEditor5.ClassicEditor.create( document.querySelector( '#editor' ), {
  plugins: [
    CKEditor5.Essentials,
    CKEditor5.Autoformat,
    CKEditor5.Bold,
    CKEditor5.Italic,
    CKEditor5.Underline,
    CKEditor5.Code,
    CKEditor5.BlockQuote,
    CKEditor5.CloudServices,
    CKEditor5.Heading,
    CKEditor5.Image,
    CKEditor5.ImageCaption,
    CKEditor5.ImageStyle,
    CKEditor5.ImageToolbar,
    CKEditor5.ImageUpload,
    CKEditor5.Indent,
    CKEditor5.Link,
    CKEditor5.List,
    CKEditor5.MediaEmbed,
    CKEditor5.PasteFromOffice,
    CKEditor5.Table,
    CKEditor5.TableCaption,
    CKEditor5.TableProperties,
    CKEditor5.TableCellProperties,
    CKEditor5.TableToolbar,
    CKEditor5.TextTransformation,
    CKEditor5.Base64UploadAdapter,
    CKEditor5.HtmlEmbed
  ],
  translations
} );
</script>

As you can see, this approach allows you to import and register any plugin you want, giving you full control over the editor's features. All without the build step.

Custom plugins without bundling

The fact that all the JavaScript code is bundled into a single file and accessible via a unique specifier has another massive benefit. It allows us to run custom CKEditor plugins directly in the browser, without the build process.

We showed this in action in one of our demos, where we import everything we need from the ckeditor5 bundle, write a custom Highlight plugin and register it in the editor (editor code, custom plugin).

Fewer requests

Thanks to bundling, the browser will only have to make up to 3 requests to load the editor, instead of the 30 requests it makes today - one request for CSS, one for JavaScript, and optionally one for translations. We will also offer a similar bundle for the commercial features, which together will result in 6 requests to access everything we offer, instead of the 131 requests your browser would have to make today.

Each custom plugin you create will add up to three more requests, but if you have a lot of them, you are free to bundle them together as we did, resulting in a total of 9 requests.

No bundler lock-in

If, after reading the previous section, you still think 9 requests is too many, you can go as low as two requests: one request for all plugins combined with translations (if you know in advance which translation you want to use), and one request for CSS. We will create a new guide showing how to do this, but it will come at the cost of introducing a build step.

However, unlike now, you'll be free to choose any bundler you want, because the code we'll distribute will be plain JavaScript and CSS. During our initial research, we tested the new bundles with webpack, Rollup, esbuild, and Vite. All of them successfully generated bundles without any custom plugins or hacks.

Browser build drawbacks ⚖️

As with everything, the new browser builds are not without their drawbacks.

Bundle size

The first drawback is the size of the download. If we bundle everything together, then everything has to be downloaded.

This is true, but it's worth noting that most of the weight comes from the editor engine and core plugins that cannot be removed. Also, the heaviest plugins are the ones that most projects use anyway, such as tables or lists.

The total download size of the DLLs in the example above is 356kB gzipped. The same example using the new browser build is 371kB gzipped, so 15kB gzipped more.

How does that affect performance? You might be surprised.

Although we are downloading 15kB more, we are only making 3 requests instead of 30. Despite using HTTP/2, which is not limited to up to 6 simultaneous requests like HTTP/1.1, our local tests (which should always be taken with a large grain of salt) show much better results for the new browser build. Total request time dropped from 69ms to 44ms and DOMContentLoaded from 254ms to 170ms.

We're aware that some of you may want to reduce the bundle size as much as possible. This can be done by creating a custom browser build, which requires a build step. However, we are considering a way to generate a personalized CDN URL with only the code you need and everything else removed.

Import map support

The second drawback is the support for import maps. While all major browsers support it, and global support is at 91-92%, Safari only added support in March 2023. This means that a significant number of Safari users are running versions without import maps support, but that number is slowly decreasing.

If you're supporting users with an older version of Safari, you'll need to use a polyfill that adds import maps support for those browsers.

Migration 🛣️

The DLLs will not be removed immediately, but will go into a deprecation period starting with the release that introduces new installation methods. We are still discussing the exact dates internally, but we do know that DLLs will have a longer deprecation window than other current installation methods.

During this deprecation window, the DLLs will be part of the normal release cycle and will still receive updates, which we hope will help everyone make the jump to using the new methods. What might that jump look like?

Steps

If you only use the official open-source and commercial packages and don't maintain any custom plugins, you'll need to:

  1. Remove all CKEditor dependencies except for the ckeditor5 package.
  2. Register an import map as shown in the examples above and access the editor code from the ckeditor5 bundle instead of relying on the global CKEditor5 object.
  3. Add CSS import.

Please note that the second step requires using <script type="module"> which behaves differently from <script>.

Custom plugins

For those of you using the package generator to create custom plugins, we will introduce a new command that will build the package for the new setup. The command will output a dist folder containing a JavaScript bundle, and optionally separate CSS and translation files.

This will likely not require any changes to the source code and will not conflict with other generated bundles, meaning that you can release new versions that are compatible with the current and new installations. This should help with a gradual migration and help avoid having to make a "big jump" where you migrate everything all at once.

Feedback 💬

Let us know if you like the proposed changes, if you have questions or doubts, or just react with 👍 or 👎 below the RFC so we know if we are on the right track.

If you think we've missed something, or have a question about your specific setup, feel free to comment below.

@filipsobol filipsobol added the type:improvement This issue reports a possible enhancement of an existing feature. label Jan 23, 2024
@filipsobol filipsobol pinned this issue Jan 23, 2024
@filipsobol filipsobol changed the title [RFC] How new installation methods can replace DLLs How new installation methods can replace DLLs Mar 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:improvement This issue reports a possible enhancement of an existing feature.
Projects
None yet
Development

No branches or pull requests

1 participant