Performance Optimization
This guide covers strategies for optimizing Vizel editor performance in production applications.
Bundle Size Optimization
Disable Unused Features
Vizel enables all features by default. Disable features you don't use to reduce bundle size and extension processing:
const editor = useVizelEditor({
features: {
mathematics: false, // KaTeX (~80KB gzipped)
diagram: false, // Mermaid (~40KB gzipped, lazy-loaded)
embed: false, // oEmbed/OGP processing
codeBlock: false, // lowlight/highlight.js (~20KB gzipped)
table: false,
dragHandle: false,
},
});Feature Size Impact
The following table shows approximate sizes (minified + gzipped) of optional features:
| Feature | Approximate Size | Dependencies |
|---|---|---|
mathematics | ~80KB gzipped | KaTeX |
diagram | ~40KB gzipped | Mermaid (lazy-loaded); GraphViz via @hpcc-js/wasm-graphviz adds additional size |
codeBlock | ~20KB gzipped | lowlight / highlight.js |
embed | ~5KB gzipped | Built-in oEmbed |
table | ~8KB gzipped | @tiptap/extension-table |
dragHandle | ~3KB gzipped | Built-in |
textColor | ~2KB gzipped | Built-in |
taskList | ~2KB gzipped | Built-in |
details | ~2KB gzipped | Built-in |
Measurement
Actual bundle sizes depend on your bundler configuration and tree-shaking. You can use tools like bundlephobia or your bundler's analysis output to measure exact sizes:
# Vite
npx vite-bundle-visualizer
# Webpack
npx webpack-bundle-analyzerLazy Loading
Vizel uses createLazyLoader() internally for optional dependencies like Mermaid. Vizel lazy-loads diagram rendering on first use, so simply having the feature enabled does not add to initial load time.
You can also lazy-load optional CSS stylesheets:
// Only import mathematics CSS when the feature is used
if (useMathematics) {
import('@vizel/core/mathematics.css');
}Code Block Language Optimization
By default, code blocks load all common languages. You can limit this to only the languages you need:
import { createLowlight } from 'lowlight';
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import css from 'highlight.js/lib/languages/css';
const lowlight = createLowlight();
lowlight.register('javascript', javascript);
lowlight.register('typescript', typescript);
lowlight.register('css', css);
const editor = useVizelEditor({
features: {
codeBlock: { lowlight },
},
});Or use the common subset instead of all:
import { createLowlight, common } from 'lowlight';
// ~40 common languages instead of all ~190
const lowlight = createLowlight(common);Large Document Handling
Character Count Limits
You can use the character count feature to enforce document size limits:
const editor = useVizelEditor({
features: {
characterCount: {
limit: 50000, // Maximum characters
},
},
});Optimize onUpdate Handlers
You should avoid expensive operations on every keystroke:
// Bad: Expensive serialization on every keystroke
onUpdate: ({ editor }) => {
setContent(editor.getJSON()); // Triggers re-render
localStorage.setItem('content',
JSON.stringify(editor.getJSON())); // Synchronous I/O
},
// Good: Use auto-save with debouncing
const editor = useVizelEditor({
onUpdate: ({ editor }) => {
// Only lightweight operations here
},
});
useVizelAutoSave(() => editor, {
debounceMs: 2000,
storage: 'localStorage',
key: 'my-content',
});Debounce State Updates
When syncing editor state to external state management:
import { useMemo, useState } from 'react';
import { useVizelEditor } from '@vizel/react';
function Editor() {
const [content, setContent] = useState(null);
// Debounce content updates
const debouncedSetContent = useMemo(
() => {
let timer: ReturnType<typeof setTimeout>;
return (json: unknown) => {
clearTimeout(timer);
timer = setTimeout(() => setContent(json), 300);
};
},
[]
);
const editor = useVizelEditor({
onUpdate: ({ editor }) => {
debouncedSetContent(editor.getJSON());
},
});
return <VizelEditor editor={editor} />;
}<script setup lang="ts">
import { shallowRef } from 'vue';
import { useVizelEditor, VizelEditor } from '@vizel/vue';
const content = shallowRef(null);
let timer: ReturnType<typeof setTimeout>;
const editor = useVizelEditor({
onUpdate: ({ editor }) => {
clearTimeout(timer);
timer = setTimeout(() => {
content.value = editor.getJSON();
}, 300);
},
});
</script>
<template>
<VizelEditor :editor="editor" />
</template><script lang="ts">
import { createVizelEditor, VizelEditor } from '@vizel/svelte';
let content = $state(null);
let timer: ReturnType<typeof setTimeout>;
const editor = createVizelEditor({
onUpdate: ({ editor }) => {
clearTimeout(timer);
timer = setTimeout(() => {
content = editor.getJSON();
}, 300);
},
});
</script>
<VizelEditor editor={editor.current} />Auto-Save Configuration
Debounce Timing
Choose a debounce value that balances responsiveness and performance:
| Use Case | Recommended debounceMs |
|---|---|
| Real-time collaboration | 500ms |
| General editing | 1000ms (default) |
| Large documents | 2000–3000ms |
| Server-side persistence | 3000–5000ms |
Storage Backend Selection
| Backend | Best For | Considerations |
|---|---|---|
localStorage | Quick prototyping | ~5MB limit, synchronous |
sessionStorage | Temporary drafts | Cleared on tab close |
| Custom (API) | Production apps | Async, no size limit |
See Auto-Save for detailed configuration.
Framework-Specific Tips
React
- Avoid creating a new
onUpdatefunction on every render. UseuseCallbackor define the function outside the component. - Wrap toolbar components that receive the editor instance with
React.memo - Do not store the entire editor state in React state. Use the
useVizelEditorStatehook for reactive state tracking.
import { useVizelEditor, useVizelEditorState } from '@vizel/react';
function Editor() {
const editor = useVizelEditor({});
// Reactive state without manual onUpdate tracking
const { characterCount, wordCount } = useVizelEditorState(() => editor);
return (
<div>
<VizelEditor editor={editor} />
<span>{characterCount} characters, {wordCount} words</span>
</div>
);
}Vue
- Use
shallowReffor the editor instance (useVizelEditordoes this automatically) - Avoid
watchwithdeep: trueon the editor. Use theuseVizelEditorStatecomposable instead. - Use
computedfor derived values from editor state
<script setup lang="ts">
import { useVizelEditor, useVizelEditorState } from '@vizel/vue';
const editor = useVizelEditor({});
// Reactive state tracking
const { characterCount, wordCount } = useVizelEditorState(() => editor.value);
</script>Svelte
- Svelte 5 runes handle reactivity efficiently by default
- Use the
createVizelEditorStaterune for reactive state tracking - Avoid frequent
$effectcalls that read the editor state. Batch updates instead.
<script lang="ts">
import { createVizelEditor, createVizelEditorState } from '@vizel/svelte';
const editor = createVizelEditor({});
// Reactive state tracking
const editorState = createVizelEditorState(() => editor.current);
</script>
<span>{editorState.current.characterCount} characters</span>Production Checklist
Before deploying to production, verify the following optimizations:
- [ ] Disable unused features — Remove features your application does not use
- [ ] Optimize code block languages — Register only the languages you need
- [ ] Configure auto-save debouncing — Set an appropriate
debounceMsfor your use case - [ ] Debounce onUpdate handlers — Avoid expensive operations on every keystroke
- [ ] Set character count limits — Prevent excessively large documents if applicable
- [ ] Lazy load optional CSS — Import
mathematics.cssonly if you use math features - [ ] Enable compression — Configure gzip or brotli compression on your server
- [ ] Analyze bundle size — Use bundle analysis tools to identify optimization opportunities
- [ ] Test with realistic content — Profile with documents similar to production usage
Next Steps
- Configuration - Editor options reference
- Features - Feature-specific configuration
- Auto-Save - Auto-save configuration
- Troubleshooting - Common issues and solutions