Performance

How Echo Editor stays fast and how to keep your integration fast.

What the editor does for you

TechniqueEffect
Shared dependenciesTiptap, ProseMirror, Vue and the UI primitives are dependencies, not bundled. The main entry is ~49 KB gzipped (down from ~437 KB in 0.x) and your app keeps a single copy of Tiptap.
Lazy feature UIsLink / table / image / column menus, the AI menu, menubar, dialogs (preview, source code, special characters) and find & replace are separate chunks, downloaded only when their extension is registered or the dialog is first opened.
Lazy syntax highlightingprism-code-editor and its grammars (~26 KB gzipped) load on the first code block.
Lazy languagesOnly English and Chinese are bundled; other languages are fetched on demand.
Frame-batched toolbarToolbar and bubble-menu state are recomputed at most once per animation frame, only for transactions that change the document, selection or stored marks — not on every reactive tick.
Cheap v-modelSerialising is throttled (200 ms, flushed on blur), echoes of the emitted value are detected without re-serialising, and deep watchers are avoided.
Batched streamingAI responses render once per frame; code blocks write back to the document in batches.
Per-editor stateNo global listeners per component: theme, locale and stores are shared or scoped.

Tips for your app

Create extensions once

// ✅ module scope or setup scope
const extensions = [BaseKit, Bold, Italic]

// ❌ creates new extension instances on every evaluation
const extensions = computed(() => [BaseKit, Bold, Italic])

The extension list is read when the editor is created; to switch sets, remount with a key.

Use useEditorState for custom UI

Reading editor.state in a computed re-runs on every transaction. useEditorState evaluates once per frame and only when something relevant changed:

import { useEditorState } from 'vue-echo-editor'

const isBold = useEditorState(editor, e => e.isActive('bold'))
const words = useEditorState(editor, e => e.storage.characterCount.words())

Prefer JSON for large documents

getJSON() is cheaper than getHTML(), and JSON round-trips losslessly. Use output="json" for very large documents and render HTML on the server when needed.

Register only what you use

Each extension adds schema, plugins and a toolbar control. The minimal preset is a good starting point.

Keep v-model handlers light

Don't send every update to the server — debounce saves or save on blur:

import { useDebounceFn } from '@vueuse/core'

const save = useDebounceFn((html: string) => api.save(html), 1000)