Skip to main content

Overview

Yoopta Editor is designed for performance, but large documents and complex plugins can impact rendering speed and user experience. This guide covers strategies to optimize your editor implementation.

Key Performance Factors

1. Document Size

  • Small docs (< 50 blocks): No optimization needed
  • Medium docs (50-200 blocks): Basic optimizations recommended
  • Large docs (200-1000 blocks): Advanced optimizations required
  • Very large docs (1000+ blocks): Consider pagination or lazy loading

2. Plugin Complexity

  • Simple plugins (Paragraph, Headings): Minimal impact
  • Media plugins (Image, Video, Embed): Moderate impact
  • Complex plugins (Table, Code with syntax highlighting): Higher impact

Optimization Strategies

Memoization

Memoize Editor Instance

Always memoize the editor instance to prevent recreation:
import { useMemo } from 'react';
import { createYooptaEditor } from '@yoopta/editor';

function Editor() {
  // Good: Memoized editor
  const editor = useMemo(
    () => createYooptaEditor({
      plugins: PLUGINS,
      marks: MARKS,
    }),
    [] // Empty deps - create only once
  );
  
  return <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>;
}
Never recreate the editor on every render:
// Bad: Editor recreated on every render
function Editor() {
  const editor = createYooptaEditor({ /* ... */ });
  return <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>;
}

Memoize Plugins Array

Memoize your plugins array if it’s defined in the component:
import Paragraph from '@yoopta/paragraph';
import HeadingOne from '@yoopta/headings/HeadingOne';
import { applyTheme } from '@yoopta/themes-shadcn';

function Editor() {
  // Good: Memoized plugins
  const plugins = useMemo(
    () => applyTheme([Paragraph, HeadingOne, /* ... */]),
    []
  );
  
  const editor = useMemo(
    () => createYooptaEditor({ plugins, marks: MARKS }),
    [plugins]
  );
  
  return <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>;
}

// Better: Define outside component
const PLUGINS = applyTheme([Paragraph, HeadingOne, /* ... */]);

function Editor() {
  const editor = useMemo(
    () => createYooptaEditor({ plugins: PLUGINS, marks: MARKS }),
    []
  );
  
  return <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>;
}

Batch Operations

Use batchOperations to group multiple changes:
// Bad: Multiple separate operations (triggers multiple re-renders)
function updateMultipleBlocks(editor: YooEditor) {
  editor.updateBlock('block-1', { /* ... */ });
  editor.updateBlock('block-2', { /* ... */ });
  editor.updateBlock('block-3', { /* ... */ });
}

// Good: Batched operations (single re-render)
function updateMultipleBlocks(editor: YooEditor) {
  editor.batchOperations(() => {
    editor.updateBlock('block-1', { /* ... */ });
    editor.updateBlock('block-2', { /* ... */ });
    editor.updateBlock('block-3', { /* ... */ });
  });
}

Debounce Save Operations

Debounce expensive operations like saving to backend:
import { useMemo, useCallback } from 'react';
import debounce from 'lodash.debounce';

function Editor() {
  const editor = useMemo(() => createYooptaEditor({ /* ... */ }), []);
  
  // Debounced save function
  const saveToBackend = useCallback(
    debounce((value: YooptaContentValue) => {
      fetch('/api/documents', {
        method: 'POST',
        body: JSON.stringify(value),
      });
    }, 1000), // Save after 1 second of inactivity
    []
  );
  
  const handleChange = useCallback(
    (value: YooptaContentValue) => {
      saveToBackend(value);
    },
    [saveToBackend]
  );
  
  return (
    <YooptaEditor editor={editor} onChange={handleChange}>
      {/* ... */}
    </YooptaEditor>
  );
}

Loading Strategies

Initial Load Optimization

Load Content Asynchronously

import { useEffect, useState } from 'react';

function Editor() {
  const [isLoading, setIsLoading] = useState(true);
  const editor = useMemo(() => createYooptaEditor({ plugins: PLUGINS }), []);
  
  useEffect(() => {
    // Load content asynchronously
    fetch('/api/document/123')
      .then(res => res.json())
      .then(data => {
        editor.setEditorValue(data.content);
        setIsLoading(false);
      });
  }, [editor]);
  
  if (isLoading) {
    return <LoadingSkeleton />;
  }
  
  return <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>;
}

Progressive Rendering

Render editor shell first, load plugins progressively:
import { lazy, Suspense } from 'react';

// Lazy load heavy plugins
const CodePlugin = lazy(() => import('@yoopta/code'));
const TablePlugin = lazy(() => import('@yoopta/table'));

function Editor() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <EditorWithLazyPlugins />
    </Suspense>
  );
}

Pagination for Large Documents

Implement virtual scrolling or pagination:
function Editor({ documentId }: { documentId: string }) {
  const [page, setPage] = useState(0);
  const [blocks, setBlocks] = useState<YooptaContentValue>({});
  
  const editor = useMemo(
    () => createYooptaEditor({ plugins: PLUGINS, value: blocks }),
    []
  );
  
  useEffect(() => {
    // Load blocks in chunks
    fetch(`/api/document/${documentId}/blocks?page=${page}&limit=100`)
      .then(res => res.json())
      .then(data => {
        setBlocks(prev => ({ ...prev, ...data }));
      });
  }, [documentId, page]);
  
  return (
    <>
      <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>
      <Pagination page={page} onChange={setPage} />
    </>
  );
}

Media Optimization

Lazy Load Images

Use native lazy loading for images:
import Image from '@yoopta/image';

const OptimizedImage = Image.extend({
  elements: {
    image: {
      render: (props) => (
        <img
          {...props.attributes}
          src={props.element.props?.src}
          alt={props.element.props?.alt}
          loading="lazy" // Native lazy loading
          decoding="async" // Async decode
        />
      ),
    },
  },
});

Image Optimization

Optimize images before upload:
import Image from '@yoopta/image';

const OptimizedImage = Image.extend({
  options: {
    onUpload: async (file: File) => {
      // Compress image before upload
      const compressed = await compressImage(file, {
        maxWidth: 1200,
        maxHeight: 1200,
        quality: 0.8,
      });
      
      // Upload compressed image
      const formData = new FormData();
      formData.append('file', compressed);
      
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });
      
      const { url } = await response.json();
      return { src: url };
    },
  },
});

function compressImage(
  file: File,
  options: { maxWidth: number; maxHeight: number; quality: number }
): Promise<Blob> {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = URL.createObjectURL(file);
    
    img.onload = () => {
      const canvas = document.createElement('canvas');
      let { width, height } = img;
      
      // Resize if needed
      if (width > options.maxWidth || height > options.maxHeight) {
        const ratio = Math.min(
          options.maxWidth / width,
          options.maxHeight / height
        );
        width *= ratio;
        height *= ratio;
      }
      
      canvas.width = width;
      canvas.height = height;
      
      const ctx = canvas.getContext('2d')!;
      ctx.drawImage(img, 0, 0, width, height);
      
      canvas.toBlob(
        (blob) => resolve(blob!),
        'image/jpeg',
        options.quality
      );
    };
  });
}

Video Optimization

Use poster images and lazy loading for videos:
import Video from '@yoopta/video';

const OptimizedVideo = Video.extend({
  elements: {
    video: {
      render: (props) => (
        <video
          {...props.attributes}
          src={props.element.props?.src}
          poster={props.element.props?.poster} // Thumbnail
          preload="metadata" // Load metadata only
          loading="lazy"
        >
          {props.children}
        </video>
      ),
    },
  },
});

Code Plugin Optimization

Lazy Load Syntax Highlighter

import { lazy, Suspense } from 'react';
import Code from '@yoopta/code';

const SyntaxHighlighter = lazy(() => import('react-syntax-highlighter'));

const OptimizedCode = Code.extend({
  elements: {
    code: {
      render: (props) => (
        <Suspense fallback={<pre>{props.element.props?.code}</pre>}>
          <SyntaxHighlighter language={props.element.props?.language}>
            {props.element.props?.code}
          </SyntaxHighlighter>
        </Suspense>
      ),
    },
  },
});

Limit Syntax Highlighting

Disable highlighting for very long code blocks:
const OptimizedCode = Code.extend({
  elements: {
    code: {
      render: (props) => {
        const code = props.element.props?.code || '';
        const maxLines = 500;
        
        // Fallback to plain text for very long code
        if (code.split('\n').length > maxLines) {
          return (
            <pre {...props.attributes}>
              <code>{code}</code>
            </pre>
          );
        }
        
        return <SyntaxHighlighter>{code}</SyntaxHighlighter>;
      },
    },
  },
});

History Optimization

Limit History Stack Size

Control undo/redo history size:
const editor = createYooptaEditor({
  plugins: PLUGINS,
  marks: MARKS,
});

// Limit history stack to 50 operations
const MAX_HISTORY = 50;

editor.on('change', () => {
  const undoStack = editor.historyStack.undos;
  if (undoStack.length > MAX_HISTORY) {
    undoStack.splice(0, undoStack.length - MAX_HISTORY);
  }
});

Disable History for Bulk Operations

function importLargeDocument(editor: YooEditor, blocks: YooptaBlockData[]) {
  editor.withoutSavingHistory(() => {
    editor.batchOperations(() => {
      blocks.forEach(block => {
        editor.insertBlock(block);
      });
    });
  });
}

Monitoring Performance

Measure Render Time

import { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
  
  if (actualDuration > 100) {
    console.warn('Slow render detected!');
  }
}

function Editor() {
  return (
    <Profiler id="YooptaEditor" onRender={onRenderCallback}>
      <YooptaEditor editor={editor}>{/* ... */}</YooptaEditor>
    </Profiler>
  );
}

Track Document Size

function useDocumentMetrics(editor: YooEditor) {
  const [metrics, setMetrics] = useState({
    blockCount: 0,
    characterCount: 0,
    estimatedSize: 0,
  });
  
  useEffect(() => {
    const unsubscribe = editor.on('change', ({ value }) => {
      const blocks = Object.values(value);
      const plainText = editor.getPlainText();
      
      setMetrics({
        blockCount: blocks.length,
        characterCount: plainText.length,
        estimatedSize: new Blob([JSON.stringify(value)]).size,
      });
    });
    
    return unsubscribe;
  }, [editor]);
  
  return metrics;
}

Best Practices

Use React DevTools Profiler to identify actual bottlenecks before optimizing. Don’t optimize prematurely.
Only include plugins you actually use. Each plugin adds overhead:
// Bad: Including all plugins
const PLUGINS = [All30Plugins];

// Good: Only what you need
const PLUGINS = [Paragraph, HeadingOne, HeadingTwo];
Always use production builds in production:
NODE_ENV=production npm run build
Development builds include extra checks and warnings that slow down performance.
Use tree-shaking and code splitting:
// Good: Import specific plugins
import HeadingOne from '@yoopta/headings/HeadingOne';

// Avoid: Importing everything
import * as Headings from '@yoopta/headings';
Use tools like Sentry or LogRocket to monitor real-world performance:
  • Track slow renders
  • Monitor bundle size
  • Measure time to interactive (TTI)

Performance Checklist

  • Editor instance is memoized
  • Plugins array is defined outside component or memoized
  • onChange handler is debounced for expensive operations
  • Large documents use pagination or virtual scrolling
  • Images are optimized and lazy-loaded
  • Videos use poster images and metadata preload
  • Code highlighting is disabled for very long blocks
  • Batch operations are used for bulk updates
  • History stack size is limited
  • Production build is used in production
  • Performance monitoring is in place

Troubleshooting

Common issues and solutions

Best Practices

General best practices

Build docs developers (and LLMs) love