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
- Any CRUD edit page with, say, 20+
TextEditorField instances (ours are nested inside a CollectionField).
- 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:
- 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.)
- 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.
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:
It never comes back down; only a page reload resets it.
Cause
assets/js/field-text-editor.jsbuilds oneTextEditorFieldper editor on the page:trix-before-initializefires once per<trix-editor>element, so a form with N editors ends up with N instances. Each instance adds two more document-leveltrix-changelisteners:#processRequiredAttribute()(line 67) →#markInvalidFormFields(), which re-scans.ea-edit-formwithquerySelectorAll('input,select,textarea')and callssetCustomValidity()on every field.#enableFormChangesDetection()(line 75) →new DirtyForm(form).DirtyForm's constructor walks the wholeform.elementscollection and attaches a newchange+inputlistener 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 leavesN_editors × N_fields × 2extra listeners behind, which the next keystroke pays for again. Hence the linear growth above.Repro
TextEditorFieldinstances (ours are nested inside aCollectionField).The number rises on every call.
Suggested fix
Two independent points:
TextEditorFieldonce.#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:(
#processRequiredAttribute()would then need to run again for editors added later — anea.collection.item-addedlistener, or re-running just the per-element part ontrix-before-initialize.)#enableFormChangesDetection()entirely.assets/js/form.jsalready instantiatesDirtyFormonce onDOMContentLoadedfor.ea-new-form/.ea-edit-form, andDirtyFormhandles<trix-editor>natively (it collectsform.querySelectorAll('trix-editor')and bindstrix-changeon them). The rebuild here looks redundant — and because it re-snapshotsinitialValueson 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_editorsfactor.Workaround
For anyone hitting this before a fix lands: keeping
trix-changeinside the editor it came from (stopPropagation()on the<trix-editor>element, which still letsform.js's ownDirtyFormsee 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 givendata-ea-trix-is-requiredis already on the textarea.Versions
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.