Skip to content

Typing in a TextEditorField gets progressively slower on a form holding several Trix editors #7786

Description

@LaurentMarquet

Typing in a TextEditorField gets progressively slower on a form holding several Trix editors

Symptom

On an edit form holding many Trix editors, typing lags, and the lag grows with every character typed. On one of our pages (31 editors, ~400 form fields), a single character insertion measured, five times in a row from a fresh page load:

142 ms → 228 ms → 294 ms → 349 ms → 398 ms

It never comes back down; only a page reload resets it.

Cause

assets/js/field-text-editor.js builds one TextEditorField per editor on the page:

// field-text-editor.js:11
document.addEventListener('trix-before-initialize', () => {
    new TextEditorField();
});

trix-before-initialize fires once per <trix-editor> element, so a form with N editors ends up with N instances. Each instance adds two more document-level trix-change listeners:

  • #processRequiredAttribute() (line 67) → #markInvalidFormFields(), which re-scans .ea-edit-form with querySelectorAll('input,select,textarea') and calls setCustomValidity() on every field.
  • #enableFormChangesDetection() (line 75) → new DirtyForm(form).

DirtyForm's constructor walks the whole form.elements collection and attaches a new change + input listener to each field. Those listeners are never removed, and the bound callbacks are new function objects each time, so nothing is deduplicated.

So one keystroke costs N_editors × N_fields, and it also leaves N_editors × N_fields × 2 extra listeners behind, which the next keystroke pays for again. Hence the linear growth above.

Repro

  1. Any CRUD edit page with, say, 20+ TextEditorField instances (ours are nested inside a CollectionField).
  2. Open the console and run this a few times in a row:
const e = document.querySelector('trix-editor');
const t = performance.now(); e.editor.insertString('a'); console.log(performance.now() - t);

The number rises on every call.

Suggested fix

Two independent points:

  1. Build TextEditorField once. #processRequiredAttribute() and #handleFormSubmission() already query the whole document / the submitted form, so a single instance covers every editor. A guard on the listener would do:
let textEditorField = null;
document.addEventListener('trix-before-initialize', () => {
    textEditorField ??= new TextEditorField();
});

(#processRequiredAttribute() would then need to run again for editors added later — an ea.collection.item-added listener, or re-running just the per-element part on trix-before-initialize.)

  1. Drop #enableFormChangesDetection() entirely. assets/js/form.js already instantiates DirtyForm once on DOMContentLoaded for .ea-new-form / .ea-edit-form, and DirtyForm handles <trix-editor> natively (it collects form.querySelectorAll('trix-editor') and binds trix-change on them). The rebuild here looks redundant — and because it re-snapshots initialValues on every keystroke, it arguably weakens the very detection it is meant to provide.

Point 2 alone removes the unbounded growth; point 1 removes the × N_editors factor.

Workaround

For anyone hitting this before a fix lands: keeping trix-change inside the editor it came from (stopPropagation() on the <trix-editor> element, which still lets form.js's own DirtyForm see it — it listens on that element, not on the document) brought that same measurement from 142 ms down to ~15 ms. It does mean taking over #markInvalidFormFields() for those fields, which is two lines given data-ea-trix-is-required is already on the textarea.

Versions

  • easycorp/easyadmin-bundle 5.5.0
  • Symfony 8.x, PHP 8.4
  • Chrome

Investigation and write-up done with Claude Code, on a real project of mine. The timings above are actual measurements I ran in my own browser console, not estimates.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions