Skip to content

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:

typescript
const editor = useVizelEditor({
  features: {
    content: {
      mathematics: false,  // KaTeX (~80KB gzipped)
      diagram: false,      // Mermaid (~40KB gzipped, lazy-loaded)
      embed: false,        // oEmbed/OGP processing
      table: false,
    },
    interaction: {
      dragHandle: false,
    },
  },
});

TIP

Code blocks, links, and Markdown import/export are part of the always-on core and cannot be disabled. To restrict the syntax-highlighter language set, pass a custom Lowlight instance through extensions instead.

Feature Size Impact ​

The following table shows approximate sizes (minified + gzipped) of optional features:

FeaturePathApproximate SizeDependencies
mathematicscontent~80KB gzippedKaTeX
diagramcontent~40KB gzippedMermaid (lazy-loaded); GraphViz via @hpcc-js/wasm-graphviz adds additional size
embedcontent~5KB gzippedBuilt-in oEmbed
tablecontent~8KB gzipped@tiptap/extension-table
dragHandleinteraction~3KB gzippedBuilt-in
textColorcontent~2KB gzippedBuilt-in
taskListcontent~2KB gzippedBuilt-in
detailscontent~2KB gzippedBuilt-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:

bash
# Vite
npx vite-bundle-visualizer

# Webpack
npx webpack-bundle-analyzer

Lazy 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:

typescript
// 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:

typescript
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);

// Code block syntax highlighting is part of the always-on core. Pass a
// restricted Lowlight instance through the `extensions` array.
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';

const editor = useVizelEditor({
  extensions: [CodeBlockLowlight.configure({ lowlight })],
});

Or use the common subset instead of all:

typescript
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:

typescript
const editor = useVizelEditor({
  features: {
    interaction: {
      characterCount: {
        limit: 50000, // Maximum characters
      },
    },
  },
});

Optimize onUpdate Handlers ​

You should avoid expensive operations on every keystroke:

typescript
// 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:

tsx
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} />;
}
vue
<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>
svelte
<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 CaseRecommended debounceMs
Real-time collaboration500ms
General editing1000ms (default)
Large documents2000–3000ms
Server-side persistence3000–5000ms

Storage Backend Selection ​

BackendBest ForConsiderations
localStorageQuick prototyping~5MB limit, synchronous
sessionStorageTemporary draftsCleared on tab close
Custom (API)Production appsAsync, no size limit

See Editor: Auto-save for detailed configuration.


Framework-Specific Tips ​

React ​

  • Avoid creating a new onUpdate function on every render. Use useCallback or 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 useVizelEditorState hook for reactive state tracking.
tsx
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 shallowRef for the editor instance (useVizelEditor does this automatically)
  • Avoid watch with deep: true on the editor. Use the useVizelEditorState composable instead.
  • Use computed for derived values from editor state
vue
<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 createVizelEditorState rune for reactive state tracking
  • Avoid frequent $effect calls that read the editor state. Batch updates instead.
svelte
<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:


Next Steps ​

Released under the MIT License.