mdx-editor / editor

A rich text editor React component for markdown
https://mdxeditor.dev
MIT License
1.74k stars 139 forks source link

Bundle size large due to ToolbarComponents not being tree-shakeable? #10

Closed denyeo closed 1 year ago

denyeo commented 1 year ago

Thanks for the great new library! I'm trying it out and finding that it produces a very large bundle size (3.6 MB in dev, ~2 MB in prod build using Rollup via Vite), even though I've only passed a few toolbar components to MDXEditor.

It looks like it's because ToolbarComponents is a single exported object, which seems to prevent tree-shaking and causes all components to get included in the build. Or am I missing a simple solution?

I'm looking to use this for a simple commenting box, so I'd definitely like to excluded the unneeded FrontMatterEditor, TableEditor, etc. code and their dependencies like CodeMirror and Acorn.js.

Do you think it makes sense to export the components individually? E.g. export { BoldItalicUnderlineButtons, LinkButton, ... };

petyosi commented 1 year ago

Hey, thank you for giving the component a try!

The toolbar components are fairly simple UI widgets - they are not including the actual capabilities (i.e. the corresponding nodes and editors). Tree-shaking them would not bring the reduction you're looking for. It's also not that simple: even if the button is not present in the toolbar, there are multiple ways for a certain capability to be activated; the markdown string can include the structure, the user may type it in the source, or a shortcut can activate it.

Back to your question, which is a legit concern, I've done code splitting for the editors. This splits the contents of the bundle. My goal with this is to have the heavy external dependencies (the most important being CodeMirror) loaded on demand only if they are needed. However, I am not 100% sure if this would also cause the editor dependencies to be lazy loaded as well - it might also need adjustments in the final bundling process.

I'm going to look at that further, so I am leaving the issue open. To confirm, you're not doing something much fancier than the regular vite setup?

denyeo commented 1 year ago

Thanks for the detailed reply! Interesting. I think my Vite setup is pretty standard, but I'm using pnpm as well as Preact's compat replacement for React - maybe those influence the situation?

In the prod bundle's analysis (vite-bundle-visualizer), I can see that FrontMatterEditor, SandpackClient runtime, TableEditor, etc. are indeed split out. They aren't included in the large 3.5MB main bundle. The 3.5MB bundle contains things like Acorn.js (required by micromark-extension-mdxjs) and sandpack-react (required by @mdxeditor/editor itself). So it looks like the core editor imports some of these dependencies.

I think there are hints in the dev build: Chrome's Network tool reports that the largest chunk chunk-ANTATEO7.js (3.6MB) is downloaded by @mdxeditor_editor.js in this line:

import {
  $createAdmonitionNode,
  $createCodeBlockNode,
  $createFrontmatterNode,
  $createImageNode,
  $createJsxNode,
  $createSandpackNode,
  $createTableNode,
  $isAdmonitionNode,
  $isCodeBlockNode,
  $isFrontmatterNode,
  $isImageNode,
  $isJsxNode,
  $isSandpackNode,
  $isTableNode,
  AdmonitionNode,
  CodeBlockNode,
  FrontmatterNode,
  ImageNode,
  JsxNode,
  MDXEditor,
  SandpackNode,
  TableNode,
  ToolbarComponents,
  defaultMdxOptionValues,
  require_Lexical,
  require_LexicalCode,
  require_LexicalComposer,
  require_LexicalComposerContext,
  require_LexicalContentEditable,
  require_LexicalErrorBoundary,
  require_LexicalHistoryPlugin,
  require_LexicalHorizontalRuleNode,
  require_LexicalHorizontalRulePlugin,
  require_LexicalLink,
  require_LexicalLinkPlugin,
  require_LexicalList,
  require_LexicalListPlugin,
  require_LexicalMarkdown,
  require_LexicalMarkdownShortcutPlugin,
  require_LexicalRichText,
  require_LexicalRichTextPlugin,
  require_LexicalSelection,
  require_LexicalTabIndentationPlugin,
  require_LexicalUtils,
  require_classnames,
  require_unidiff
} from "/node_modules/.vite/deps/chunk-ANTATEO7.js?v=f6fcb197";

So maybe some of these Nodes or MDXEditor itself are automatically including some dependencies even though the Table/Code/Sandpack editors aren't loaded yet.

the markdown string can include the structure, the user may type it in the source, or a shortcut can activate it.

Maybe these activations should only occur if the Editor has been configured to have the relevant extension enabled? Of course, easier said than done (if the existing structure has these listeners/events coupled). I guess this might be why other rich text editors have the dev import the desired extensions/plugins explicitly and statically, e.g. import insertTableCommand, BlockquoteElement, etc.

If it helps, happy to jump on a chat to share further details/data e.g. the Vite bundle analysis. It'd be great to have a slim bundle for simple use cases!

petyosi commented 1 year ago

That's a good hint. Looks like even though the components are loaded on demand, some of the dependencies are referred eagerly.

I will explore this further. Enabling MDX on demand is a good idea, as you might not need it.

petyosi commented 1 year ago

I dug into the bundle analysis to see what can be optimized, there are several low-hanging (and some relatively easy) things to be done.

To touch on the subject of tree-shaking: I love the concept, and at some point, I will probably do something to take advantage of it, but first I would love to see how much can be done with code splitting. The main concern with tree shaking (like you pointed out) is that to take advantage of it, An explicit plugin/part assembly step is necessary and offloaded to the integrator. This creates some friction and can be confusing for newcomers.

Happy to learn more about your use case - what other kind of customization do you see necessary? I would assume that the diff/source view tool is not necessary for a comment box.

denyeo commented 1 year ago

Having tried several editors already, I personally think the best balance could be to provide a boilerplate upfront in the Getting Started docs:

import FooEditor from 'foo-editor'
import { gfm, boldItalicUnderline, blockquote, ... } from 'foo-editor/plugins'

// Pick your plugins; any config/styling can be passed in via the calls in future
const plugins = [ gfm(), boldItalicUnderline(), blockquote(), linebreaks(), ... ]

function Editor() {
    return (<FooEditor plugins={ plugins }>)
}

This is close to what ByteMD does, giving the core functionality out of the box mostly fuss-free while still giving customisability, and I think it's great. Other libraries make setup much more complicated. If you want to simplify even further, you could even offer presets (plugin sets):

import { FooEditor, gfmPresetPlugins as plugins } from 'foo-editor'

// To add on Sandpack, TableEditor, etc.: plugins.push(codeEditor(), tableEditor(), ...) 
return (<FooEditor plugins={ plugins }>)

For my use case, I need GFM with some typical buttons (formatting, blockquote, links, lists, hr, headings/block type). I was experimenting with ByteMD to achieve this but ran into an infinite render loop with its React component.

Hope this is helpful!

petyosi commented 1 year ago

@denyeo I reworked the component to a plugin architecture. To ensure that the bundle does not get extra calories, the package now provides multiple entry points.

Check the updated documentation section. Surprisingly, the syntax I ended up with looks a lot like the example you provided.

Happy to get your feedback, thank you beforehand!