Performance & Optimization¶
This guide covers bundling, tree-shaking, and strategies for keeping the editor fast with large documents.
Bundle Size¶
The default sciflow-editor.js bundle includes all features, the ProseMirror runtime, and the Lit web component layer.
| Asset | Raw | Gzipped |
|---|---|---|
sciflow-editor.js |
~677 KB | ~170 KB |
sciflow-editor.css |
~29 KB | ~5.6 KB |
| MathJax 4 (external) | — | ~250 KB (loaded on demand) |
Measuring Bundle Size¶
The project uses size-limit to enforce budgets and rollup-plugin-visualizer for interactive treemaps.
# Check bundle sizes against budgets
npm run size
# Generate interactive treemap (opens stats.html)
npm run visualize
Size limits are configured in .size-limit.json and enforced during npm run prepublish:check. If a change pushes the bundle over budget, the check fails.
Reducing Bundle Size¶
1. Select only the features you need. Pass an explicit feature array to avoid loading unused code:
import { citationFeature, headingFeature, paragraphFeature, markFormattingFeature }
from '@sciflow/editor-core';
editor.configureFeatures([
citationFeature,
headingFeature,
paragraphFeature,
markFormattingFeature,
]);
Features you omit (e.g., table, math, annotation) won't register their plugins.
2. Lazy-load MathJax. Only add the MathJax script tag when a document actually contains math nodes:
if (doc.doc.content.some(n => n.type === 'math' ||
n.content?.some(c => c.type === 'math'))) {
const s = document.createElement('script');
s.defer = true;
s.src = 'https://cdn.jsdelivr.net/npm/mathjax@4/tex-svg.js';
document.head.appendChild(s);
}
3. Use import maps or a bundler. If you import from @sciflow/editor-core directly (instead of using the pre-built bundle), your bundler's tree-shaking can eliminate unused feature modules.
Compression & Caching¶
For production deployments:
Version the bundle filename or use a query-string hash so you can set long cache lifetimes without stale-content risks.
Large Documents¶
ProseMirror renders the entire document in a single contenteditable container. For most manuscripts (up to ~100 pages) this performs well. For very large documents:
1. Minimize plugin overhead. Each plugin runs on every transaction. Avoid plugins that traverse the full document tree in apply(). Use docChanged guards:
2. Debounce external listeners. The editor-change event fires on every transaction. Debounce persistence and UI updates:
let timeout;
editor.addEventListener('editor-change', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => persist(e.detail), 300);
});
3. Avoid full-document serialization on every change. If you only need to detect changes (not persist the full snapshot), check event.detail.operations for incremental diffs instead of serializing the entire doc.
4. Profile with Chrome DevTools. Use the Performance tab to identify expensive transaction handlers. Look for long dispatchTransaction frames. ProseMirror's DevTools extension can help inspect plugin state.
Rendering Performance¶
Shadow DOM. The editor uses Shadow DOM for style encapsulation. Style injection via setShadowStyles() is synchronous and cheap — but avoid injecting large stylesheets on every transaction.
Decorations. If you write custom plugins that create many decorations (highlights, widgets), prefer DecorationSet.map() over rebuilding from scratch. ProseMirror's decoration set is designed for efficient incremental updates.
Node views. Custom node views (Lit components or vanilla) are only re-rendered when their node changes. Avoid side effects in update() that aren't gated on actual attribute changes.
Build Configuration¶
SciFlow targets ES2022 and ships as ESM only. This means:
- Modern browsers only (Chrome 94+, Firefox 93+, Safari 15+, Edge 94+)
- No CommonJS or UMD fallback is provided
- If you need to support older browsers, run the bundle through your own Babel/SWC pipeline with appropriate polyfills
The Vite build configuration uses:
CSS is split into a separate file (sciflow-editor.css) that can be loaded independently or inlined.