Shrinking Mermaid >30%.
Mermaid
Mermaid is a markdown based diagramming tool. It is a javascript library that can be used to generate diagrams from text, similar to Markdown.
I’ve been a maintainer of the mermaid project for a while now. Mostly focused on the live editor, mermaid.live .
Let’s see how far we can shrink mermaid from its current 2.05 MiB size.
Types of bundles in Mermaid
Standalone bundle - mermaid.js
This is usually imported directly by static sites via a script
tag, and contains all dependencies required to render mermaid diagrams inside it. This is the largest bundle, with 0 external dependencies.
mermaid.esm.mjs
is the ESM version of mermaid, which is the recommended version to use in modern browsers.
mermaid.min.js
and mermaid.esm.min.mjs
are minified versions of these bundles.
Mermaid Core bundle - mermaid.core.mjs
This contains only the core mermaid code and is supposed to be used with bundlers like Vite/Webpack/etc when including mermaid as a dependency in another project. There is no minified build as it is expected that the bundler will minify the code.
Package sizes
Bundle | Size | gzip Size |
---|---|---|
mermaid.core.mjs | 1080.73 KiB | 217.63 KiB |
mermaid.esm.mjs | 2165.83 KiB | 455.45 KiB |
mermaid.esm.min.mjs | 1554.08 KiB | 392.58 KiB |
mermaid.js | 2281.14 KiB | 460.57 KiB |
mermaid.min.js | 1148.52 KiB | 347.79 KiB |
Composition of bundles
Now we need to know what each bundle contains and how they are composed. Only then can we figure out how we can reduce the bundle size.
A quick google search landed me on rollup-plugin-visualizer , which is a rollup plugin that generates a visual representation of the bundle.
This PR adds the visualizer plugin to the mermaid repo and generates 3 types of graphs.
Treemap & Sunburst are useful to see the space utilization. Network shows us the dependency graph. It’s a bit messy, so you might need to play around with the regex to clean some stuff out.
Opening the diagrams in new tab will give a better view.
mermaid.js
Treemap Diagram | Full screen
Network Diagram | Full screen
Sunburst Diagram | Full screen
mermaid.core.mjs
Treemap Diagram | Full screen
Network Diagram | Full screen
Sunburst Diagram | Full screen
You can see that the core build contains just the core internal components. (There are some extra dependencies added, I’m not really sure why vite included it, but it’s a very small fraction (7.8%), compared to 58.8% of unified build)
Shrinking the bundle
Lodash
Lodash is a very popular utility library, and is used by mermaid to perform some operations. It is a very large library, and is used in a very small fraction of the codebase.
We could replace it with custom code, but it might not help much as lodash
is already a sub-dependency of dagre
and dagre-d3.
But is there something easier?
The tree-map tells us that it is ~28% of our bundle size. That’s huge for something that’s only used in 2 places & 3 test files.
Turns out, yes.
The network graph shows that the biggest lodash
mport is from mermaidAPI.ts
.
And when looking at the import, it’s
import { isEmpty } from "lodash";
Whereas everywhere else, it’s
import memoize from "lodash/memoize";
So, let’s try changing that import.
- import { isEmpty } from "lodash";
+ import isEmpty from "lodash/isEmpty";
Build… And… Voilà!
Treemap after Lodash fix | Full screen
Network after Lodash fix | Full screen
Sunburst after Lodash fix | Full screen
Bundle | Initial size (KiB) | After fix | Change | % Change |
---|---|---|---|---|
mermaid.core.mjs | 1080.32 | 1080.32 | 0 | 0.00 |
mermaid.esm.mjs | 2165.83 | 1951.35 | -214.48 | -9.90 |
mermaid.esm.min.mjs | 1554.08 | 1429.69 | -124.39 | -8.00 |
mermaid.js | 2281.14 | 2055.7 | -225.44 | -9.88 |
mermaid.min.js | 1148.52 | 1071.37 | -77.15 | -6.72 |
And we can see that it has reduced the bundle size by 9.9% (214.48 KiB) for the ESM build, and 9.88% (225.44 KiB) for the UMD build. There is no change in core build, as it doesn’t contain any external dependencies.
Bundle | Initial gzip size | After fix | Change | % Change |
---|---|---|---|---|
mermaid.core.mjs | 218.15 | 218.15 | 0 | 0.00 |
mermaid.esm.mjs | 455.45 | 414.91 | -40.54 | -8.90 |
mermaid.esm.min.mjs | 392.58 | 361.61 | -30.97 | -7.89 |
mermaid.js | 460.57 | 419.65 | -40.92 | -8.88 |
mermaid.min.js | 347.79 | 320.45 | -27.34 | -7.86 |
The gzip and minified size changes are inline with the bundle size changes, so I won’t be showing them for the rest of the post. The ESM and UMD bundle size changes are also inline with each other, so I’ll only be showing the mermaid.js
bundle size changes.
Bundle | Initial (KiB) | After fix | Change | % Change |
---|---|---|---|---|
mermaid.core.mjs | 1080.32 | 1080.32 | 0 | 0.00 |
mermaid.js | 2281.14 | 2055.7 | -225.44 | -9.88 |
So just one line change reduced the bundle size by 9.88% (225.44 KiB). Awesome!
I tried using the lodash-es
package, which is a ES module version of lodash, but it didn’t help much. It actually increased the size of the bundle by 30 KiB. So let’s stick with the lodash
package.
Bundle | lodash | lodash-es | Change | % Change |
---|---|---|---|---|
mermaid.core.mjs | 1080.32 | 1104.15 | +23.83 | +2.22 |
mermaid.js | 2055.7 | 2085.77 | +30.07 | +1.46 |
But, if we were using the old import { isEmpty } from "lodash-es";
syntax, it would have reduced the bundle size without us having to change all imports to lodash/isEmpty
, etc.
The sunburst diagram is a great tool to see which are the biggest components of the bundle. We can see that lodash
s now 10.08% of the bundle size, and is still the biggest dependency.
Dagre & Dagre-D3
As these libraries were unmaintained, Alois Klink
raised this PR
to replace them with dagre-d3-es
, which shaved off another 216KiB from the mermaid.js
bundle.
Bundle | Initial | dagre-d3-es | Change | % Change |
---|---|---|---|---|
mermaid.js | 2281.14 | 1839.41 | -441.73 | -19.36 |
mermaid.core.mjs | 1080.32 | 1313.80 | +233.48 | +21.61 |
But there’s something interesting here, the core build has gone up 21% in size. Let’s dig in.
Treemap after adding dagre-d3-es | Full screen
Network after adding dagre-d3-es | Full screen
Sunburst after adding dagre-d3-es | Full screen
So we can see that the savings came from unused stuff in dagre-d3
being tree shaken off, but a humongous lodash-es dependency was added. lodash-es
was supposed to be tree-shakeable and the solution to the problem, but it seems like it’s not. So let’s try to fix it.
Looking at the source, we see the following import.
import _ from "lodash-es";
From a blog I read earlier (don’t remember which), I knew that this was a bad practice, and that we should import the specific functions we need, instead of the whole library. Or, we should do the following to enable tree-shaking.
import * as _ from "lodash-es";
As it’s an external dependency, we’ll have to raise a PR in the dagre-d3-es
repo. But to validate the fix, we have to
- Fork it
- Build locally
- Link the local build it in pnpm
- Link it to mermaid
- Build mermaid
Or…
- Do a find & replace in
node_modules > dagre-d3-es
- Build mermaid
- import _ from 'lodash-es';
+ import * as _ from 'lodash-es';
Guess which one I did? 😅
Bundle | Initial | After dagre-es |
After fix | Change | % Change |
---|---|---|---|---|---|
mermaid.js | 2281.14 | 1839.41 | 1694.35 | -586.79 | -25.72 |
mermaid.core.mjs | 1080.32 | 1313.80 | 1178.73 | +98.41 | +9.11 |
So, just by changing the import syntax, the bundle size shrank 7.88% (145 KiB).
I raised a PR
in the dagre-es
repo, and it was merged.
But the core is still up by 9.11%. What could it be?
Treemap after fixing dagre-d3-es | Full screen
Network after fixing dagre-d3-es | Full screen
Sunburst after fixing dagre-d3-es | Full screen
Hmm… lodash
and lodash-es
… 🤔
Remember when I said lodash
is a sub-dependency, well if you look at the packages, dagre
& dagre-d3
were the ones importing lodash
. Now that they’re gone, lodash
is a direct, unnecessary dependency of mermaid. Let’s replace it with lodash-es
, what dagre-d3-es
is using.
Bundle | Initial | dagre-es |
Fix import | lodash-es |
Change | % Change |
---|---|---|---|---|---|---|
mermaid.js | 2281 | 1839 | 1694 | 1707 | -574 | -25.17 |
core.mjs | 1080 | 1313 | 1178 | 1105 | +25 | +2.36 |
So, core went from +9.11% to +2.36%. But mermaid.js
went from -17.58% to -16.94%. What’s going on?
Treemap after replacing lodash with lodash-es | Full screen
Network after replacing lodash with lodash-es | Full screen
Sunburst after replacing lodash with lodash-es | Full screen
There is still 2 lodash! Ughh…
The network graph is supposed to be used to find dependencies, but I wasn’t able to find who is importing lodash. So I just randomly moved the cursor inside lodash
to see if there’s any “imported by” that shows up. And there was!
Alois did mention in a comment
that dagre-d3-es
has a graphlib
implementation. So let’s see if we can use that.
- import graphlib from 'graphlib';
+ import * as graphlib from 'dagre-d3-es/src/graphlib';
+ import * as graphlibJson from 'dagre-d3-es/src/graphlib/json';
...
- graphlib.json.write(graph);
+ graphlibJson.write(graph);
After making the above changes, we can see that the bundle size has gone down again by 8.4% (144 KiB). core
is still up 2%, not great, not terrible.
Bundle | Initial | lodash-es |
graphlib |
Change | % Change |
---|---|---|---|---|---|
mermaid.js | 2281 | 1707 | 1563 | -718.14 | -31.48 |
core.mjs | 1080 | 1105 | 1106 | +26 | +2.38 |
Treemap after fixing graphlib | Full screen
Network after fixing graphlib | Full screen
Sunburst after fixing graphlib | Full screen
Trimming mermaid.core.mjs
There were some external dependencies being added in mermaid.core.mjs
, which was confusing as the core build is supposed to be free of external dependencies. Alois figured out why and even had a solution for it.
It looks like the issue we have is that vite is ignoring all of our dependencies for the mermaid.core.* builds, but it’s not ignoring the dependencies of our dependencies. This could be fixed with something like https://www.npmjs.com/package/rollup-plugin-node-externals maybe (I think rollup plugins usually work in Vite at least)
– Alois
Unfortunately, this plugin wasn’t working with vite. Digging into the source, I got the regex they were using to filter stuff out. Adding it directly into the build script worked.
- external.push(...Object.keys(dependencies));
+ external.push(new RegExp('^(?:' + Object.keys(dependencies).join('|') + ')(?:/.+)?$'));
Bundle | Initial | Final | Change | % Change |
---|---|---|---|---|
mermaid.js | 2281.14 | 1563.71 | -718.14 | -31.48 |
core.mjs | 1080.73 | 1025.56 | -54.76 | -5.07 |
Finally, core is clean!
Treemap after fixing core build | Full screen
Network after fixing core build | Full screen
Sunburst after fixing core build | Full screen
Conclusion
So, we started with a 2.28 MiB bundle, and ended up with a 1.56 MiB bundle. That’s a 31.48% reduction in bundle size. Not bad for a day’s work.
The load time difference on 4G connection preset.
I’ll cover how we added lazy loading to mermaid to further reduce the bundle size in a future post.
Special thanks to Alois , Thibaut (Teebo) , Denis Bardadym and all the creators of the tools used in this post.